maven: fix test mocks for CalendarEvents interface (verification)

- Add CalendarEvents method to recordingAPI in auth_test.go
- Add CalendarEvents method to fakeCore in handlers_test.go

Co-Authored-By: opencode <opencode@anthropic.com>
This commit is contained in:
kami
2026-07-06 04:20:16 +04:00
parent 880715fad4
commit d52f60c54e
71 changed files with 287 additions and 280 deletions
+3 -3
View File
@@ -21,8 +21,8 @@
// delivery end-to-end. The reference for "the most-recently-active client // delivery end-to-end. The reference for "the most-recently-active client
// plays it": run one, fire a tick, see the file appear. // plays it": run one, fire a tick, see the file appear.
// //
// mavenclient -listen -out-prefix /tmp/maven-nudge- // mavenclient -listen -out-prefix /tmp/maven-nudge-
// # then trigger a tick; /tmp/maven-nudge-1.wav, -2.wav ... appear // # then trigger a tick; /tmp/maven-nudge-1.wav, -2.wav ... appear
package main package main
import ( import (
@@ -160,4 +160,4 @@ func jsonUnmarshal(b []byte, v any) error { return json.Unmarshal(b, v) }
var _ = strconv.Atoi var _ = strconv.Atoi
var _ io.Reader = (io.Reader)(nil) var _ io.Reader = (io.Reader)(nil)
var _ = net.IPv4 var _ = net.IPv4
var _ = time.Second var _ = time.Second
+1 -1
View File
@@ -230,4 +230,4 @@ func run(args []string) error {
wg.Wait() wg.Wait()
log.Printf("mavend: bye") log.Printf("mavend: bye")
return nil return nil
} }
+3 -3
View File
@@ -45,8 +45,8 @@ type tickLoop struct {
phraser phraser.Phraser phraser phraser.Phraser
rules []loop.Rule rules []loop.Rule
tickInterval time.Duration tickInterval time.Duration
repeatInterval time.Duration repeatInterval time.Duration
autotuneInterval time.Duration // 0 ⇒ autotune disabled (gatherer falls back to static Base) autotuneInterval time.Duration // 0 ⇒ autotune disabled (gatherer falls back to static Base)
// digestCfg — the digest/batching config. nil ⇒ every nudge is sent // digestCfg — the digest/batching config. nil ⇒ every nudge is sent
@@ -429,4 +429,4 @@ func toIPCGateDetail(d loop.GateDetail) ipc.GateDetail {
Presence: d.Presence, Presence: d.Presence,
InertKeysMissing: d.InertKeysMissing, InertKeysMissing: d.InertKeysMissing,
} }
} }
+1 -1
View File
@@ -134,7 +134,7 @@ func TestTickCooldownSuppressesSecondSend(t *testing.T) {
sink := &fakeSink{} sink := &fakeSink{}
tl := newTestTickLoop(t, st, sink, nil) tl := newTestTickLoop(t, st, sink, nil)
tl.tick(ctx, now) // fires tl.tick(ctx, now) // fires
tl.tick(ctx, now.Add(time.Minute)) // still within 30m cooldown ⇒ suppressed tl.tick(ctx, now.Add(time.Minute)) // still within 30m cooldown ⇒ suppressed
if len(sink.sends) != 1 { if len(sink.sends) != 1 {
+15 -15
View File
@@ -50,17 +50,17 @@ import (
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"strconv" "strconv"
"strings"
"sync" "sync"
"time" "time"
"github.com/kami/maven/internal/audio" "github.com/kami/maven/internal/audio"
"github.com/kami/maven/internal/memory"
"github.com/kami/maven/internal/config" "github.com/kami/maven/internal/config"
"github.com/kami/maven/internal/delivery" "github.com/kami/maven/internal/delivery"
"github.com/kami/maven/internal/delivery/voicesink" "github.com/kami/maven/internal/delivery/voicesink"
"github.com/kami/maven/internal/ipc" "github.com/kami/maven/internal/ipc"
"github.com/kami/maven/internal/memory"
"github.com/kami/maven/internal/phraser" "github.com/kami/maven/internal/phraser"
"github.com/kami/maven/internal/router" "github.com/kami/maven/internal/router"
"github.com/kami/maven/internal/stt" "github.com/kami/maven/internal/stt"
@@ -207,18 +207,18 @@ func wireVoice(cfg *config.Config, coreAPI ipc.CoreAPI, phr phraser.Phraser) (*v
// ----- the handler (the reactive path; closes over stt / tts / router / coreAPI / memory) ----- // ----- the handler (the reactive path; closes over stt / tts / router / coreAPI / memory) -----
h := &reactiveHandler{ h := &reactiveHandler{
stt: transcriber, stt: transcriber,
tts: synthesizer, tts: synthesizer,
router: rtr, router: rtr,
embedder: emb, embedder: emb,
api: coreAPI, api: coreAPI,
tools: exec, tools: exec,
phraser: phr, phraser: phr,
replier: voice.NewStubReplier(), replier: voice.NewStubReplier(),
now: time.Now, now: time.Now,
weatherProvider: weatherProvider, weatherProvider: weatherProvider,
weatherLocation: weatherLocation, weatherLocation: weatherLocation,
memStore: memStore, memStore: memStore,
} }
// ----- the server (TCP listener) ----- // ----- the server (TCP listener) -----
@@ -968,4 +968,4 @@ func jsonStringImpl(s string) string {
} }
b = append(b, '"') b = append(b, '"')
return string(b) return string(b)
} }
+1 -1
View File
@@ -53,7 +53,7 @@ func TestParseMaxHandshake(t *testing.T) {
{"", 0}, {"", 0},
{"pubkeyAAA\t0\n", 0}, // never handshaked {"pubkeyAAA\t0\n", 0}, // never handshaked
{"pubkeyAAA\t1700000000\n", 1700000000}, {"pubkeyAAA\t1700000000\n", 1700000000},
{"pubkeyAAA\t1700000000\npubkeyBBB\t1700000500\n", 1700000500}, // max wins {"pubkeyAAA\t1700000000\npubkeyBBB\t1700000500\n", 1700000500}, // max wins
{"wg0\tpubkeyAAA\t1700000000\nwg0\tpubkeyBBB\t0\n", 1700000000}, // 'all' 3-field form {"wg0\tpubkeyAAA\t1700000000\nwg0\tpubkeyBBB\t0\n", 1700000000}, // 'all' 3-field form
} }
for _, c := range cases { for _, c := range cases {
+3 -3
View File
@@ -27,10 +27,10 @@ func TestGateReason(t *testing.T) {
gated bool // true ⇒ expect a non-empty reason (dropped) gated bool // true ⇒ expect a non-empty reason (dropped)
}{ }{
{"empty", nil, true}, {"empty", nil, true},
{"too short", sine(100, 0.5), true}, // 100ms < 300ms {"too short", sine(100, 0.5), true}, // 100ms < 300ms
{"long but silent", make([]float32, whisperSampleRate), true}, // 1s of zeros {"long but silent", make([]float32, whisperSampleRate), true}, // 1s of zeros
{"long but near-silent", sine(500, 0.005), true}, // rms ~0.0035 < floor {"long but near-silent", sine(500, 0.005), true}, // rms ~0.0035 < floor
{"real speech-ish", sine(500, 0.3), false}, // loud enough, long enough {"real speech-ish", sine(500, 0.3), false}, // loud enough, long enough
} }
for _, tc := range tests { for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
+1 -1
View File
@@ -126,4 +126,4 @@ func defaultSocket(name string) string {
return name return name
} }
return home + "/.local/share/maven/" + name return home + "/.local/share/maven/" + name
} }
+8 -8
View File
@@ -13,19 +13,19 @@ import (
) )
type piperHandler struct { type piperHandler struct {
piperPath string piperPath string
modelPath string modelPath string
configPath string configPath string
espeakData string espeakData string
tashkeelModel string tashkeelModel string
} }
func newPiperHandler(piperPath, modelPath, espeakData, tashkeelModel string) *piperHandler { func newPiperHandler(piperPath, modelPath, espeakData, tashkeelModel string) *piperHandler {
return &piperHandler{ return &piperHandler{
piperPath: piperPath, piperPath: piperPath,
modelPath: modelPath, modelPath: modelPath,
configPath: modelPath + ".json", configPath: modelPath + ".json",
espeakData: espeakData, espeakData: espeakData,
tashkeelModel: tashkeelModel, tashkeelModel: tashkeelModel,
} }
} }
+3
View File
@@ -116,6 +116,9 @@ func (f *fakeCore) RecentFacts(_ context.Context, _ int) ([]ipc.Fact, error) {
} }
return f.facts, nil return f.facts, nil
} }
func (f *fakeCore) CalendarEvents(_ context.Context, _, _ time.Time) ([]ipc.Fact, error) {
return nil, nil
}
func (f *fakeCore) RecentNudges(_ context.Context, _ int) ([]ipc.Nudge, error) { func (f *fakeCore) RecentNudges(_ context.Context, _ int) ([]ipc.Nudge, error) {
if f.nudgesErr != nil { if f.nudgesErr != nil {
+1 -1
View File
@@ -83,4 +83,4 @@ func (f Format) IsValid() bool {
f.Channels == PCM16kMono.Channels && f.Channels == PCM16kMono.Channels &&
f.SampleBits == PCM16kMono.SampleBits && f.SampleBits == PCM16kMono.SampleBits &&
f.Encoding == PCM16kMono.Encoding f.Encoding == PCM16kMono.Encoding
} }
+1 -1
View File
@@ -101,4 +101,4 @@ func TestPCMFromWAVRejectsNonCanonical(t *testing.T) {
if _, _, err := PCMFromWAV(wav); err == nil { if _, _, err := PCMFromWAV(wav); err == nil {
t.Fatalf("non-PCM format should error") t.Fatalf("non-PCM format should error")
} }
} }
+2 -2
View File
@@ -106,7 +106,7 @@ func WAVFromPCM(format Format, pcm []byte) ([]byte, error) {
// fmt chunk // fmt chunk
copy(out[12:16], []byte("fmt ")) copy(out[12:16], []byte("fmt "))
binary.LittleEndian.PutUint32(out[16:20], 16) // fmt chunk size binary.LittleEndian.PutUint32(out[16:20], 16) // fmt chunk size
binary.LittleEndian.PutUint16(out[20:22], 1) // PCM binary.LittleEndian.PutUint16(out[20:22], 1) // PCM
binary.LittleEndian.PutUint16(out[22:24], uint16(format.Channels)) binary.LittleEndian.PutUint16(out[22:24], uint16(format.Channels))
binary.LittleEndian.PutUint32(out[24:28], uint32(format.SampleRate)) binary.LittleEndian.PutUint32(out[24:28], uint32(format.SampleRate))
byteRate := uint32(format.SampleRate) * uint32(format.Channels) * uint32(format.SampleBits) / 8 byteRate := uint32(format.SampleRate) * uint32(format.Channels) * uint32(format.SampleBits) / 8
@@ -118,4 +118,4 @@ func WAVFromPCM(format Format, pcm []byte) ([]byte, error) {
copy(out[36:40], []byte("data")) copy(out[36:40], []byte("data"))
binary.LittleEndian.PutUint32(out[40:44], uint32(len(pcm))) binary.LittleEndian.PutUint32(out[40:44], uint32(len(pcm)))
return out, nil return out, nil
} }
+3
View File
@@ -354,6 +354,9 @@ func (r *recordingAPI) RecentOutcomes(_ context.Context, _ string, _ int) ([]str
func (r *recordingAPI) RecentFacts(_ context.Context, _ int) ([]ipc.Fact, error) { func (r *recordingAPI) RecentFacts(_ context.Context, _ int) ([]ipc.Fact, error) {
return nil, nil return nil, nil
} }
func (r *recordingAPI) CalendarEvents(_ context.Context, _, _ time.Time) ([]ipc.Fact, error) {
return nil, nil
}
func (r *recordingAPI) RecentNudges(_ context.Context, _ int) ([]ipc.Nudge, error) { func (r *recordingAPI) RecentNudges(_ context.Context, _ int) ([]ipc.Nudge, error) {
return nil, nil return nil, nil
} }
+10 -10
View File
@@ -186,8 +186,8 @@ type VoiceConfig struct {
// WeatherConfig configures the weather provider for voice queries. // WeatherConfig configures the weather provider for voice queries.
type WeatherConfig struct { type WeatherConfig struct {
Provider string `json:"provider,omitempty"` // "open-meteo" or "" → stub Provider string `json:"provider,omitempty"` // "open-meteo" or "" → stub
DefaultLocation string `json:"default_location,omitempty"` // e.g. "Moscow" DefaultLocation string `json:"default_location,omitempty"` // e.g. "Moscow"
} }
// ToolConfig — one enabled tool. Name is the spoken verb ("restart"); Cmd is // ToolConfig — one enabled tool. Name is the spoken verb ("restart"); Cmd is
@@ -223,11 +223,11 @@ type DigestConfig struct {
// - NCtx defaults to 2048. // - NCtx defaults to 2048.
// - Timeout defaults to 30s per request. // - Timeout defaults to 30s per request.
type PhraserConfig struct { type PhraserConfig struct {
ModelPath string `json:"model_path"` ModelPath string `json:"model_path"`
BinPath string `json:"bin_path,omitempty"` BinPath string `json:"bin_path,omitempty"`
Listen string `json:"listen,omitempty"` Listen string `json:"listen,omitempty"`
NGpuLayers int `json:"n_gpu_layers,omitempty"` NGpuLayers int `json:"n_gpu_layers,omitempty"`
NCtx int `json:"n_ctx,omitempty"` NCtx int `json:"n_ctx,omitempty"`
Timeout Duration `json:"timeout,omitempty"` Timeout Duration `json:"timeout,omitempty"`
} }
@@ -237,9 +237,9 @@ type PhraserConfig struct {
// floor HashEmbedder stub. Model_path is the ONNX model file, tokenizer_path // floor HashEmbedder stub. Model_path is the ONNX model file, tokenizer_path
// is tokenizer.json (Unigram), lib_path is the ONNX Runtime shared library. // is tokenizer.json (Unigram), lib_path is the ONNX Runtime shared library.
type EmbedderConfig struct { type EmbedderConfig struct {
ModelPath string `json:"model_path,omitempty"` ModelPath string `json:"model_path,omitempty"`
TokenizerPath string `json:"tokenizer_path,omitempty"` TokenizerPath string `json:"tokenizer_path,omitempty"`
LibPath string `json:"lib_path,omitempty"` LibPath string `json:"lib_path,omitempty"`
} }
// WorkerConfig — a unix-socket worker module connection. Used by Stt and // WorkerConfig — a unix-socket worker module connection. Used by Stt and
@@ -428,4 +428,4 @@ func defaultRuntimeDir() string {
// /run/user/$UID is the typical answer; without XDG_RUNTIME_DIR, fall back // /run/user/$UID is the typical answer; without XDG_RUNTIME_DIR, fall back
// to the data dir (still works; just not tmpfs-clearance-on-reboot clean). // to the data dir (still works; just not tmpfs-clearance-on-reboot clean).
return filepath.Join(defaultDataDir()) return filepath.Join(defaultDataDir())
} }
+1 -1
View File
@@ -187,4 +187,4 @@ func TestDurationRoundTrip(t *testing.T) {
if d2 != d { if d2 != d {
t.Errorf("round-trip = %v, want %v", d2, d) t.Errorf("round-trip = %v, want %v", d2, d)
} }
} }
+1 -1
View File
@@ -17,4 +17,4 @@ func writeFile(t *testing.T, path, body string) error {
} }
} }
return os.WriteFile(path, []byte(body), 0o600) return os.WriteFile(path, []byte(body), 0o600)
} }
+6 -4
View File
@@ -5,17 +5,19 @@
// - routing = f(severity, presence). presence decides REACHABILITY; severity // - routing = f(severity, presence). presence decides REACHABILITY; severity
// decides INSISTENCE. need both. // decides INSISTENCE. need both.
// //
// | | present | away | // | | present | away |
// | sev12 (care) | voice | drop | // | sev12 (care) | voice | drop |
// | sev3 (ops soft) | voice | ntfy, once | // | sev3 (ops soft) | voice | ntfy, once |
// | sev4 (ops hard) | voice + ntfy | telegram, repeat til ack | // | sev4 (ops hard) | voice + ntfy | telegram, repeat til ack |
// //
// - sev ≤ 2 drops on away, sev ≥ 3 holds. "a missed water nudge is noise; // - sev ≤ 2 drops on away, sev ≥ 3 holds. "a missed water nudge is noise;
// a missed backup-failure isn't." // a missed backup-failure isn't."
//
// - away channels (ntfy/telegram) leave the box — the one path that crosses // - away channels (ntfy/telegram) leave the box — the one path that crosses
// "never phones home," through your relay. MINIMAL BODY — "disk low on // "never phones home," through your relay. MINIMAL BODY — "disk low on
// homesrv," not detail. don't make notifications a shoulder-surf exfil // homesrv," not detail. don't make notifications a shoulder-surf exfil
// surface. // surface.
//
// - reminders are a SEPARATE class — two delivery paths. reminders bypass // - reminders are a SEPARATE class — two delivery paths. reminders bypass
// the restraint gate ("wake me 7" fires in quiet hours; that's the point). // the restraint gate ("wake me 7" fires in quiet hours; that's the point).
// snooze still applies. voice when present, ntfy when away. fire once. // snooze still applies. voice when present, ntfy when away. fire once.
+1 -1
View File
@@ -84,7 +84,7 @@ func (f *fakeReminderCompleter) RescheduleReminder(_ context.Context, id int64,
} }
type fakeAck struct { type fakeAck struct {
acked map[string]bool acked map[string]bool
lastSent map[string]time.Time lastSent map[string]time.Time
} }
@@ -196,4 +196,4 @@ func (s *Sink) Send(ctx context.Context, d delivery.Sendable) error {
// token never leaves the sink, no logging. // token never leaves the sink, no logging.
func (s *Sink) sendMessageURL() string { func (s *Sink) sendMessageURL() string {
return s.base + "/bot" + s.cfg.BotToken + "/sendMessage" return s.base + "/bot" + s.cfg.BotToken + "/sendMessage"
} }
@@ -399,4 +399,4 @@ func TestReminderSendUsesSamePath(t *testing.T) {
if req.Text != "wake up" { if req.Text != "wake up" {
t.Fatalf("reminder text: want 'wake up', got %q", req.Text) t.Fatalf("reminder text: want 'wake up', got %q", req.Text)
} }
} }
+1 -1
View File
@@ -106,4 +106,4 @@ func (s *Sink) Send(ctx context.Context, send delivery.Sendable) error {
// Format.IsValid which is a method on the imported audio.Format). The alias // Format.IsValid which is a method on the imported audio.Format). The alias
// below keeps the import alive even if a future refactor moves the only // below keeps the import alive even if a future refactor moves the only
// reference. Today, the synthesizer's audio.Audio directly flows through. // reference. Today, the synthesizer's audio.Audio directly flows through.
var _ = audio.PCM16kMono var _ = audio.PCM16kMono
-2
View File
@@ -105,5 +105,3 @@ func InheritSlots(prev, cur Slots) Slots {
} }
return out return out
} }
+20 -20
View File
@@ -12,14 +12,14 @@ import (
// Fact — one observation. Ts is valid-time (true-as-of), as in store. // Fact — one observation. Ts is valid-time (true-as-of), as in store.
type Fact struct { type Fact struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Ts time.Time `json:"ts"` Ts time.Time `json:"ts"`
Kind string `json:"kind"` // "self" | "env" | "config" Kind string `json:"kind"` // "self" | "env" | "config"
Key string `json:"key"` Key string `json:"key"`
Value string `json:"value"` // raw json if structured Value string `json:"value"` // raw json if structured
Source string `json:"source"` // tap:*|infer:*|poll:*|ambient|promote|feedback Source string `json:"source"` // tap:*|infer:*|poll:*|ambient|promote|feedback
Confidence float64 `json:"confidence"` Confidence float64 `json:"confidence"`
VoidsID *int64 `json:"voids_id,omitempty"` VoidsID *int64 `json:"voids_id,omitempty"`
} }
// Bucket — presence hysteresis state: "present" | "away". // Bucket — presence hysteresis state: "present" | "away".
@@ -66,9 +66,9 @@ type Reminder struct {
// routing and tone. presence = reachability, NOT wakefulness (spec). Loop // routing and tone. presence = reachability, NOT wakefulness (spec). Loop
// reads probes + computes score itself; modules get the resolved snapshot. // reads probes + computes score itself; modules get the resolved snapshot.
type Presence struct { type Presence struct {
Bucket Bucket `json:"bucket"` Bucket Bucket `json:"bucket"`
Score float64 `json:"score"` Score float64 `json:"score"`
Updated time.Time `json:"updated"` Updated time.Time `json:"updated"`
} }
// WriteFactReq — the only state mutation a capture/tool module performs. // WriteFactReq — the only state mutation a capture/tool module performs.
@@ -315,19 +315,19 @@ func CallerFrom(ctx context.Context) (Caller, bool) {
// Sentinel errors. Mirror store's 1:1 so module code reads the same whether // Sentinel errors. Mirror store's 1:1 so module code reads the same whether
// in-process or over the wire. The store adapter translates store.* → these. // in-process or over the wire. The store adapter translates store.* → these.
var ( var (
ErrNoFact = errors.New("ipc: no fact for key") ErrNoFact = errors.New("ipc: no fact for key")
ErrConfidence = errors.New("ipc: confidence must be in (0.0, 1.0]") ErrConfidence = errors.New("ipc: confidence must be in (0.0, 1.0]")
ErrVoidsMissing = errors.New("ipc: voids_id does not reference an existing fact") ErrVoidsMissing = errors.New("ipc: voids_id does not reference an existing fact")
ErrNudgeNotFound = errors.New("ipc: nudge not found") ErrNudgeNotFound = errors.New("ipc: nudge not found")
ErrNudgeOutcome = errors.New("ipc: nudge already resolved") ErrNudgeOutcome = errors.New("ipc: nudge already resolved")
ErrReminderNotFound = errors.New("ipc: reminder not found") ErrReminderNotFound = errors.New("ipc: reminder not found")
ErrReminderState = errors.New("ipc: reminder not in a mutable state") ErrReminderState = errors.New("ipc: reminder not in a mutable state")
ErrUnknownMethod = errors.New("ipc: unknown method") ErrUnknownMethod = errors.New("ipc: unknown method")
ErrBadParams = errors.New("ipc: bad params") ErrBadParams = errors.New("ipc: bad params")
// ErrForbidden — the caller's authority doesn't cover this call. The // ErrForbidden — the caller's authority doesn't cover this call. The
// auth layer's only wire-exported verdict: surface caps the layer, or a // auth layer's only wire-exported verdict: surface caps the layer, or a
// write was out-of-scope, or step-up was required but not asserted. The // write was out-of-scope, or step-up was required but not asserted. The
// text ErrForbidden carries is derived during dispatch (from auth.ErrForbidden // text ErrForbidden carries is derived during dispatch (from auth.ErrForbidden
// via fmt.Errorf %w wrapping); the wire carries codeForbidden. // via fmt.Errorf %w wrapping); the wire carries codeForbidden.
ErrForbidden = errors.New("ipc: forbidden") ErrForbidden = errors.New("ipc: forbidden")
) )
+1 -1
View File
@@ -372,4 +372,4 @@ func (c *Client) RevertFact(ctx context.Context, key string) (int64, error) {
} }
// Compile-time check: *Client satisfies CoreAPI. // Compile-time check: *Client satisfies CoreAPI.
var _ CoreAPI = (*Client)(nil) var _ CoreAPI = (*Client)(nil)
+1 -1
View File
@@ -83,4 +83,4 @@ func readFrame(r io.Reader, v any) error {
return fmt.Errorf("ipc: unmarshal frame: %w", err) return fmt.Errorf("ipc: unmarshal frame: %w", err)
} }
return nil return nil
} }
+1 -1
View File
@@ -423,4 +423,4 @@ func mustJSON(v any) []byte {
panic(err) panic(err)
} }
return b return b
} }
+8 -8
View File
@@ -280,9 +280,9 @@ type Server struct {
api CoreAPI api CoreAPI
path string path string
ln net.Listener ln net.Listener
wg sync.WaitGroup wg sync.WaitGroup
done chan struct{} done chan struct{}
// Check — optional authorization hook. dispatch runs it BEFORE method // Check — optional authorization hook. dispatch runs it BEFORE method
// dispatch, with the raw params, so the auth layer can make verdicts // dispatch, with the raw params, so the auth layer can make verdicts
@@ -343,10 +343,10 @@ func Listen(path string, api CoreAPI) (*Server, error) {
return nil, fmt.Errorf("ipc: chmod socket: %w", err) return nil, fmt.Errorf("ipc: chmod socket: %w", err)
} }
return &Server{ return &Server{
api: api, api: api,
path: path, path: path,
ln: ln, ln: ln,
done: make(chan struct{}), done: make(chan struct{}),
}, nil }, nil
} }
@@ -754,4 +754,4 @@ func peerCaller(c net.Conn) (Caller, bool) {
return Caller{}, false return Caller{}, false
} }
return Caller{Uid: int32(cred.Uid), Pid: int32(cred.Pid)}, true return Caller{Uid: int32(cred.Uid), Pid: int32(cred.Pid)}, true
} }
+27 -27
View File
@@ -13,38 +13,38 @@ import (
type Method string type Method string
const ( const (
MethodWriteFact Method = "write_fact" MethodWriteFact Method = "write_fact"
MethodLatestFact Method = "latest_fact" MethodLatestFact Method = "latest_fact"
MethodLatestFactBySource Method = "latest_fact_by_source" MethodLatestFactBySource Method = "latest_fact_by_source"
MethodSince Method = "since" MethodSince Method = "since"
MethodPresence Method = "presence" MethodPresence Method = "presence"
MethodCreateReminder Method = "create_reminder" MethodCreateReminder Method = "create_reminder"
MethodMarkReminder Method = "mark_reminder" MethodMarkReminder Method = "mark_reminder"
MethodRecordNudge Method = "record_nudge" MethodRecordNudge Method = "record_nudge"
MethodResolveNudge Method = "resolve_nudge" MethodResolveNudge Method = "resolve_nudge"
MethodRecentOutcomes Method = "recent_outcomes" MethodRecentOutcomes Method = "recent_outcomes"
MethodRecentFacts Method = "recent_facts" MethodRecentFacts Method = "recent_facts"
MethodCalendarEvents Method = "calendar_events" MethodCalendarEvents Method = "calendar_events"
MethodRecentNudges Method = "recent_nudges" MethodRecentNudges Method = "recent_nudges"
MethodWriteNote Method = "write_note" MethodWriteNote Method = "write_note"
MethodQueryNotes Method = "query_notes" MethodQueryNotes Method = "query_notes"
MethodRecentNotes Method = "recent_notes" MethodRecentNotes Method = "recent_notes"
MethodProposeTool Method = "propose_tool" MethodProposeTool Method = "propose_tool"
MethodEnableTool Method = "enable_tool" MethodEnableTool Method = "enable_tool"
MethodDisableTool Method = "disable_tool" MethodDisableTool Method = "disable_tool"
MethodAssertStepUp Method = "assert_stepup" MethodAssertStepUp Method = "assert_stepup"
MethodLookupTool Method = "lookup_tool" MethodLookupTool Method = "lookup_tool"
MethodListTools Method = "list_tools" MethodListTools Method = "list_tools"
MethodRevertFact Method = "revert_fact" MethodRevertFact Method = "revert_fact"
MethodTickTrace Method = "tick_trace" MethodTickTrace Method = "tick_trace"
) )
// Request — one frame from module to core. Params is the JSON-encoded argument // Request — one frame from module to core. Params is the JSON-encoded argument
// struct for Method (see api.go for the per-method shapes). The server // struct for Method (see api.go for the per-method shapes). The server
// unmarshals Params based on Method; an unknown Method ⇒ ErrUnknownMethod. // unmarshals Params based on Method; an unknown Method ⇒ ErrUnknownMethod.
type Request struct { type Request struct {
Method Method `json:"m"` Method Method `json:"m"`
Params json.RawMessage `json:"p,omitempty"` Params json.RawMessage `json:"p,omitempty"`
} }
// Response — one frame from core back to module. Exactly one of Result/Error // Response — one frame from core back to module. Exactly one of Result/Error
@@ -52,7 +52,7 @@ type Request struct {
// scalar, a struct, or null for void methods). // scalar, a struct, or null for void methods).
type Response struct { type Response struct {
Result json.RawMessage `json:"r,omitempty"` Result json.RawMessage `json:"r,omitempty"`
Error *RpcError `json:"e,omitempty"` Error *RpcError `json:"e,omitempty"`
} }
// RpcError — a typed wire error. Code is one of the sentinel codes below; // RpcError — a typed wire error. Code is one of the sentinel codes below;
@@ -132,4 +132,4 @@ func rpcErr(err error) *RpcError {
return &RpcError{Code: c, Message: err.Error()} return &RpcError{Code: c, Message: err.Error()}
} }
return &RpcError{Code: c} return &RpcError{Code: c}
} }
+6 -6
View File
@@ -27,12 +27,12 @@ type RuleTrace struct {
// GateDetail — snapshot of the values the gate checked. // GateDetail — snapshot of the values the gate checked.
type GateDetail struct { type GateDetail struct {
SnoozeUntil *time.Time `json:"snooze_until,omitempty"` SnoozeUntil *time.Time `json:"snooze_until,omitempty"`
CooldownUntil *time.Time `json:"cooldown_until,omitempty"` CooldownUntil *time.Time `json:"cooldown_until,omitempty"`
QuietHours bool `json:"quiet_hours"` QuietHours bool `json:"quiet_hours"`
CalendarBusy bool `json:"calendar_busy"` CalendarBusy bool `json:"calendar_busy"`
Presence string `json:"presence"` // "present"|"away" Presence string `json:"presence"` // "present"|"away"
InertKeysMissing []string `json:"inert_keys_missing,omitempty"` InertKeysMissing []string `json:"inert_keys_missing,omitempty"`
} }
// ExplainGate runs the same checks as Gate() but records the first blocker. // ExplainGate runs the same checks as Gate() but records the first blocker.
+1 -1
View File
@@ -119,4 +119,4 @@ func ParseCooldownFact(f store.Fact) (time.Duration, bool) {
func MarshalCooldown(d time.Duration) string { func MarshalCooldown(d time.Duration) string {
b, _ := json.Marshal(int64(d)) // int64 marshal never errors b, _ := json.Marshal(int64(d)) // int64 marshal never errors
return string(b) return string(b)
} }
+4 -4
View File
@@ -97,10 +97,10 @@ func TestFeedbackKeyMatchesRuleName(t *testing.T) {
func TestParseCooldownFactRoundTrip(t *testing.T) { func TestParseCooldownFactRoundTrip(t *testing.T) {
d := 45 * time.Minute d := 45 * time.Minute
f := store.Fact{ f := store.Fact{
Key: "cooldown:water", Key: "cooldown:water",
Source: FeedbackSource, Source: FeedbackSource,
Value: MarshalCooldown(d), Value: MarshalCooldown(d),
Ts: refTime(), Ts: refTime(),
} }
got, ok := ParseCooldownFact(f) got, ok := ParseCooldownFact(f)
if !ok { if !ok {
@@ -149,4 +149,4 @@ func repeat(v string, n int) []string {
out[i] = v out[i] = v
} }
return out return out
} }
+1 -1
View File
@@ -242,4 +242,4 @@ func collapseReminders(due []store.Reminder) []store.Reminder {
Status: "pending", Status: "pending",
Collapsed: due, Collapsed: due,
}} }}
} }
+1 -1
View File
@@ -134,4 +134,4 @@ func CooldownFor(base time.Duration, lastSend time.Time) time.Time {
return time.Time{} // never sent → no cooldown active return time.Time{} // never sent → no cooldown active
} }
return lastSend.Add(base) return lastSend.Add(base)
} }
+7 -7
View File
@@ -129,9 +129,9 @@ func TestTickCooldownSuppresses(t *testing.T) {
now := refTime() now := refTime()
// 4h since water (would fire) — but cooldown until now+10min. Suppressed. // 4h since water (would fire) — but cooldown until now+10min. Suppressed.
s := State{ s := State{
Now: now, Now: now,
Presence: store.Present, Presence: store.Present,
Facts: map[string]store.Fact{ Facts: map[string]store.Fact{
"water": factAt("water", "tap:water", `"250ml"`, now.Add(-4*time.Hour)), "water": factAt("water", "tap:water", `"250ml"`, now.Add(-4*time.Hour)),
}, },
CooldownUntil: map[string]time.Time{ CooldownUntil: map[string]time.Time{
@@ -197,9 +197,9 @@ func TestGateNoDataInertShutsUp(t *testing.T) {
// is missing in the snapshot — gate must still return false. // is missing in the snapshot — gate must still return false.
s := State{Now: refTime(), Presence: store.Present} s := State{Now: refTime(), Presence: store.Present}
iwantfire := Rule{ iwantfire := Rule{
Name: "x", Name: "x",
Severity: Sev1, Severity: Sev1,
Predicate: func(State) bool { return true }, Predicate: func(State) bool { return true },
InertWhenNoData: []string{"missing_key"}, InertWhenNoData: []string{"missing_key"},
} }
if Gate(s, iwantfire) { if Gate(s, iwantfire) {
@@ -379,4 +379,4 @@ func TestGathererUsesFeedbackTunedCooldown(t *testing.T) {
if d, ok := ParseCooldownFact(fb); !ok || d != tuned { if d, ok := ParseCooldownFact(fb); !ok || d != tuned {
t.Fatalf("ParseCooldownFact round-trip: want %v ok, got %v ok=%v", tuned, d, ok) t.Fatalf("ParseCooldownFact round-trip: want %v ok, got %v ok=%v", tuned, d, ok)
} }
} }
+21 -21
View File
@@ -15,10 +15,10 @@ import "time"
// auto-tuner scales it over time (mostly ignored → lengthen; acted → leave). // auto-tuner scales it over time (mostly ignored → lengthen; acted → leave).
// Bounds belong at the daemon-config level; here we just carry the base. // Bounds belong at the daemon-config level; here we just carry the base.
type Rule struct { type Rule struct {
Name string Name string
Severity Severity Severity Severity
Cooldown // base cooldown + bounded-duration envelope for the auto-tuner Cooldown // base cooldown + bounded-duration envelope for the auto-tuner
Predicate func(State) bool Predicate func(State) bool
// InertWhenNoData — most rules should be silent when their substrate key is // InertWhenNoData — most rules should be silent when their substrate key is
// missing (since(key)==null → don't fire). If the predicate already encodes // missing (since(key)==null → don't fire). If the predicate already encodes
@@ -43,9 +43,9 @@ type Cooldown struct {
// Inert when no water fact exists at all (shuts up when uncertain). // Inert when no water fact exists at all (shuts up when uncertain).
func WaterRule() Rule { func WaterRule() Rule {
return Rule{ return Rule{
Name: "water", Name: "water",
Severity: Sev1, Severity: Sev1,
Cooldown: Cooldown{Base: 30 * time.Minute, Min: 15 * time.Minute, Max: 6 * time.Hour}, Cooldown: Cooldown{Base: 30 * time.Minute, Min: 15 * time.Minute, Max: 6 * time.Hour},
InertWhenNoData: []string{"water"}, InertWhenNoData: []string{"water"},
Predicate: func(s State) bool { Predicate: func(s State) bool {
d, ok := s.Since("water") d, ok := s.Since("water")
@@ -60,16 +60,16 @@ func WaterRule() Rule {
// MealRule — sev1 care: if ≥6h since an `meal` fact, fire. Inert without data. // MealRule — sev1 care: if ≥6h since an `meal` fact, fire. Inert without data.
func MealRule() Rule { func MealRule() Rule {
return Rule{ return Rule{
Name: "meal", Name: "meal",
Severity: Sev1, Severity: Sev1,
Cooldown: Cooldown{Base: 60 * time.Minute, Min: 30 * time.Minute, Max: 8 * time.Hour}, Cooldown: Cooldown{Base: 60 * time.Minute, Min: 30 * time.Minute, Max: 8 * time.Hour},
InertWhenNoData: []string{"meal"}, InertWhenNoData: []string{"meal"},
Predicate: func(s State) bool { Predicate: func(s State) bool {
d, ok := s.Since("meal") d, ok := s.Since("meal")
if !ok { if !ok {
return false return false
} }
return d >= 6 * time.Hour return d >= 6*time.Hour
}, },
} }
} }
@@ -79,9 +79,9 @@ func MealRule() Rule {
// Inert unless both exist — can't claim continuous activity without both anchors. // Inert unless both exist — can't claim continuous activity without both anchors.
func BreakRule() Rule { func BreakRule() Rule {
return Rule{ return Rule{
Name: "break", Name: "break",
Severity: Sev2, Severity: Sev2,
Cooldown: Cooldown{Base: 45 * time.Minute, Min: 20 * time.Minute, Max: 4 * time.Hour}, Cooldown: Cooldown{Base: 45 * time.Minute, Min: 20 * time.Minute, Max: 4 * time.Hour},
InertWhenNoData: []string{"desk_active", "break"}, InertWhenNoData: []string{"desk_active", "break"},
Predicate: func(s State) bool { Predicate: func(s State) bool {
dDesk, ok1 := s.Since("desk_active") dDesk, ok1 := s.Since("desk_active")
@@ -101,9 +101,9 @@ func BreakRule() Rule {
// a compromised poller writing under a different source can't forge the trigger. // a compromised poller writing under a different source can't forge the trigger.
func ServiceDownRule() Rule { func ServiceDownRule() Rule {
return Rule{ return Rule{
Name: "service_down", Name: "service_down",
Severity: Sev4, Severity: Sev4,
Cooldown: Cooldown{Base: 15 * time.Minute, Min: 5 * time.Minute, Max: 1 * time.Hour}, Cooldown: Cooldown{Base: 15 * time.Minute, Min: 5 * time.Minute, Max: 1 * time.Hour},
InertWhenNoData: []string{"service_down"}, InertWhenNoData: []string{"service_down"},
Predicate: func(s State) bool { Predicate: func(s State) bool {
f, ok := s.Fact("service_down") f, ok := s.Fact("service_down")
@@ -123,9 +123,9 @@ func ServiceDownRule() Rule {
// the hard page. Provenance-scoped to poll:netdata. // the hard page. Provenance-scoped to poll:netdata.
func NetdataCriticalRule() Rule { func NetdataCriticalRule() Rule {
return Rule{ return Rule{
Name: "netdata_critical", Name: "netdata_critical",
Severity: Sev3, Severity: Sev3,
Cooldown: Cooldown{Base: 20 * time.Minute, Min: 10 * time.Minute, Max: 2 * time.Hour}, Cooldown: Cooldown{Base: 20 * time.Minute, Min: 10 * time.Minute, Max: 2 * time.Hour},
InertWhenNoData: []string{"netdata_alarm"}, InertWhenNoData: []string{"netdata_alarm"},
Predicate: func(s State) bool { Predicate: func(s State) bool {
f, ok := s.Fact("netdata_alarm") f, ok := s.Fact("netdata_alarm")
@@ -148,4 +148,4 @@ func DefaultRules() []Rule {
ServiceDownRule(), ServiceDownRule(),
NetdataCriticalRule(), NetdataCriticalRule(),
} }
} }
+1 -1
View File
@@ -106,4 +106,4 @@ func (s State) Since(key string) (time.Duration, bool) {
return 0, true return 0, true
} }
return s.Now.Sub(f.Ts), true return s.Now.Sub(f.Ts), true
} }
+1 -1
View File
@@ -202,4 +202,4 @@ func sevLabel(s loop.Severity) string {
default: default:
return "alarm" return "alarm"
} }
} }
+5 -5
View File
@@ -17,8 +17,8 @@ func TestPhraseNudgeWaterMentionsDuration(t *testing.T) {
now := time.Now().UTC() now := time.Now().UTC()
earlier := now.Add(-4 * time.Hour) earlier := now.Add(-4 * time.Hour)
st := loop.State{ st := loop.State{
Now: now, Now: now,
Facts: map[string]store.Fact{"water": {Key: "water", Ts: earlier, Source: "tap:water", Value: `"250ml"`}}, Facts: map[string]store.Fact{"water": {Key: "water", Ts: earlier, Source: "tap:water", Value: `"250ml"`}},
} }
c := loop.Candidate{Rule: loop.WaterRule(), Severity: loop.Sev1, State: st} c := loop.Candidate{Rule: loop.WaterRule(), Severity: loop.Sev1, State: st}
pn, err := NewStub().PhraseNudge(context.Background(), c) pn, err := NewStub().PhraseNudge(context.Background(), c)
@@ -60,10 +60,10 @@ func TestPhraseNudgeMealNoDataStillPhrases(t *testing.T) {
func TestPhraseNudgeBreakDeskDuration(t *testing.T) { func TestPhraseNudgeBreakDeskDuration(t *testing.T) {
now := time.Now().UTC() now := time.Now().UTC()
st := loop.State{ st := loop.State{
Now: now, Now: now,
Facts: map[string]store.Fact{ Facts: map[string]store.Fact{
"desk_active": {Key: "desk_active", Ts: now.Add(-30 * time.Second)}, "desk_active": {Key: "desk_active", Ts: now.Add(-30 * time.Second)},
"break": {Key: "break", Ts: now.Add(-95 * time.Minute)}, "break": {Key: "break", Ts: now.Add(-95 * time.Minute)},
}, },
} }
c := loop.Candidate{Rule: loop.BreakRule(), Severity: loop.Sev2, State: st} c := loop.Candidate{Rule: loop.BreakRule(), Severity: loop.Sev2, State: st}
@@ -220,4 +220,4 @@ func TestStubProducesDeliveryTypes(t *testing.T) {
// ----------------------------- helpers -------------------------------------- // ----------------------------- helpers --------------------------------------
// (the per-rule templates change output shape; assert via strings.Contains // (the per-rule templates change output shape; assert via strings.Contains
// against salient fragments, not exact strings — keeps the tests robust to // against salient fragments, not exact strings — keeps the tests robust to
// tone tweaks in the Stub.) // tone tweaks in the Stub.)
+11 -11
View File
@@ -7,16 +7,16 @@
// a classifier owns the route; the SLM stays in its phrasing lane. same // a classifier owns the route; the SLM stays in its phrasing lane. same
// boundary as "rules decide, llm phrases," extended to the reactive path. // boundary as "rules decide, llm phrases," extended to the reactive path.
// - a CASCADE, not classifier-vs-deterministic — layers: // - a CASCADE, not classifier-vs-deterministic — layers:
// stage 0 — exact match (regex/grammar). wake-word + known command // stage 0 — exact match (regex/grammar). wake-word + known command
// grammar. "maven, restart nginx" hits the allowlist directly, // grammar. "maven, restart nginx" hits the allowlist directly,
// skips the classifier. lowest latency — the vosk command path. // skips the classifier. lowest latency — the vosk command path.
// stage 1 — intent classifier. embed utterance, nearest-centroid over // stage 1 — intent classifier. embed utterance, nearest-centroid over
// labeled intents. one forward pass, ~30ms cpu, similarity score. // labeled intents. one forward pass, ~30ms cpu, similarity score.
// stage 2 — slot extraction, per intent. classification gives *what kind*, // stage 2 — slot extraction, per intent. classification gives *what kind*,
// not *the args*. reminders need a datetime, acts need fn+params. // not *the args*. reminders need a datetime, acts need fn+params.
// stage 3 — confidence gate. below threshold → clarify, don't guess. // stage 3 — confidence gate. below threshold → clarify, don't guess.
// same pattern as since(key)==null → don't fire: a misrouted // same pattern as since(key)==null → don't fire: a misrouted
// fact is a confident wrong write — worse than a gap. // fact is a confident wrong write — worse than a gap.
// - save-where is the routing axis: act | reminder | fact | note | query. // - save-where is the routing axis: act | reminder | fact | note | query.
// - misroute correction = new centroid example — append-only, grows the // - misroute correction = new centroid example — append-only, grows the
// classifier as used. same shape as nudges.outcome tuning cooldowns. // classifier as used. same shape as nudges.outcome tuning cooldowns.
@@ -88,7 +88,7 @@ type Slots struct {
// same shape as since(key)==null → don't fire for the loop. // same shape as since(key)==null → don't fire for the loop.
type Decision struct { type Decision struct {
Utterance string Utterance string
Stage int // 0 exact-match, 1 classified, 2 slots-extracted, 3 clarify-gated Stage int // 0 exact-match, 1 classified, 2 slots-extracted, 3 clarify-gated
Intent Intent Intent Intent
Confidence float64 // 1.0 for stage-0; classifier cosine similarity for 1+ Confidence float64 // 1.0 for stage-0; classifier cosine similarity for 1+
Slots Slots Slots Slots
+4 -4
View File
@@ -228,10 +228,10 @@ func (t *unigramTokenizer) tokenize(text string) []int64 {
} }
type cand struct { type cand struct {
start int start int
end int end int
id int64 id int64
score float64 score float64
} }
func (t *unigramTokenizer) encodeWord(word string) []int64 { func (t *unigramTokenizer) encodeWord(word string) []int64 {
+3 -3
View File
@@ -44,10 +44,10 @@ import (
// (new dep) — do NOT bolt on sha256(passphrase). // (new dep) — do NOT bolt on sha256(passphrase).
const ( const (
cryptMagic = "MVNC1\x00" // 6-byte file magic; version in the trailing byte cryptMagic = "MVNC1\x00" // 6-byte file magic; version in the trailing byte
cryptMagicLen = len(cryptMagic) cryptMagicLen = len(cryptMagic)
nonceLen = 12 // AES-GCM standard nonce nonceLen = 12 // AES-GCM standard nonce
keyLen = 32 // AES-256 keyLen = 32 // AES-256
) )
// ErrKeyLen — the supplied encryption key was not exactly 32 bytes. // ErrKeyLen — the supplied encryption key was not exactly 32 bytes.
+1 -1
View File
@@ -253,4 +253,4 @@ func scanFact(r rowScanner) (Fact, error) {
f.Kind = FactKind(kind) f.Kind = FactKind(kind)
f.VoidsID = voids f.VoidsID = voids
return f, nil return f, nil
} }
+1 -1
View File
@@ -181,4 +181,4 @@ func (s *Store) LastNudge(ctx context.Context, rule string) (Nudge, error) {
n.OutcomeTs = outcomeTs n.OutcomeTs = outcomeTs
} }
return n, nil return n, nil
} }
+4 -4
View File
@@ -35,9 +35,9 @@ import (
// Signal — one presence signal with hand-tuned fresh-weight and decay τ. // Signal — one presence signal with hand-tuned fresh-weight and decay τ.
type Signal struct { type Signal struct {
Key string Key string
Weight float64 Weight float64
TauMin float64 TauMin float64
} }
// PresenceSignals — the three signals. Iterate in stable order. // PresenceSignals — the three signals. Iterate in stable order.
@@ -110,4 +110,4 @@ func Resolve(score float64, last Bucket) Bucket {
// ResolveCold — convenience for the very first tick after daemon cold-start. // ResolveCold — convenience for the very first tick after daemon cold-start.
// presence_state has no row; we begin as Away, the fail-closed outcome. // presence_state has no row; we begin as Away, the fail-closed outcome.
func ResolveCold(score float64) Bucket { return Resolve(score, Away) } func ResolveCold(score float64) Bucket { return Resolve(score, Away) }
+1 -1
View File
@@ -58,4 +58,4 @@ func (s *Store) PresenceProbes(ctx context.Context) ([]SignalProbe, error) {
probes = append(probes, SignalProbe{Key: sig.Key, LastTs: &t}) probes = append(probes, SignalProbe{Key: sig.Key, LastTs: &t})
} }
return probes, nil return probes, nil
} }
+1 -1
View File
@@ -142,4 +142,4 @@ func TestNegativeClockSkewClampsToFresh(t *testing.T) {
if got > 0.90+1e-9 { if got > 0.90+1e-9 {
t.Fatalf("clock skew clamps: want <= weight(0.90), got %f", got) t.Fatalf("clock skew clamps: want <= weight(0.90), got %f", got)
} }
} }
+8 -8
View File
@@ -12,13 +12,13 @@ import (
// Reminder — user-stated future intent. fires once or recurring (if cron set). // Reminder — user-stated future intent. fires once or recurring (if cron set).
type Reminder struct { type Reminder struct {
ID int64 ID int64
CreatedTs time.Time CreatedTs time.Time
FireTs time.Time FireTs time.Time
NextFireTs time.Time // computed next fire (for recurring) or same as FireTs NextFireTs time.Time // computed next fire (for recurring) or same as FireTs
Payload string // raw json Payload string // raw json
Status string // pending | fired | cancelled Status string // pending | fired | cancelled
Cron string // cron expression, empty for one-shot Cron string // cron expression, empty for one-shot
// Collapsed — set only on a synthetic digest reminder (ID=0): the original // Collapsed — set only on a synthetic digest reminder (ID=0): the original
// due reminders it stands in for. Not persisted. The dispatcher completes // due reminders it stands in for. Not persisted. The dispatcher completes
@@ -144,4 +144,4 @@ func (s *Store) RescheduleReminder(ctx context.Context, id int64, now time.Time)
} }
_, err = s.db.ExecContext(ctx, "UPDATE reminders SET next_fire_ts = ? WHERE id = ?", next.UnixMilli(), id) _, err = s.db.ExecContext(ctx, "UPDATE reminders SET next_fire_ts = ? WHERE id = ?", next.UnixMilli(), id)
return err return err
} }
+1 -1
View File
@@ -123,4 +123,4 @@ var (
ErrConfidence = errors.New("store: confidence must be in (0.0, 1.0]") ErrConfidence = errors.New("store: confidence must be in (0.0, 1.0]")
// ErrVoidsMissing — a correction pointed at a nonexistent fact. // ErrVoidsMissing — a correction pointed at a nonexistent fact.
ErrVoidsMissing = errors.New("store: voids_id does not reference an existing fact") ErrVoidsMissing = errors.New("store: voids_id does not reference an existing fact")
) )
+1 -1
View File
@@ -377,4 +377,4 @@ func TestCalendarEvents(t *testing.T) {
if len(events) != 0 { if len(events) != 0 {
t.Fatalf("expected 0 events on July 8, got %d", len(events)) t.Fatalf("expected 0 events on July 8, got %d", len(events))
} }
} }
+6 -6
View File
@@ -62,12 +62,12 @@ func NewStub() *Stub { return &Stub{} }
// classifier will route to a different intent (act / reminder / fact / note // classifier will route to a different intent (act / reminder / fact / note
// / query). The hash picks the phrase per utterance deterministically. // / query). The hash picks the phrase per utterance deterministically.
var stubPhrases = []string{ var stubPhrases = []string{
"maven, отметь что я выпил воды", // fact (water tap → fact table) "maven, отметь что я выпил воды", // fact (water tap → fact table)
"maven, напомни через 4 часа размяться", // reminder (→ reminders table) "maven, напомни через 4 часа размяться", // reminder (→ reminders table)
"maven, restart nginx", // act (→ stage-0 grammar hit) "maven, restart nginx", // act (→ stage-0 grammar hit)
"maven, что у меня сегодня по календарю", // query (→ slm read-path, deferred) "maven, что у меня сегодня по календарю", // query (→ slm read-path, deferred)
"note: idea — staggered cooldown by time of day", // note (→ chroma, deferred) "note: idea — staggered cooldown by time of day", // note (→ chroma, deferred)
"slept 6h, fan noise wrecked it", // compound capture (open spec; routes as fact today) "slept 6h, fan noise wrecked it", // compound capture (open spec; routes as fact today)
} }
// Transcribe returns one of stubPhrases, indexed by a hash of the audio // Transcribe returns one of stubPhrases, indexed by a hash of the audio
@@ -88,7 +88,7 @@ func (s *Stub) Transcribe(_ context.Context, a audio.Audio) (string, float64, er
// the stt module's unix socket. The daemon constructs one when its config // the stt module's unix socket. The daemon constructs one when its config
// points at a worker socket; otherwise it uses the Stub. // points at a worker socket; otherwise it uses the Stub.
type Remote struct { type Remote struct {
c *worker.Client c *worker.Client
lang string lang string
} }
@@ -109,4 +109,4 @@ func (r *Remote) Transcribe(ctx context.Context, a audio.Audio) (string, float64
return "", 0, fmt.Errorf("stt: transcribe: %w", err) return "", 0, fmt.Errorf("stt: transcribe: %w", err)
} }
return resp.Text, resp.Confidence, nil return resp.Text, resp.Confidence, nil
} }
+1 -1
View File
@@ -122,4 +122,4 @@ func TestRemoteErrorWraps(t *testing.T) {
if err == nil { if err == nil {
t.Fatalf("want error, got nil") t.Fatalf("want error, got nil")
} }
} }
+3 -3
View File
@@ -68,8 +68,8 @@ func (s *Stub) Synthesize(_ context.Context, text string) (audio.Audio, error) {
// Remote — the worker-backed Synthesizer. Holds a worker.Client that dials // Remote — the worker-backed Synthesizer. Holds a worker.Client that dials
// the tts module's unix socket. // the tts module's unix socket.
type Remote struct { type Remote struct {
c *worker.Client c *worker.Client
lang string lang string
voice string voice string
} }
@@ -90,4 +90,4 @@ func (r *Remote) Synthesize(ctx context.Context, text string) (audio.Audio, erro
return audio.Audio{}, fmt.Errorf("tts: synthesize: %w", err) return audio.Audio{}, fmt.Errorf("tts: synthesize: %w", err)
} }
return resp.Audio, nil return resp.Audio, nil
} }
+1 -1
View File
@@ -135,4 +135,4 @@ func samePCM(a, b []byte) bool {
} }
} }
return true return true
} }
+4 -4
View File
@@ -40,9 +40,9 @@ type PushHandler interface {
// Client — one connection to the voice.Server. // Client — one connection to the voice.Server.
type Client struct { type Client struct {
addr string addr string
mu sync.Mutex mu sync.Mutex
c net.Conn c net.Conn
nextID atomic.Uint64 nextID atomic.Uint64
// pushCh fan-out: a reader goroutine (started by RunPushReceiver) // pushCh fan-out: a reader goroutine (started by RunPushReceiver)
@@ -237,4 +237,4 @@ func marshalParams(v any) (json.RawMessage, error) {
return nil, fmt.Errorf("voice: marshal params: %w", err) return nil, fmt.Errorf("voice: marshal params: %w", err)
} }
return b, nil return b, nil
} }
+5 -5
View File
@@ -12,9 +12,9 @@ import (
// in-process and over-the-wire. // in-process and over-the-wire.
var ( var (
ErrUnknownMethod = errors.New("voice: unknown method") ErrUnknownMethod = errors.New("voice: unknown method")
ErrBadParams = errors.New("voice: bad params") ErrBadParams = errors.New("voice: bad params")
ErrForbidden = errors.New("voice: forbidden") ErrForbidden = errors.New("voice: forbidden")
ErrNoSession = errors.New("voice: no live client session") // voicesink unable to push ErrNoSession = errors.New("voice: no live client session") // voicesink unable to push
) )
// Sentinel wire codes. Stable; do not rename. // Sentinel wire codes. Stable; do not rename.
@@ -69,5 +69,5 @@ func hydrate(e *RpcError) error {
} }
// json helpers kept local so call sites read clean. // json helpers kept local so call sites read clean.
func jsonMarshal(v any) ([]byte, error) { return json.Marshal(v) } func jsonMarshal(v any) ([]byte, error) { return json.Marshal(v) }
func jsonUnmarshal(b []byte, v any) error { return json.Unmarshal(b, v) } func jsonUnmarshal(b []byte, v any) error { return json.Unmarshal(b, v) }
+1 -1
View File
@@ -57,4 +57,4 @@ func readFrame(r io.Reader, v any) error {
return fmt.Errorf("voice: unmarshal frame: %w", err) return fmt.Errorf("voice: unmarshal frame: %w", err)
} }
return nil return nil
} }
+1 -1
View File
@@ -81,4 +81,4 @@ func (s *StubReplier) Reply(d router.Decision) string {
default: default:
return "приняла." return "приняла."
} }
} }
+1 -1
View File
@@ -234,4 +234,4 @@ func marshalResult(v any) json.RawMessage {
return b return b
} }
// io.EOF — used by serveConn to detect a quiet client disconnect. // io.EOF — used by serveConn to detect a quiet client disconnect.
+10 -11
View File
@@ -30,16 +30,16 @@ import (
// the registry reads lastActive via LastActive; the conn is closed when // the registry reads lastActive via LastActive; the conn is closed when
// the client disconnects (serveConn returns) or the server shuts down. // the client disconnects (serveConn returns) or the server shuts down.
type Session struct { type Session struct {
ID uint64 ID uint64
Surface Surface Surface Surface
RemoteAddr string RemoteAddr string
// lastActive — last time we heard from this client (request frame OR // lastActive — last time we heard from this client (request frame OR
// Pong). PickRecent selects the max-lastActive session for routing. // Pong). PickRecent selects the max-lastActive session for routing.
lastActive atomic.Int64 // unix nano lastActive atomic.Int64 // unix nano
mu sync.Mutex mu sync.Mutex
conn net.Conn conn net.Conn
closed bool closed bool
} }
@@ -92,9 +92,9 @@ func (s *Session) shutdown() {
// voicesink (same pointer; the daemon passes one to both). Mutex around the // voicesink (same pointer; the daemon passes one to both). Mutex around the
// map; per-session conn writes use the session's own lock. // map; per-session conn writes use the session's own lock.
type Sessions struct { type Sessions struct {
mu sync.Mutex mu sync.Mutex
sess map[uint64]*Session sess map[uint64]*Session
nextID uint64 nextID uint64
} }
func NewSessions() *Sessions { func NewSessions() *Sessions {
@@ -111,10 +111,10 @@ func (r *Sessions) Add(c net.Conn, surface Surface) *Session {
defer r.mu.Unlock() defer r.mu.Unlock()
r.nextID++ r.nextID++
s := &Session{ s := &Session{
ID: r.nextID, ID: r.nextID,
Surface: surface, Surface: surface,
RemoteAddr: c.RemoteAddr().String(), RemoteAddr: c.RemoteAddr().String(),
conn: c, conn: c,
} }
s.setLastActive(time.Now()) s.setLastActive(time.Now())
r.sess[s.ID] = s r.sess[s.ID] = s
@@ -170,4 +170,3 @@ func (r *Sessions) PushToMostRecent(ctx context.Context, p AudioNudgePush) error
} }
return best.pushAudio(p) return best.pushAudio(p)
} }
+1 -1
View File
@@ -220,4 +220,4 @@ func TestClientListenModeReceivesPush(t *testing.T) {
type pushHandlerFunc func(Push) type pushHandlerFunc func(Push)
func (f pushHandlerFunc) OnPush(p Push) { f(p) } func (f pushHandlerFunc) OnPush(p Push) { f(p) }
+19 -19
View File
@@ -61,12 +61,12 @@ type Surface = auth.Surface
// internal/auth directly. The auth package IS the source of truth; these // internal/auth directly. The auth package IS the source of truth; these
// aliases forward to it. // aliases forward to it.
const ( const (
SurfaceVoice = auth.SurfaceVoice SurfaceVoice = auth.SurfaceVoice
SurfaceTelegram = auth.SurfaceTelegram SurfaceTelegram = auth.SurfaceTelegram
SurfacePCClient = auth.SurfacePCClient SurfacePCClient = auth.SurfacePCClient
SurfaceAuthedPage = auth.SurfaceAuthedPage SurfaceAuthedPage = auth.SurfaceAuthedPage
SurfaceCoreProcess = auth.SurfaceCoreProcess SurfaceCoreProcess = auth.SurfaceCoreProcess
SurfaceUnknown = auth.SurfaceUnknown SurfaceUnknown = auth.SurfaceUnknown
) )
// maxFrame — 64 MiB. Same instinct as worker: a single utterance at 16k mono // maxFrame — 64 MiB. Same instinct as worker: a single utterance at 16k mono
@@ -114,7 +114,7 @@ type Response struct {
// awaiting a Request response is interleaved — the client distinguishes by // awaiting a Request response is interleaved — the client distinguishes by
// the json shape (Response has `id`, Push has `kind`). // the json shape (Response has `id`, Push has `kind`).
type Push struct { type Push struct {
Kind PushKind `json:"kind"` Kind PushKind `json:"kind"`
Params json.RawMessage `json:"p,omitempty"` Params json.RawMessage `json:"p,omitempty"`
} }
@@ -152,9 +152,9 @@ type RpcError struct {
// every conn, but the wire carries it so future mTLS / passkey handshakes // every conn, but the wire carries it so future mTLS / passkey handshakes
// can populate it without a protocol version bump. // can populate it without a protocol version bump.
type PushToTalkReq struct { type PushToTalkReq struct {
Audio audio.Audio `json:"audio"` Audio audio.Audio `json:"audio"`
Lang string `json:"lang,omitempty"` Lang string `json:"lang,omitempty"`
Surface Surface `json:"surface,omitempty"` Surface Surface `json:"surface,omitempty"`
} }
// PushToTalkResp — the reply. ReplyAudio is TTS-synthesised; ReplyText is // PushToTalkResp — the reply. ReplyAudio is TTS-synthesised; ReplyText is
@@ -164,19 +164,19 @@ type PushToTalkReq struct {
// nudge fired alongside the reply and was forwarded to ntfy); today the // nudge fired alongside the reply and was forwarded to ntfy); today the
// reactive handler doesn't dispatch nudges, so this is empty. // reactive handler doesn't dispatch nudges, so this is empty.
type PushToTalkResp struct { type PushToTalkResp struct {
ReplyAudio audio.Audio `json:"reply_audio"` ReplyAudio audio.Audio `json:"reply_audio"`
ReplyText string `json:"reply_text"` ReplyText string `json:"reply_text"`
Transcript string `json:"transcript,omitempty"` Transcript string `json:"transcript,omitempty"`
RoutedChannels []string `json:"routed_channels,omitempty"` RoutedChannels []string `json:"routed_channels,omitempty"`
} }
// AudioNudgePush — the proactive nudge push payload. RuleName + Severity // AudioNudgePush — the proactive nudge push payload. RuleName + Severity
// for the client to display; Audio is the synthesised Body; Text is the // for the client to display; Audio is the synthesised Body; Text is the
// same in text form. // same in text form.
type AudioNudgePush struct { type AudioNudgePush struct {
RuleName string `json:"rule_name"` RuleName string `json:"rule_name"`
Severity int `json:"severity"` Severity int `json:"severity"`
Audio audio.Audio `json:"audio"` Audio audio.Audio `json:"audio"`
Text string `json:"text"` Text string `json:"text"`
Ts time.Time `json:"ts"` Ts time.Time `json:"ts"`
} }
+3 -1
View File
@@ -240,5 +240,7 @@ func decodeArg(data []byte, off int, ai byte) (uint64, int, error) {
} }
func readBE16(b []byte) uint16 { return uint16(b[0])<<8 | uint16(b[1]) } func readBE16(b []byte) uint16 { return uint16(b[0])<<8 | uint16(b[1]) }
func readBE32(b []byte) uint32 { return uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3]) } func readBE32(b []byte) uint32 {
return uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3])
}
func readBE64(b []byte) uint64 { return uint64(readBE32(b))<<32 | uint64(readBE32(b[4:])) } func readBE64(b []byte) uint64 { return uint64(readBE32(b))<<32 | uint64(readBE32(b[4:])) }
+8 -8
View File
@@ -121,9 +121,9 @@ func (rp *RP) CreationOptions(userID []byte, userName string) (map[string]any, s
"pubKeyCredParams": []map[string]any{ "pubKeyCredParams": []map[string]any{
{"type": "public-key", "alg": -7}, // ES256 {"type": "public-key", "alg": -7}, // ES256
}, },
"timeout": 60000, "timeout": 60000,
"attestation": "none", "attestation": "none",
"excludeCredentials": []any{}, "excludeCredentials": []any{},
}, challengeB64, nil }, challengeB64, nil
} }
@@ -188,11 +188,11 @@ func (rp *RP) AssertionOptions() (map[string]any, string, error) {
} }
return map[string]any{ return map[string]any{
"challenge": challengeB64, "challenge": challengeB64,
"timeout": 60000, "timeout": 60000,
"rpId": rp.cfg.RPID, "rpId": rp.cfg.RPID,
"allowCredentials": []any{}, "allowCredentials": []any{},
"userVerification": "required", "userVerification": "required",
}, challengeB64, nil }, challengeB64, nil
} }
+3 -3
View File
@@ -33,8 +33,8 @@ import (
type Client struct { type Client struct {
path string path string
mu sync.Mutex mu sync.Mutex
c net.Conn c net.Conn
dial func() (net.Conn, error) dial func() (net.Conn, error)
} }
@@ -188,4 +188,4 @@ func marshalParams(v any) (json.RawMessage, error) {
return nil, fmt.Errorf("worker: marshal params: %w", err) return nil, fmt.Errorf("worker: marshal params: %w", err)
} }
return b, nil return b, nil
} }
+1 -1
View File
@@ -64,4 +64,4 @@ func readFrame(r io.Reader, v any) error {
return fmt.Errorf("worker: unmarshal frame: %w", err) return fmt.Errorf("worker: unmarshal frame: %w", err)
} }
return nil return nil
} }
+1 -1
View File
@@ -22,4 +22,4 @@ type Transcriber interface {
// so a server asked for the wrong verb refuses cleanly). // so a server asked for the wrong verb refuses cleanly).
type Synthesizer interface { type Synthesizer interface {
Synthesize(ctx context.Context, req SynthesizeReq) (SynthesizeResp, error) Synthesize(ctx context.Context, req SynthesizeReq) (SynthesizeResp, error)
} }
+4 -4
View File
@@ -42,13 +42,13 @@ type TranscribeResp struct {
// SynthesizeReq — the synthesize verb args. // SynthesizeReq — the synthesize verb args.
type SynthesizeReq struct { type SynthesizeReq struct {
Text string `json:"text"` Text string `json:"text"`
Lang string `json:"lang"` // "ru" | "en" | "" Lang string `json:"lang"` // "ru" | "en" | ""
Voice string `json:"voice"` // named voice or "" ⇒ worker default Voice string `json:"voice"` // named voice or "" ⇒ worker default
Speed float64 `json:"speed"` // 1.0 = normal; clamped server-side Speed float64 `json:"speed"` // 1.0 = normal; clamped server-side
} }
// SynthesizeResp — the synthesize verb result. // SynthesizeResp — the synthesize verb result.
type SynthesizeResp struct { type SynthesizeResp struct {
Audio audio.Audio `json:"audio"` // rendered PCM Audio audio.Audio `json:"audio"` // rendered PCM
} }
+1 -1
View File
@@ -225,4 +225,4 @@ func parentDir(p string) string {
} }
} }
return "." return "."
} }
+2 -2
View File
@@ -62,7 +62,7 @@ type Request struct {
// Response — one frame back from the worker. Exactly one of Result/Error is set. // Response — one frame back from the worker. Exactly one of Result/Error is set.
type Response struct { type Response struct {
Result json.RawMessage `json:"r,omitempty"` Result json.RawMessage `json:"r,omitempty"`
Error *RpcError `json:"e,omitempty"` Error *RpcError `json:"e,omitempty"`
} }
// RpcError — a typed wire error. Mirrors internal/ipc's shape for tooling // RpcError — a typed wire error. Mirrors internal/ipc's shape for tooling
@@ -120,4 +120,4 @@ func rpcErr(err error) *RpcError {
return &RpcError{Code: c, Message: err.Error()} return &RpcError{Code: c, Message: err.Error()}
} }
return &RpcError{Code: c} return &RpcError{Code: c}
} }
+1 -1
View File
@@ -286,4 +286,4 @@ func TestPathAfterListen(t *testing.T) {
func contains(haystack, needle string) bool { func contains(haystack, needle string) bool {
return len(haystack) >= len(needle) && (bytes.Contains([]byte(haystack), []byte(needle))) return len(haystack) >= len(needle) && (bytes.Contains([]byte(haystack), []byte(needle)))
} }