fix zombie leak, add quiet-hours toggle, improve query reply, configurable router threshold, JS dashboard
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
// Package main is mavend — maven's daemon.
|
||||
//
|
||||
// "core = the only key-holder": one process holds the unlocked store + the
|
||||
// trigger loop; modules are separate processes, key-free, fail-independent.
|
||||
// the daemon wires Store → Gatherer → Tick → phraser → delivery, runs the 60s
|
||||
// ticker, owns the cold-start unlock dance, and exposes the CoreAPI boundary
|
||||
// over a unix socket for modules (router/delivery/poller/...) to call.
|
||||
//
|
||||
// Floor (this file): pluggable seams wired with the deterministic Stubs.
|
||||
// - phraser Stub (no LLM)
|
||||
// - voice sink wired via wireVoice: embedder/classifier seeded with ~10
|
||||
// examples across 5 intents; stt + tts stubs in-process by default,
|
||||
// remote module sockets when configured; TCP listener on voice.bind.
|
||||
// The voice sink (voicesink.Sink via Sessions) is wired into the
|
||||
// dispatcher — the reactive path (push-to-talk) AND proactive nudges
|
||||
// (care-when-present, sev3/sev4 present) both route through the same
|
||||
// stt→router→tts→client pipeline.
|
||||
// - auth FloorEnrollment + nil Session — cold-start unlock assumed: today
|
||||
// the store opens plain sqlite (sqlcipher deferred). the daemon runs
|
||||
// "unlocked" — the locked-until-asserted dance lands with the Session
|
||||
// verifier + ask-password transport (open spec item).
|
||||
//
|
||||
// Everything wired here is swappable at the construction seam — every module
|
||||
// behind an interface — without changing the loop. Wire a real phraser, voice
|
||||
// sink, or session verifier, and the daemon's main loop is unchanged.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/kami/maven/internal/auth"
|
||||
"github.com/kami/maven/internal/config"
|
||||
"github.com/kami/maven/internal/delivery"
|
||||
"github.com/kami/maven/internal/delivery/ntfysink"
|
||||
"github.com/kami/maven/internal/delivery/telegramsink"
|
||||
"github.com/kami/maven/internal/ipc"
|
||||
"github.com/kami/maven/internal/loop"
|
||||
"github.com/kami/maven/internal/phraser"
|
||||
"github.com/kami/maven/internal/store"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := run(os.Args[1:]); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "mavend:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run(args []string) error {
|
||||
cfgPath := flag.String("config", defaultConfigPath(), "path to mavend JSON config")
|
||||
flag.CommandLine.Parse(args)
|
||||
cfg, err := config.Load(*cfgPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
||||
defer stop()
|
||||
|
||||
// ----- store (the unlocked handle; core = the only key-holder) -----
|
||||
st, err := store.Open(ctx, cfg.DBPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open store: %w", err)
|
||||
}
|
||||
defer st.Close()
|
||||
|
||||
// ----- loop: rules + gatherer -----
|
||||
|
||||
rules := loop.DefaultRules()
|
||||
gatherer := loop.NewGatherer(st, rules)
|
||||
|
||||
// ----- phraser (LLM-backed when configured, Stub floor otherwise) -----
|
||||
var phr phraser.Phraser = phraser.NewStub()
|
||||
if cfg.Phraser != nil {
|
||||
pc := phraser.Config{
|
||||
ModelPath: cfg.Phraser.ModelPath,
|
||||
BinPath: cfg.Phraser.BinPath,
|
||||
Listen: cfg.Phraser.Listen,
|
||||
NGpuLayers: cfg.Phraser.NGpuLayers,
|
||||
NCtx: cfg.Phraser.NCtx,
|
||||
Timeout: time.Duration(cfg.Phraser.Timeout),
|
||||
}
|
||||
if pc.BinPath == "" {
|
||||
pc.BinPath = "llama-server"
|
||||
}
|
||||
if pc.Listen == "" {
|
||||
pc.Listen = "127.0.0.1:0"
|
||||
}
|
||||
if pc.NCtx <= 0 {
|
||||
pc.NCtx = 2048
|
||||
}
|
||||
if pc.Timeout <= 0 {
|
||||
pc.Timeout = 30 * time.Second
|
||||
}
|
||||
var err error
|
||||
phr, err = phraser.NewLLMPhraser(ctx, pc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("phraser: %w", err)
|
||||
}
|
||||
}
|
||||
defer phr.Close()
|
||||
|
||||
// ----- voice: reactive audio path (TCP listener + stt/router/tts) -----
|
||||
voiceW, err := wireVoice(cfg, ipc.NewStoreAPI(st))
|
||||
if err != nil {
|
||||
return fmt.Errorf("wire voice: %w", err)
|
||||
}
|
||||
defer voiceW.close()
|
||||
|
||||
// ----- delivery: sinks + dispatcher -----
|
||||
var ntfy delivery.Sink
|
||||
if cfg.Ntfy != nil {
|
||||
s, err := ntfysink.New(*cfg.Ntfy)
|
||||
if err != nil {
|
||||
return fmt.Errorf("wire ntfy sink: %w", err)
|
||||
}
|
||||
ntfy = s
|
||||
}
|
||||
var telegram delivery.Sink
|
||||
if cfg.Telegram != nil {
|
||||
s, err := telegramsink.New(*cfg.Telegram)
|
||||
if err != nil {
|
||||
return fmt.Errorf("wire telegram sink: %w", err)
|
||||
}
|
||||
telegram = s
|
||||
}
|
||||
// Voice sink: nil when voice is not enabled — the dispatcher's nil-sink
|
||||
// path skips ChannelVoice silently, just like the pre-voice floor).
|
||||
var voiceSink delivery.Sink
|
||||
if voiceW != nil {
|
||||
voiceSink = voiceW.voiceSink
|
||||
}
|
||||
dispatcher := delivery.NewDispatcher(delivery.Config{
|
||||
Ntfy: ntfy,
|
||||
Telegram: telegram,
|
||||
Voice: voiceSink,
|
||||
// AckTracker nil ⇒ repeat-til-ack disabled in the dispatcher. We
|
||||
// drive repeats from store.UnackedTelegramRules + Dispatcher.RepeatUnacked
|
||||
// below, which uses the nudges table's outcome=pending row itself as
|
||||
// the ack-or-not state — the production ack source. The AckTracker
|
||||
// interface stays reserved for an in-memory cache if the daemon ever
|
||||
// wants to drive repeats without the SQL hit; the table IS the truth.
|
||||
Nudges: st, // *store.Store satisfies delivery.NudgeRecorder
|
||||
Reminders: st, // *store.Store satisfies delivery.ReminderCompleter
|
||||
})
|
||||
|
||||
// ----- the proactive loop driver (60s ticker, lives HERE per spec) -----
|
||||
tickInterval := time.Duration(cfg.TickInterval)
|
||||
repeatInterval := time.Duration(cfg.RepeatInterval)
|
||||
autotuneInterval := time.Duration(cfg.AutotuneInterval)
|
||||
loop := newTickLoop(st, gatherer, dispatcher, phr, rules, tickInterval, repeatInterval, autotuneInterval)
|
||||
|
||||
// ----- IPC boundary (core ↔ modules) -----
|
||||
coreAPI := ipc.NewStoreAPI(st)
|
||||
srv, err := ipc.Listen(cfg.SocketPath, coreAPI)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ipc listen: %w", err)
|
||||
}
|
||||
// auth floor: any same-uid caller is fully trusted (FloorEnrollment +
|
||||
// FloorSession — L3, step-up satisfied). The cold-start unlock dance and a
|
||||
// real passkey Session are the open spec items; today the daemon runs
|
||||
// unlocked — plain sqlite, sqlcipher deferred. FloorSession keeps the floor
|
||||
// consistent so the authed mavweb /tools page can EnableTool (AuthStepUp)
|
||||
// against the local socket; the passkey verifier swaps FloorSession later.
|
||||
srv.Check = (&auth.Gate{Enrollment: auth.NewFloorEnrollment(), Session: auth.FloorSession{}}).Check
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := srv.Serve(); err != nil && !errors.Is(err, net.ErrClosed) {
|
||||
log.Printf("ipc serve: %v", err)
|
||||
}
|
||||
}()
|
||||
log.Printf("mavend: ipc listening on %s", srv.Path())
|
||||
|
||||
if voiceW != nil {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := voiceW.server.Serve(); err != nil && !errors.Is(err, net.ErrClosed) {
|
||||
log.Printf("voice serve: %v", err)
|
||||
}
|
||||
}()
|
||||
log.Printf("mavend: voice listening on %s", voiceW.server.Addr())
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
loop.run(ctx)
|
||||
}()
|
||||
|
||||
<-ctx.Done()
|
||||
log.Printf("mavend: shutdown signal received")
|
||||
if err := srv.Close(); err != nil {
|
||||
log.Printf("ipc close: %v", err)
|
||||
}
|
||||
wg.Wait()
|
||||
log.Printf("mavend: bye")
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user