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 (
|
||||||
@@ -160,4 +160,4 @@ func jsonUnmarshal(b []byte, v any) error { return json.Unmarshal(b, v) }
|
|||||||
var _ = strconv.Atoi
|
var _ = strconv.Atoi
|
||||||
var _ io.Reader = (io.Reader)(nil)
|
var _ io.Reader = (io.Reader)(nil)
|
||||||
var _ = net.IPv4
|
var _ = net.IPv4
|
||||||
var _ = time.Second
|
var _ = time.Second
|
||||||
|
|||||||
+1
-1
@@ -230,4 +230,4 @@ func run(args []string) error {
|
|||||||
wg.Wait()
|
wg.Wait()
|
||||||
log.Printf("mavend: bye")
|
log.Printf("mavend: bye")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -45,8 +45,8 @@ type tickLoop struct {
|
|||||||
phraser phraser.Phraser
|
phraser phraser.Phraser
|
||||||
rules []loop.Rule
|
rules []loop.Rule
|
||||||
|
|
||||||
tickInterval time.Duration
|
tickInterval time.Duration
|
||||||
repeatInterval time.Duration
|
repeatInterval time.Duration
|
||||||
autotuneInterval time.Duration // 0 ⇒ autotune disabled (gatherer falls back to static Base)
|
autotuneInterval time.Duration // 0 ⇒ autotune disabled (gatherer falls back to static Base)
|
||||||
|
|
||||||
// digestCfg — the digest/batching config. nil ⇒ every nudge is sent
|
// digestCfg — the digest/batching config. nil ⇒ every nudge is sent
|
||||||
@@ -429,4 +429,4 @@ func toIPCGateDetail(d loop.GateDetail) ipc.GateDetail {
|
|||||||
Presence: d.Presence,
|
Presence: d.Presence,
|
||||||
InertKeysMissing: d.InertKeysMissing,
|
InertKeysMissing: d.InertKeysMissing,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ func TestTickCooldownSuppressesSecondSend(t *testing.T) {
|
|||||||
sink := &fakeSink{}
|
sink := &fakeSink{}
|
||||||
tl := newTestTickLoop(t, st, sink, nil)
|
tl := newTestTickLoop(t, st, sink, nil)
|
||||||
|
|
||||||
tl.tick(ctx, now) // fires
|
tl.tick(ctx, now) // fires
|
||||||
tl.tick(ctx, now.Add(time.Minute)) // still within 30m cooldown ⇒ suppressed
|
tl.tick(ctx, now.Add(time.Minute)) // still within 30m cooldown ⇒ suppressed
|
||||||
|
|
||||||
if len(sink.sends) != 1 {
|
if len(sink.sends) != 1 {
|
||||||
|
|||||||
+15
-15
@@ -50,17 +50,17 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/kami/maven/internal/audio"
|
"github.com/kami/maven/internal/audio"
|
||||||
"github.com/kami/maven/internal/memory"
|
|
||||||
"github.com/kami/maven/internal/config"
|
"github.com/kami/maven/internal/config"
|
||||||
"github.com/kami/maven/internal/delivery"
|
"github.com/kami/maven/internal/delivery"
|
||||||
"github.com/kami/maven/internal/delivery/voicesink"
|
"github.com/kami/maven/internal/delivery/voicesink"
|
||||||
"github.com/kami/maven/internal/ipc"
|
"github.com/kami/maven/internal/ipc"
|
||||||
|
"github.com/kami/maven/internal/memory"
|
||||||
"github.com/kami/maven/internal/phraser"
|
"github.com/kami/maven/internal/phraser"
|
||||||
"github.com/kami/maven/internal/router"
|
"github.com/kami/maven/internal/router"
|
||||||
"github.com/kami/maven/internal/stt"
|
"github.com/kami/maven/internal/stt"
|
||||||
@@ -207,18 +207,18 @@ func wireVoice(cfg *config.Config, coreAPI ipc.CoreAPI, phr phraser.Phraser) (*v
|
|||||||
|
|
||||||
// ----- the handler (the reactive path; closes over stt / tts / router / coreAPI / memory) -----
|
// ----- the handler (the reactive path; closes over stt / tts / router / coreAPI / memory) -----
|
||||||
h := &reactiveHandler{
|
h := &reactiveHandler{
|
||||||
stt: transcriber,
|
stt: transcriber,
|
||||||
tts: synthesizer,
|
tts: synthesizer,
|
||||||
router: rtr,
|
router: rtr,
|
||||||
embedder: emb,
|
embedder: emb,
|
||||||
api: coreAPI,
|
api: coreAPI,
|
||||||
tools: exec,
|
tools: exec,
|
||||||
phraser: phr,
|
phraser: phr,
|
||||||
replier: voice.NewStubReplier(),
|
replier: voice.NewStubReplier(),
|
||||||
now: time.Now,
|
now: time.Now,
|
||||||
weatherProvider: weatherProvider,
|
weatherProvider: weatherProvider,
|
||||||
weatherLocation: weatherLocation,
|
weatherLocation: weatherLocation,
|
||||||
memStore: memStore,
|
memStore: memStore,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----- the server (TCP listener) -----
|
// ----- the server (TCP listener) -----
|
||||||
@@ -968,4 +968,4 @@ func jsonStringImpl(s string) string {
|
|||||||
}
|
}
|
||||||
b = append(b, '"')
|
b = append(b, '"')
|
||||||
return string(b)
|
return string(b)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
+1
-1
@@ -126,4 +126,4 @@ func defaultSocket(name string) string {
|
|||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
return home + "/.local/share/maven/" + name
|
return home + "/.local/share/maven/" + name
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -83,4 +83,4 @@ func (f Format) IsValid() bool {
|
|||||||
f.Channels == PCM16kMono.Channels &&
|
f.Channels == PCM16kMono.Channels &&
|
||||||
f.SampleBits == PCM16kMono.SampleBits &&
|
f.SampleBits == PCM16kMono.SampleBits &&
|
||||||
f.Encoding == PCM16kMono.Encoding
|
f.Encoding == PCM16kMono.Encoding
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,4 +101,4 @@ func TestPCMFromWAVRejectsNonCanonical(t *testing.T) {
|
|||||||
if _, _, err := PCMFromWAV(wav); err == nil {
|
if _, _, err := PCMFromWAV(wav); err == nil {
|
||||||
t.Fatalf("non-PCM format should error")
|
t.Fatalf("non-PCM format should error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ func WAVFromPCM(format Format, pcm []byte) ([]byte, error) {
|
|||||||
// fmt chunk
|
// fmt chunk
|
||||||
copy(out[12:16], []byte("fmt "))
|
copy(out[12:16], []byte("fmt "))
|
||||||
binary.LittleEndian.PutUint32(out[16:20], 16) // fmt chunk size
|
binary.LittleEndian.PutUint32(out[16:20], 16) // fmt chunk size
|
||||||
binary.LittleEndian.PutUint16(out[20:22], 1) // PCM
|
binary.LittleEndian.PutUint16(out[20:22], 1) // PCM
|
||||||
binary.LittleEndian.PutUint16(out[22:24], uint16(format.Channels))
|
binary.LittleEndian.PutUint16(out[22:24], uint16(format.Channels))
|
||||||
binary.LittleEndian.PutUint32(out[24:28], uint32(format.SampleRate))
|
binary.LittleEndian.PutUint32(out[24:28], uint32(format.SampleRate))
|
||||||
byteRate := uint32(format.SampleRate) * uint32(format.Channels) * uint32(format.SampleBits) / 8
|
byteRate := uint32(format.SampleRate) * uint32(format.Channels) * uint32(format.SampleBits) / 8
|
||||||
@@ -118,4 +118,4 @@ func WAVFromPCM(format Format, pcm []byte) ([]byte, error) {
|
|||||||
copy(out[36:40], []byte("data"))
|
copy(out[36:40], []byte("data"))
|
||||||
binary.LittleEndian.PutUint32(out[40:44], uint32(len(pcm)))
|
binary.LittleEndian.PutUint32(out[40:44], uint32(len(pcm)))
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -354,6 +354,9 @@ func (r *recordingAPI) RecentOutcomes(_ context.Context, _ string, _ int) ([]str
|
|||||||
func (r *recordingAPI) RecentFacts(_ context.Context, _ int) ([]ipc.Fact, error) {
|
func (r *recordingAPI) RecentFacts(_ context.Context, _ int) ([]ipc.Fact, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
func (r *recordingAPI) CalendarEvents(_ context.Context, _, _ time.Time) ([]ipc.Fact, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
func (r *recordingAPI) RecentNudges(_ context.Context, _ int) ([]ipc.Nudge, error) {
|
func (r *recordingAPI) RecentNudges(_ context.Context, _ int) ([]ipc.Nudge, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-10
@@ -186,8 +186,8 @@ type VoiceConfig struct {
|
|||||||
|
|
||||||
// WeatherConfig configures the weather provider for voice queries.
|
// WeatherConfig configures the weather provider for voice queries.
|
||||||
type WeatherConfig struct {
|
type WeatherConfig struct {
|
||||||
Provider string `json:"provider,omitempty"` // "open-meteo" or "" → stub
|
Provider string `json:"provider,omitempty"` // "open-meteo" or "" → stub
|
||||||
DefaultLocation string `json:"default_location,omitempty"` // e.g. "Moscow"
|
DefaultLocation string `json:"default_location,omitempty"` // e.g. "Moscow"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToolConfig — one enabled tool. Name is the spoken verb ("restart"); Cmd is
|
// ToolConfig — one enabled tool. Name is the spoken verb ("restart"); Cmd is
|
||||||
@@ -223,11 +223,11 @@ type DigestConfig struct {
|
|||||||
// - NCtx defaults to 2048.
|
// - NCtx defaults to 2048.
|
||||||
// - Timeout defaults to 30s per request.
|
// - Timeout defaults to 30s per request.
|
||||||
type PhraserConfig struct {
|
type PhraserConfig struct {
|
||||||
ModelPath string `json:"model_path"`
|
ModelPath string `json:"model_path"`
|
||||||
BinPath string `json:"bin_path,omitempty"`
|
BinPath string `json:"bin_path,omitempty"`
|
||||||
Listen string `json:"listen,omitempty"`
|
Listen string `json:"listen,omitempty"`
|
||||||
NGpuLayers int `json:"n_gpu_layers,omitempty"`
|
NGpuLayers int `json:"n_gpu_layers,omitempty"`
|
||||||
NCtx int `json:"n_ctx,omitempty"`
|
NCtx int `json:"n_ctx,omitempty"`
|
||||||
Timeout Duration `json:"timeout,omitempty"`
|
Timeout Duration `json:"timeout,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,9 +237,9 @@ type PhraserConfig struct {
|
|||||||
// floor HashEmbedder stub. Model_path is the ONNX model file, tokenizer_path
|
// floor HashEmbedder stub. Model_path is the ONNX model file, tokenizer_path
|
||||||
// is tokenizer.json (Unigram), lib_path is the ONNX Runtime shared library.
|
// is tokenizer.json (Unigram), lib_path is the ONNX Runtime shared library.
|
||||||
type EmbedderConfig struct {
|
type EmbedderConfig struct {
|
||||||
ModelPath string `json:"model_path,omitempty"`
|
ModelPath string `json:"model_path,omitempty"`
|
||||||
TokenizerPath string `json:"tokenizer_path,omitempty"`
|
TokenizerPath string `json:"tokenizer_path,omitempty"`
|
||||||
LibPath string `json:"lib_path,omitempty"`
|
LibPath string `json:"lib_path,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// WorkerConfig — a unix-socket worker module connection. Used by Stt and
|
// WorkerConfig — a unix-socket worker module connection. Used by Stt and
|
||||||
@@ -428,4 +428,4 @@ func defaultRuntimeDir() string {
|
|||||||
// /run/user/$UID is the typical answer; without XDG_RUNTIME_DIR, fall back
|
// /run/user/$UID is the typical answer; without XDG_RUNTIME_DIR, fall back
|
||||||
// to the data dir (still works; just not tmpfs-clearance-on-reboot clean).
|
// to the data dir (still works; just not tmpfs-clearance-on-reboot clean).
|
||||||
return filepath.Join(defaultDataDir())
|
return filepath.Join(defaultDataDir())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -187,4 +187,4 @@ func TestDurationRoundTrip(t *testing.T) {
|
|||||||
if d2 != d {
|
if d2 != d {
|
||||||
t.Errorf("round-trip = %v, want %v", d2, d)
|
t.Errorf("round-trip = %v, want %v", d2, d)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,4 +17,4 @@ func writeFile(t *testing.T, path, body string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return os.WriteFile(path, []byte(body), 0o600)
|
return os.WriteFile(path, []byte(body), 0o600)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -196,4 +196,4 @@ func (s *Sink) Send(ctx context.Context, d delivery.Sendable) error {
|
|||||||
// token never leaves the sink, no logging.
|
// token never leaves the sink, no logging.
|
||||||
func (s *Sink) sendMessageURL() string {
|
func (s *Sink) sendMessageURL() string {
|
||||||
return s.base + "/bot" + s.cfg.BotToken + "/sendMessage"
|
return s.base + "/bot" + s.cfg.BotToken + "/sendMessage"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -399,4 +399,4 @@ func TestReminderSendUsesSamePath(t *testing.T) {
|
|||||||
if req.Text != "wake up" {
|
if req.Text != "wake up" {
|
||||||
t.Fatalf("reminder text: want 'wake up', got %q", req.Text)
|
t.Fatalf("reminder text: want 'wake up', got %q", req.Text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,4 +106,4 @@ func (s *Sink) Send(ctx context.Context, send delivery.Sendable) error {
|
|||||||
// Format.IsValid which is a method on the imported audio.Format). The alias
|
// Format.IsValid which is a method on the imported audio.Format). The alias
|
||||||
// below keeps the import alive even if a future refactor moves the only
|
// below keeps the import alive even if a future refactor moves the only
|
||||||
// reference. Today, the synthesizer's audio.Audio directly flows through.
|
// reference. Today, the synthesizer's audio.Audio directly flows through.
|
||||||
var _ = audio.PCM16kMono
|
var _ = audio.PCM16kMono
|
||||||
|
|||||||
@@ -105,5 +105,3 @@ func InheritSlots(prev, cur Slots) Slots {
|
|||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+20
-20
@@ -12,14 +12,14 @@ import (
|
|||||||
|
|
||||||
// Fact — one observation. Ts is valid-time (true-as-of), as in store.
|
// Fact — one observation. Ts is valid-time (true-as-of), as in store.
|
||||||
type Fact struct {
|
type Fact struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Ts time.Time `json:"ts"`
|
Ts time.Time `json:"ts"`
|
||||||
Kind string `json:"kind"` // "self" | "env" | "config"
|
Kind string `json:"kind"` // "self" | "env" | "config"
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
Value string `json:"value"` // raw json if structured
|
Value string `json:"value"` // raw json if structured
|
||||||
Source string `json:"source"` // tap:*|infer:*|poll:*|ambient|promote|feedback
|
Source string `json:"source"` // tap:*|infer:*|poll:*|ambient|promote|feedback
|
||||||
Confidence float64 `json:"confidence"`
|
Confidence float64 `json:"confidence"`
|
||||||
VoidsID *int64 `json:"voids_id,omitempty"`
|
VoidsID *int64 `json:"voids_id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bucket — presence hysteresis state: "present" | "away".
|
// Bucket — presence hysteresis state: "present" | "away".
|
||||||
@@ -66,9 +66,9 @@ type Reminder struct {
|
|||||||
// routing and tone. presence = reachability, NOT wakefulness (spec). Loop
|
// routing and tone. presence = reachability, NOT wakefulness (spec). Loop
|
||||||
// reads probes + computes score itself; modules get the resolved snapshot.
|
// reads probes + computes score itself; modules get the resolved snapshot.
|
||||||
type Presence struct {
|
type Presence struct {
|
||||||
Bucket Bucket `json:"bucket"`
|
Bucket Bucket `json:"bucket"`
|
||||||
Score float64 `json:"score"`
|
Score float64 `json:"score"`
|
||||||
Updated time.Time `json:"updated"`
|
Updated time.Time `json:"updated"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteFactReq — the only state mutation a capture/tool module performs.
|
// WriteFactReq — the only state mutation a capture/tool module performs.
|
||||||
@@ -315,19 +315,19 @@ func CallerFrom(ctx context.Context) (Caller, bool) {
|
|||||||
// Sentinel errors. Mirror store's 1:1 so module code reads the same whether
|
// Sentinel errors. Mirror store's 1:1 so module code reads the same whether
|
||||||
// in-process or over the wire. The store adapter translates store.* → these.
|
// in-process or over the wire. The store adapter translates store.* → these.
|
||||||
var (
|
var (
|
||||||
ErrNoFact = errors.New("ipc: no fact for key")
|
ErrNoFact = errors.New("ipc: no fact for key")
|
||||||
ErrConfidence = errors.New("ipc: confidence must be in (0.0, 1.0]")
|
ErrConfidence = errors.New("ipc: confidence must be in (0.0, 1.0]")
|
||||||
ErrVoidsMissing = errors.New("ipc: voids_id does not reference an existing fact")
|
ErrVoidsMissing = errors.New("ipc: voids_id does not reference an existing fact")
|
||||||
ErrNudgeNotFound = errors.New("ipc: nudge not found")
|
ErrNudgeNotFound = errors.New("ipc: nudge not found")
|
||||||
ErrNudgeOutcome = errors.New("ipc: nudge already resolved")
|
ErrNudgeOutcome = errors.New("ipc: nudge already resolved")
|
||||||
ErrReminderNotFound = errors.New("ipc: reminder not found")
|
ErrReminderNotFound = errors.New("ipc: reminder not found")
|
||||||
ErrReminderState = errors.New("ipc: reminder not in a mutable state")
|
ErrReminderState = errors.New("ipc: reminder not in a mutable state")
|
||||||
ErrUnknownMethod = errors.New("ipc: unknown method")
|
ErrUnknownMethod = errors.New("ipc: unknown method")
|
||||||
ErrBadParams = errors.New("ipc: bad params")
|
ErrBadParams = errors.New("ipc: bad params")
|
||||||
// ErrForbidden — the caller's authority doesn't cover this call. The
|
// ErrForbidden — the caller's authority doesn't cover this call. The
|
||||||
// auth layer's only wire-exported verdict: surface caps the layer, or a
|
// auth layer's only wire-exported verdict: surface caps the layer, or a
|
||||||
// write was out-of-scope, or step-up was required but not asserted. The
|
// write was out-of-scope, or step-up was required but not asserted. The
|
||||||
// text ErrForbidden carries is derived during dispatch (from auth.ErrForbidden
|
// text ErrForbidden carries is derived during dispatch (from auth.ErrForbidden
|
||||||
// via fmt.Errorf %w wrapping); the wire carries codeForbidden.
|
// via fmt.Errorf %w wrapping); the wire carries codeForbidden.
|
||||||
ErrForbidden = errors.New("ipc: forbidden")
|
ErrForbidden = errors.New("ipc: forbidden")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -372,4 +372,4 @@ func (c *Client) RevertFact(ctx context.Context, key string) (int64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Compile-time check: *Client satisfies CoreAPI.
|
// Compile-time check: *Client satisfies CoreAPI.
|
||||||
var _ CoreAPI = (*Client)(nil)
|
var _ CoreAPI = (*Client)(nil)
|
||||||
|
|||||||
@@ -83,4 +83,4 @@ func readFrame(r io.Reader, v any) error {
|
|||||||
return fmt.Errorf("ipc: unmarshal frame: %w", err)
|
return fmt.Errorf("ipc: unmarshal frame: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -423,4 +423,4 @@ func mustJSON(v any) []byte {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -280,9 +280,9 @@ type Server struct {
|
|||||||
api CoreAPI
|
api CoreAPI
|
||||||
path string
|
path string
|
||||||
|
|
||||||
ln net.Listener
|
ln net.Listener
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
|
|
||||||
// Check — optional authorization hook. dispatch runs it BEFORE method
|
// Check — optional authorization hook. dispatch runs it BEFORE method
|
||||||
// dispatch, with the raw params, so the auth layer can make verdicts
|
// dispatch, with the raw params, so the auth layer can make verdicts
|
||||||
@@ -343,10 +343,10 @@ func Listen(path string, api CoreAPI) (*Server, error) {
|
|||||||
return nil, fmt.Errorf("ipc: chmod socket: %w", err)
|
return nil, fmt.Errorf("ipc: chmod socket: %w", err)
|
||||||
}
|
}
|
||||||
return &Server{
|
return &Server{
|
||||||
api: api,
|
api: api,
|
||||||
path: path,
|
path: path,
|
||||||
ln: ln,
|
ln: ln,
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -754,4 +754,4 @@ func peerCaller(c net.Conn) (Caller, bool) {
|
|||||||
return Caller{}, false
|
return Caller{}, false
|
||||||
}
|
}
|
||||||
return Caller{Uid: int32(cred.Uid), Pid: int32(cred.Pid)}, true
|
return Caller{Uid: int32(cred.Uid), Pid: int32(cred.Pid)}, true
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-27
@@ -13,38 +13,38 @@ import (
|
|||||||
type Method string
|
type Method string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
MethodWriteFact Method = "write_fact"
|
MethodWriteFact Method = "write_fact"
|
||||||
MethodLatestFact Method = "latest_fact"
|
MethodLatestFact Method = "latest_fact"
|
||||||
MethodLatestFactBySource Method = "latest_fact_by_source"
|
MethodLatestFactBySource Method = "latest_fact_by_source"
|
||||||
MethodSince Method = "since"
|
MethodSince Method = "since"
|
||||||
MethodPresence Method = "presence"
|
MethodPresence Method = "presence"
|
||||||
MethodCreateReminder Method = "create_reminder"
|
MethodCreateReminder Method = "create_reminder"
|
||||||
MethodMarkReminder Method = "mark_reminder"
|
MethodMarkReminder Method = "mark_reminder"
|
||||||
MethodRecordNudge Method = "record_nudge"
|
MethodRecordNudge Method = "record_nudge"
|
||||||
MethodResolveNudge Method = "resolve_nudge"
|
MethodResolveNudge Method = "resolve_nudge"
|
||||||
MethodRecentOutcomes Method = "recent_outcomes"
|
MethodRecentOutcomes Method = "recent_outcomes"
|
||||||
MethodRecentFacts Method = "recent_facts"
|
MethodRecentFacts Method = "recent_facts"
|
||||||
MethodCalendarEvents Method = "calendar_events"
|
MethodCalendarEvents Method = "calendar_events"
|
||||||
MethodRecentNudges Method = "recent_nudges"
|
MethodRecentNudges Method = "recent_nudges"
|
||||||
MethodWriteNote Method = "write_note"
|
MethodWriteNote Method = "write_note"
|
||||||
MethodQueryNotes Method = "query_notes"
|
MethodQueryNotes Method = "query_notes"
|
||||||
MethodRecentNotes Method = "recent_notes"
|
MethodRecentNotes Method = "recent_notes"
|
||||||
MethodProposeTool Method = "propose_tool"
|
MethodProposeTool Method = "propose_tool"
|
||||||
MethodEnableTool Method = "enable_tool"
|
MethodEnableTool Method = "enable_tool"
|
||||||
MethodDisableTool Method = "disable_tool"
|
MethodDisableTool Method = "disable_tool"
|
||||||
MethodAssertStepUp Method = "assert_stepup"
|
MethodAssertStepUp Method = "assert_stepup"
|
||||||
MethodLookupTool Method = "lookup_tool"
|
MethodLookupTool Method = "lookup_tool"
|
||||||
MethodListTools Method = "list_tools"
|
MethodListTools Method = "list_tools"
|
||||||
MethodRevertFact Method = "revert_fact"
|
MethodRevertFact Method = "revert_fact"
|
||||||
MethodTickTrace Method = "tick_trace"
|
MethodTickTrace Method = "tick_trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Request — one frame from module to core. Params is the JSON-encoded argument
|
// Request — one frame from module to core. Params is the JSON-encoded argument
|
||||||
// struct for Method (see api.go for the per-method shapes). The server
|
// struct for Method (see api.go for the per-method shapes). The server
|
||||||
// unmarshals Params based on Method; an unknown Method ⇒ ErrUnknownMethod.
|
// unmarshals Params based on Method; an unknown Method ⇒ ErrUnknownMethod.
|
||||||
type Request struct {
|
type Request struct {
|
||||||
Method Method `json:"m"`
|
Method Method `json:"m"`
|
||||||
Params json.RawMessage `json:"p,omitempty"`
|
Params json.RawMessage `json:"p,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Response — one frame from core back to module. Exactly one of Result/Error
|
// Response — one frame from core back to module. Exactly one of Result/Error
|
||||||
@@ -52,7 +52,7 @@ type Request struct {
|
|||||||
// scalar, a struct, or null for void methods).
|
// scalar, a struct, or null for void methods).
|
||||||
type Response struct {
|
type Response struct {
|
||||||
Result json.RawMessage `json:"r,omitempty"`
|
Result json.RawMessage `json:"r,omitempty"`
|
||||||
Error *RpcError `json:"e,omitempty"`
|
Error *RpcError `json:"e,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RpcError — a typed wire error. Code is one of the sentinel codes below;
|
// RpcError — a typed wire error. Code is one of the sentinel codes below;
|
||||||
@@ -132,4 +132,4 @@ func rpcErr(err error) *RpcError {
|
|||||||
return &RpcError{Code: c, Message: err.Error()}
|
return &RpcError{Code: c, Message: err.Error()}
|
||||||
}
|
}
|
||||||
return &RpcError{Code: c}
|
return &RpcError{Code: c}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -119,4 +119,4 @@ func ParseCooldownFact(f store.Fact) (time.Duration, bool) {
|
|||||||
func MarshalCooldown(d time.Duration) string {
|
func MarshalCooldown(d time.Duration) string {
|
||||||
b, _ := json.Marshal(int64(d)) // int64 marshal never errors
|
b, _ := json.Marshal(int64(d)) // int64 marshal never errors
|
||||||
return string(b)
|
return string(b)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,10 +97,10 @@ func TestFeedbackKeyMatchesRuleName(t *testing.T) {
|
|||||||
func TestParseCooldownFactRoundTrip(t *testing.T) {
|
func TestParseCooldownFactRoundTrip(t *testing.T) {
|
||||||
d := 45 * time.Minute
|
d := 45 * time.Minute
|
||||||
f := store.Fact{
|
f := store.Fact{
|
||||||
Key: "cooldown:water",
|
Key: "cooldown:water",
|
||||||
Source: FeedbackSource,
|
Source: FeedbackSource,
|
||||||
Value: MarshalCooldown(d),
|
Value: MarshalCooldown(d),
|
||||||
Ts: refTime(),
|
Ts: refTime(),
|
||||||
}
|
}
|
||||||
got, ok := ParseCooldownFact(f)
|
got, ok := ParseCooldownFact(f)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -149,4 +149,4 @@ func repeat(v string, n int) []string {
|
|||||||
out[i] = v
|
out[i] = v
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -242,4 +242,4 @@ func collapseReminders(due []store.Reminder) []store.Reminder {
|
|||||||
Status: "pending",
|
Status: "pending",
|
||||||
Collapsed: due,
|
Collapsed: due,
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,4 +134,4 @@ func CooldownFor(base time.Duration, lastSend time.Time) time.Time {
|
|||||||
return time.Time{} // never sent → no cooldown active
|
return time.Time{} // never sent → no cooldown active
|
||||||
}
|
}
|
||||||
return lastSend.Add(base)
|
return lastSend.Add(base)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,9 +129,9 @@ func TestTickCooldownSuppresses(t *testing.T) {
|
|||||||
now := refTime()
|
now := refTime()
|
||||||
// 4h since water (would fire) — but cooldown until now+10min. Suppressed.
|
// 4h since water (would fire) — but cooldown until now+10min. Suppressed.
|
||||||
s := State{
|
s := State{
|
||||||
Now: now,
|
Now: now,
|
||||||
Presence: store.Present,
|
Presence: store.Present,
|
||||||
Facts: map[string]store.Fact{
|
Facts: map[string]store.Fact{
|
||||||
"water": factAt("water", "tap:water", `"250ml"`, now.Add(-4*time.Hour)),
|
"water": factAt("water", "tap:water", `"250ml"`, now.Add(-4*time.Hour)),
|
||||||
},
|
},
|
||||||
CooldownUntil: map[string]time.Time{
|
CooldownUntil: map[string]time.Time{
|
||||||
@@ -197,9 +197,9 @@ func TestGateNoDataInertShutsUp(t *testing.T) {
|
|||||||
// is missing in the snapshot — gate must still return false.
|
// is missing in the snapshot — gate must still return false.
|
||||||
s := State{Now: refTime(), Presence: store.Present}
|
s := State{Now: refTime(), Presence: store.Present}
|
||||||
iwantfire := Rule{
|
iwantfire := Rule{
|
||||||
Name: "x",
|
Name: "x",
|
||||||
Severity: Sev1,
|
Severity: Sev1,
|
||||||
Predicate: func(State) bool { return true },
|
Predicate: func(State) bool { return true },
|
||||||
InertWhenNoData: []string{"missing_key"},
|
InertWhenNoData: []string{"missing_key"},
|
||||||
}
|
}
|
||||||
if Gate(s, iwantfire) {
|
if Gate(s, iwantfire) {
|
||||||
@@ -379,4 +379,4 @@ func TestGathererUsesFeedbackTunedCooldown(t *testing.T) {
|
|||||||
if d, ok := ParseCooldownFact(fb); !ok || d != tuned {
|
if d, ok := ParseCooldownFact(fb); !ok || d != tuned {
|
||||||
t.Fatalf("ParseCooldownFact round-trip: want %v ok, got %v ok=%v", tuned, d, ok)
|
t.Fatalf("ParseCooldownFact round-trip: want %v ok, got %v ok=%v", tuned, d, ok)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-21
@@ -15,10 +15,10 @@ import "time"
|
|||||||
// auto-tuner scales it over time (mostly ignored → lengthen; acted → leave).
|
// auto-tuner scales it over time (mostly ignored → lengthen; acted → leave).
|
||||||
// Bounds belong at the daemon-config level; here we just carry the base.
|
// Bounds belong at the daemon-config level; here we just carry the base.
|
||||||
type Rule struct {
|
type Rule struct {
|
||||||
Name string
|
Name string
|
||||||
Severity Severity
|
Severity Severity
|
||||||
Cooldown // base cooldown + bounded-duration envelope for the auto-tuner
|
Cooldown // base cooldown + bounded-duration envelope for the auto-tuner
|
||||||
Predicate func(State) bool
|
Predicate func(State) bool
|
||||||
|
|
||||||
// InertWhenNoData — most rules should be silent when their substrate key is
|
// InertWhenNoData — most rules should be silent when their substrate key is
|
||||||
// missing (since(key)==null → don't fire). If the predicate already encodes
|
// missing (since(key)==null → don't fire). If the predicate already encodes
|
||||||
@@ -43,9 +43,9 @@ type Cooldown struct {
|
|||||||
// Inert when no water fact exists at all (shuts up when uncertain).
|
// Inert when no water fact exists at all (shuts up when uncertain).
|
||||||
func WaterRule() Rule {
|
func WaterRule() Rule {
|
||||||
return Rule{
|
return Rule{
|
||||||
Name: "water",
|
Name: "water",
|
||||||
Severity: Sev1,
|
Severity: Sev1,
|
||||||
Cooldown: Cooldown{Base: 30 * time.Minute, Min: 15 * time.Minute, Max: 6 * time.Hour},
|
Cooldown: Cooldown{Base: 30 * time.Minute, Min: 15 * time.Minute, Max: 6 * time.Hour},
|
||||||
InertWhenNoData: []string{"water"},
|
InertWhenNoData: []string{"water"},
|
||||||
Predicate: func(s State) bool {
|
Predicate: func(s State) bool {
|
||||||
d, ok := s.Since("water")
|
d, ok := s.Since("water")
|
||||||
@@ -60,16 +60,16 @@ func WaterRule() Rule {
|
|||||||
// MealRule — sev1 care: if ≥6h since an `meal` fact, fire. Inert without data.
|
// MealRule — sev1 care: if ≥6h since an `meal` fact, fire. Inert without data.
|
||||||
func MealRule() Rule {
|
func MealRule() Rule {
|
||||||
return Rule{
|
return Rule{
|
||||||
Name: "meal",
|
Name: "meal",
|
||||||
Severity: Sev1,
|
Severity: Sev1,
|
||||||
Cooldown: Cooldown{Base: 60 * time.Minute, Min: 30 * time.Minute, Max: 8 * time.Hour},
|
Cooldown: Cooldown{Base: 60 * time.Minute, Min: 30 * time.Minute, Max: 8 * time.Hour},
|
||||||
InertWhenNoData: []string{"meal"},
|
InertWhenNoData: []string{"meal"},
|
||||||
Predicate: func(s State) bool {
|
Predicate: func(s State) bool {
|
||||||
d, ok := s.Since("meal")
|
d, ok := s.Since("meal")
|
||||||
if !ok {
|
if !ok {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return d >= 6 * time.Hour
|
return d >= 6*time.Hour
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,9 +79,9 @@ func MealRule() Rule {
|
|||||||
// Inert unless both exist — can't claim continuous activity without both anchors.
|
// Inert unless both exist — can't claim continuous activity without both anchors.
|
||||||
func BreakRule() Rule {
|
func BreakRule() Rule {
|
||||||
return Rule{
|
return Rule{
|
||||||
Name: "break",
|
Name: "break",
|
||||||
Severity: Sev2,
|
Severity: Sev2,
|
||||||
Cooldown: Cooldown{Base: 45 * time.Minute, Min: 20 * time.Minute, Max: 4 * time.Hour},
|
Cooldown: Cooldown{Base: 45 * time.Minute, Min: 20 * time.Minute, Max: 4 * time.Hour},
|
||||||
InertWhenNoData: []string{"desk_active", "break"},
|
InertWhenNoData: []string{"desk_active", "break"},
|
||||||
Predicate: func(s State) bool {
|
Predicate: func(s State) bool {
|
||||||
dDesk, ok1 := s.Since("desk_active")
|
dDesk, ok1 := s.Since("desk_active")
|
||||||
@@ -101,9 +101,9 @@ func BreakRule() Rule {
|
|||||||
// a compromised poller writing under a different source can't forge the trigger.
|
// a compromised poller writing under a different source can't forge the trigger.
|
||||||
func ServiceDownRule() Rule {
|
func ServiceDownRule() Rule {
|
||||||
return Rule{
|
return Rule{
|
||||||
Name: "service_down",
|
Name: "service_down",
|
||||||
Severity: Sev4,
|
Severity: Sev4,
|
||||||
Cooldown: Cooldown{Base: 15 * time.Minute, Min: 5 * time.Minute, Max: 1 * time.Hour},
|
Cooldown: Cooldown{Base: 15 * time.Minute, Min: 5 * time.Minute, Max: 1 * time.Hour},
|
||||||
InertWhenNoData: []string{"service_down"},
|
InertWhenNoData: []string{"service_down"},
|
||||||
Predicate: func(s State) bool {
|
Predicate: func(s State) bool {
|
||||||
f, ok := s.Fact("service_down")
|
f, ok := s.Fact("service_down")
|
||||||
@@ -123,9 +123,9 @@ func ServiceDownRule() Rule {
|
|||||||
// the hard page. Provenance-scoped to poll:netdata.
|
// the hard page. Provenance-scoped to poll:netdata.
|
||||||
func NetdataCriticalRule() Rule {
|
func NetdataCriticalRule() Rule {
|
||||||
return Rule{
|
return Rule{
|
||||||
Name: "netdata_critical",
|
Name: "netdata_critical",
|
||||||
Severity: Sev3,
|
Severity: Sev3,
|
||||||
Cooldown: Cooldown{Base: 20 * time.Minute, Min: 10 * time.Minute, Max: 2 * time.Hour},
|
Cooldown: Cooldown{Base: 20 * time.Minute, Min: 10 * time.Minute, Max: 2 * time.Hour},
|
||||||
InertWhenNoData: []string{"netdata_alarm"},
|
InertWhenNoData: []string{"netdata_alarm"},
|
||||||
Predicate: func(s State) bool {
|
Predicate: func(s State) bool {
|
||||||
f, ok := s.Fact("netdata_alarm")
|
f, ok := s.Fact("netdata_alarm")
|
||||||
@@ -148,4 +148,4 @@ func DefaultRules() []Rule {
|
|||||||
ServiceDownRule(),
|
ServiceDownRule(),
|
||||||
NetdataCriticalRule(),
|
NetdataCriticalRule(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,4 +106,4 @@ func (s State) Since(key string) (time.Duration, bool) {
|
|||||||
return 0, true
|
return 0, true
|
||||||
}
|
}
|
||||||
return s.Now.Sub(f.Ts), true
|
return s.Now.Sub(f.Ts), true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,4 +202,4 @@ func sevLabel(s loop.Severity) string {
|
|||||||
default:
|
default:
|
||||||
return "alarm"
|
return "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}
|
||||||
@@ -220,4 +220,4 @@ func TestStubProducesDeliveryTypes(t *testing.T) {
|
|||||||
// ----------------------------- helpers --------------------------------------
|
// ----------------------------- helpers --------------------------------------
|
||||||
// (the per-rule templates change output shape; assert via strings.Contains
|
// (the per-rule templates change output shape; assert via strings.Contains
|
||||||
// against salient fragments, not exact strings — keeps the tests robust to
|
// against salient fragments, not exact strings — keeps the tests robust to
|
||||||
// tone tweaks in the Stub.)
|
// tone tweaks in the Stub.)
|
||||||
|
|||||||
+11
-11
@@ -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.
|
||||||
|
|||||||
@@ -253,4 +253,4 @@ func scanFact(r rowScanner) (Fact, error) {
|
|||||||
f.Kind = FactKind(kind)
|
f.Kind = FactKind(kind)
|
||||||
f.VoidsID = voids
|
f.VoidsID = voids
|
||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,4 +181,4 @@ func (s *Store) LastNudge(ctx context.Context, rule string) (Nudge, error) {
|
|||||||
n.OutcomeTs = outcomeTs
|
n.OutcomeTs = outcomeTs
|
||||||
}
|
}
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ import (
|
|||||||
|
|
||||||
// Signal — one presence signal with hand-tuned fresh-weight and decay τ.
|
// Signal — one presence signal with hand-tuned fresh-weight and decay τ.
|
||||||
type Signal struct {
|
type Signal struct {
|
||||||
Key string
|
Key string
|
||||||
Weight float64
|
Weight float64
|
||||||
TauMin float64
|
TauMin float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// PresenceSignals — the three signals. Iterate in stable order.
|
// PresenceSignals — the three signals. Iterate in stable order.
|
||||||
@@ -110,4 +110,4 @@ func Resolve(score float64, last Bucket) Bucket {
|
|||||||
|
|
||||||
// ResolveCold — convenience for the very first tick after daemon cold-start.
|
// ResolveCold — convenience for the very first tick after daemon cold-start.
|
||||||
// presence_state has no row; we begin as Away, the fail-closed outcome.
|
// presence_state has no row; we begin as Away, the fail-closed outcome.
|
||||||
func ResolveCold(score float64) Bucket { return Resolve(score, Away) }
|
func ResolveCold(score float64) Bucket { return Resolve(score, Away) }
|
||||||
|
|||||||
@@ -58,4 +58,4 @@ func (s *Store) PresenceProbes(ctx context.Context) ([]SignalProbe, error) {
|
|||||||
probes = append(probes, SignalProbe{Key: sig.Key, LastTs: &t})
|
probes = append(probes, SignalProbe{Key: sig.Key, LastTs: &t})
|
||||||
}
|
}
|
||||||
return probes, nil
|
return probes, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,4 +142,4 @@ func TestNegativeClockSkewClampsToFresh(t *testing.T) {
|
|||||||
if got > 0.90+1e-9 {
|
if got > 0.90+1e-9 {
|
||||||
t.Fatalf("clock skew clamps: want <= weight(0.90), got %f", got)
|
t.Fatalf("clock skew clamps: want <= weight(0.90), got %f", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ import (
|
|||||||
|
|
||||||
// Reminder — user-stated future intent. fires once or recurring (if cron set).
|
// Reminder — user-stated future intent. fires once or recurring (if cron set).
|
||||||
type Reminder struct {
|
type Reminder struct {
|
||||||
ID int64
|
ID int64
|
||||||
CreatedTs time.Time
|
CreatedTs time.Time
|
||||||
FireTs time.Time
|
FireTs time.Time
|
||||||
NextFireTs time.Time // computed next fire (for recurring) or same as FireTs
|
NextFireTs time.Time // computed next fire (for recurring) or same as FireTs
|
||||||
Payload string // raw json
|
Payload string // raw json
|
||||||
Status string // pending | fired | cancelled
|
Status string // pending | fired | cancelled
|
||||||
Cron string // cron expression, empty for one-shot
|
Cron string // cron expression, empty for one-shot
|
||||||
|
|
||||||
// Collapsed — set only on a synthetic digest reminder (ID=0): the original
|
// Collapsed — set only on a synthetic digest reminder (ID=0): the original
|
||||||
// due reminders it stands in for. Not persisted. The dispatcher completes
|
// due reminders it stands in for. Not persisted. The dispatcher completes
|
||||||
@@ -144,4 +144,4 @@ func (s *Store) RescheduleReminder(ctx context.Context, id int64, now time.Time)
|
|||||||
}
|
}
|
||||||
_, err = s.db.ExecContext(ctx, "UPDATE reminders SET next_fire_ts = ? WHERE id = ?", next.UnixMilli(), id)
|
_, err = s.db.ExecContext(ctx, "UPDATE reminders SET next_fire_ts = ? WHERE id = ?", next.UnixMilli(), id)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,4 +123,4 @@ var (
|
|||||||
ErrConfidence = errors.New("store: confidence must be in (0.0, 1.0]")
|
ErrConfidence = errors.New("store: confidence must be in (0.0, 1.0]")
|
||||||
// ErrVoidsMissing — a correction pointed at a nonexistent fact.
|
// ErrVoidsMissing — a correction pointed at a nonexistent fact.
|
||||||
ErrVoidsMissing = errors.New("store: voids_id does not reference an existing fact")
|
ErrVoidsMissing = errors.New("store: voids_id does not reference an existing fact")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -377,4 +377,4 @@ func TestCalendarEvents(t *testing.T) {
|
|||||||
if len(events) != 0 {
|
if len(events) != 0 {
|
||||||
t.Fatalf("expected 0 events on July 8, got %d", len(events))
|
t.Fatalf("expected 0 events on July 8, got %d", len(events))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-6
@@ -62,12 +62,12 @@ func NewStub() *Stub { return &Stub{} }
|
|||||||
// classifier will route to a different intent (act / reminder / fact / note
|
// classifier will route to a different intent (act / reminder / fact / note
|
||||||
// / query). The hash picks the phrase per utterance deterministically.
|
// / query). The hash picks the phrase per utterance deterministically.
|
||||||
var stubPhrases = []string{
|
var stubPhrases = []string{
|
||||||
"maven, отметь что я выпил воды", // fact (water tap → fact table)
|
"maven, отметь что я выпил воды", // fact (water tap → fact table)
|
||||||
"maven, напомни через 4 часа размяться", // reminder (→ reminders table)
|
"maven, напомни через 4 часа размяться", // reminder (→ reminders table)
|
||||||
"maven, restart nginx", // act (→ stage-0 grammar hit)
|
"maven, restart nginx", // act (→ stage-0 grammar hit)
|
||||||
"maven, что у меня сегодня по календарю", // query (→ slm read-path, deferred)
|
"maven, что у меня сегодня по календарю", // query (→ slm read-path, deferred)
|
||||||
"note: idea — staggered cooldown by time of day", // note (→ chroma, deferred)
|
"note: idea — staggered cooldown by time of day", // note (→ chroma, deferred)
|
||||||
"slept 6h, fan noise wrecked it", // compound capture (open spec; routes as fact today)
|
"slept 6h, fan noise wrecked it", // compound capture (open spec; routes as fact today)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transcribe returns one of stubPhrases, indexed by a hash of the audio
|
// Transcribe returns one of stubPhrases, indexed by a hash of the audio
|
||||||
@@ -88,7 +88,7 @@ func (s *Stub) Transcribe(_ context.Context, a audio.Audio) (string, float64, er
|
|||||||
// the stt module's unix socket. The daemon constructs one when its config
|
// the stt module's unix socket. The daemon constructs one when its config
|
||||||
// points at a worker socket; otherwise it uses the Stub.
|
// points at a worker socket; otherwise it uses the Stub.
|
||||||
type Remote struct {
|
type Remote struct {
|
||||||
c *worker.Client
|
c *worker.Client
|
||||||
lang string
|
lang string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,4 +109,4 @@ func (r *Remote) Transcribe(ctx context.Context, a audio.Audio) (string, float64
|
|||||||
return "", 0, fmt.Errorf("stt: transcribe: %w", err)
|
return "", 0, fmt.Errorf("stt: transcribe: %w", err)
|
||||||
}
|
}
|
||||||
return resp.Text, resp.Confidence, nil
|
return resp.Text, resp.Confidence, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,4 +122,4 @@ func TestRemoteErrorWraps(t *testing.T) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("want error, got nil")
|
t.Fatalf("want error, got nil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -68,8 +68,8 @@ func (s *Stub) Synthesize(_ context.Context, text string) (audio.Audio, error) {
|
|||||||
// Remote — the worker-backed Synthesizer. Holds a worker.Client that dials
|
// Remote — the worker-backed Synthesizer. Holds a worker.Client that dials
|
||||||
// the tts module's unix socket.
|
// the tts module's unix socket.
|
||||||
type Remote struct {
|
type Remote struct {
|
||||||
c *worker.Client
|
c *worker.Client
|
||||||
lang string
|
lang string
|
||||||
voice string
|
voice string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,4 +90,4 @@ func (r *Remote) Synthesize(ctx context.Context, text string) (audio.Audio, erro
|
|||||||
return audio.Audio{}, fmt.Errorf("tts: synthesize: %w", err)
|
return audio.Audio{}, fmt.Errorf("tts: synthesize: %w", err)
|
||||||
}
|
}
|
||||||
return resp.Audio, nil
|
return resp.Audio, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,4 +135,4 @@ func samePCM(a, b []byte) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,9 +40,9 @@ type PushHandler interface {
|
|||||||
|
|
||||||
// Client — one connection to the voice.Server.
|
// Client — one connection to the voice.Server.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
addr string
|
addr string
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
c net.Conn
|
c net.Conn
|
||||||
nextID atomic.Uint64
|
nextID atomic.Uint64
|
||||||
|
|
||||||
// pushCh fan-out: a reader goroutine (started by RunPushReceiver)
|
// pushCh fan-out: a reader goroutine (started by RunPushReceiver)
|
||||||
@@ -237,4 +237,4 @@ func marshalParams(v any) (json.RawMessage, error) {
|
|||||||
return nil, fmt.Errorf("voice: marshal params: %w", err)
|
return nil, fmt.Errorf("voice: marshal params: %w", err)
|
||||||
}
|
}
|
||||||
return b, nil
|
return b, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) }
|
||||||
|
|||||||
@@ -57,4 +57,4 @@ func readFrame(r io.Reader, v any) error {
|
|||||||
return fmt.Errorf("voice: unmarshal frame: %w", err)
|
return fmt.Errorf("voice: unmarshal frame: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,4 +81,4 @@ func (s *StubReplier) Reply(d router.Decision) string {
|
|||||||
default:
|
default:
|
||||||
return "приняла."
|
return "приняла."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -234,4 +234,4 @@ func marshalResult(v any) json.RawMessage {
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
// io.EOF — used by serveConn to detect a quiet client disconnect.
|
// io.EOF — used by serveConn to detect a quiet client disconnect.
|
||||||
|
|||||||
+10
-11
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -220,4 +220,4 @@ func TestClientListenModeReceivesPush(t *testing.T) {
|
|||||||
|
|
||||||
type pushHandlerFunc func(Push)
|
type pushHandlerFunc func(Push)
|
||||||
|
|
||||||
func (f pushHandlerFunc) OnPush(p Push) { f(p) }
|
func (f pushHandlerFunc) OnPush(p Push) { f(p) }
|
||||||
|
|||||||
+19
-19
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,4 +188,4 @@ func marshalParams(v any) (json.RawMessage, error) {
|
|||||||
return nil, fmt.Errorf("worker: marshal params: %w", err)
|
return nil, fmt.Errorf("worker: marshal params: %w", err)
|
||||||
}
|
}
|
||||||
return b, nil
|
return b, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,4 +64,4 @@ func readFrame(r io.Reader, v any) error {
|
|||||||
return fmt.Errorf("worker: unmarshal frame: %w", err)
|
return fmt.Errorf("worker: unmarshal frame: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,4 +22,4 @@ type Transcriber interface {
|
|||||||
// so a server asked for the wrong verb refuses cleanly).
|
// so a server asked for the wrong verb refuses cleanly).
|
||||||
type Synthesizer interface {
|
type Synthesizer interface {
|
||||||
Synthesize(ctx context.Context, req SynthesizeReq) (SynthesizeResp, error)
|
Synthesize(ctx context.Context, req SynthesizeReq) (SynthesizeResp, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,13 +42,13 @@ type TranscribeResp struct {
|
|||||||
|
|
||||||
// SynthesizeReq — the synthesize verb args.
|
// SynthesizeReq — the synthesize verb args.
|
||||||
type SynthesizeReq struct {
|
type SynthesizeReq struct {
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
Lang string `json:"lang"` // "ru" | "en" | ""
|
Lang string `json:"lang"` // "ru" | "en" | ""
|
||||||
Voice string `json:"voice"` // named voice or "" ⇒ worker default
|
Voice string `json:"voice"` // named voice or "" ⇒ worker default
|
||||||
Speed float64 `json:"speed"` // 1.0 = normal; clamped server-side
|
Speed float64 `json:"speed"` // 1.0 = normal; clamped server-side
|
||||||
}
|
}
|
||||||
|
|
||||||
// SynthesizeResp — the synthesize verb result.
|
// SynthesizeResp — the synthesize verb result.
|
||||||
type SynthesizeResp struct {
|
type SynthesizeResp struct {
|
||||||
Audio audio.Audio `json:"audio"` // rendered PCM
|
Audio audio.Audio `json:"audio"` // rendered PCM
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -225,4 +225,4 @@ func parentDir(p string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "."
|
return "."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ type Request struct {
|
|||||||
// Response — one frame back from the worker. Exactly one of Result/Error is set.
|
// Response — one frame back from the worker. Exactly one of Result/Error is set.
|
||||||
type Response struct {
|
type Response struct {
|
||||||
Result json.RawMessage `json:"r,omitempty"`
|
Result json.RawMessage `json:"r,omitempty"`
|
||||||
Error *RpcError `json:"e,omitempty"`
|
Error *RpcError `json:"e,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RpcError — a typed wire error. Mirrors internal/ipc's shape for tooling
|
// RpcError — a typed wire error. Mirrors internal/ipc's shape for tooling
|
||||||
@@ -120,4 +120,4 @@ func rpcErr(err error) *RpcError {
|
|||||||
return &RpcError{Code: c, Message: err.Error()}
|
return &RpcError{Code: c, Message: err.Error()}
|
||||||
}
|
}
|
||||||
return &RpcError{Code: c}
|
return &RpcError{Code: c}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -286,4 +286,4 @@ func TestPathAfterListen(t *testing.T) {
|
|||||||
|
|
||||||
func contains(haystack, needle string) bool {
|
func contains(haystack, needle string) bool {
|
||||||
return len(haystack) >= len(needle) && (bytes.Contains([]byte(haystack), []byte(needle)))
|
return len(haystack) >= len(needle) && (bytes.Contains([]byte(haystack), []byte(needle)))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user