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
+2 -2
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 (
+2 -2
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
+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 {
+14 -14
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) -----
+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) {
+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
@@ -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
+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
} }
+9 -9
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
+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
} }
-2
View File
@@ -105,5 +105,3 @@ func InheritSlots(prev, cur Slots) Slots {
} }
return out return out
} }
+19 -19
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,15 +315,15 @@ 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
+7 -7
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
} }
+26 -26
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;
+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.
+3 -3
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 {
+6 -6
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) {
+20 -20
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")
+4 -4
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}
+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.
+3 -3
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.
+7 -7
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
+5 -5
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
} }
+2 -2
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
} }
+3 -3
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)
+4 -4
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) }
+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)
} }
+18 -18
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
} }
+2 -2
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)
} }
+3 -3
View File
@@ -42,9 +42,9 @@ 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
} }
+1 -1
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