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:
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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) -----
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 |
|
||||||
// | sev1–2 (care) | voice | drop |
|
// | sev1–2 (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.
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -105,5 +105,3 @@ func InheritSlots(prev, cur Slots) Slots {
|
|||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+19
-19
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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")
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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"`
|
||||||
}
|
}
|
||||||
@@ -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:])) }
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user