diff --git a/SESSION-06-07-2026.md b/SESSION-06-07-2026.md new file mode 100644 index 0000000..eb045f4 --- /dev/null +++ b/SESSION-06-07-2026.md @@ -0,0 +1,347 @@ +# Overnight Session — 2026-07-06 + +Branch: `overnight-jul6` (from `master`) +Executor: a single unsupervised agent working through the night. + +--- + +## READ THIS FIRST — Operating rules (do not skip) + +You are working **unsupervised**. Optimize for *not breaking anything* over +finishing every task. A half-finished task that compiles and is committed is a +success; a clever half-rewrite that breaks the build is a failure. + +**Hard rules:** + +1. **One task = one commit.** Never batch two tasks into one commit. Commit + message: `maven: (task N)`. Sign-off line required (see repo + convention — Co-Authored-By trailer). +2. **TDD, always.** For every task that touches Go: write the test first, watch + it fail, then write code until it passes. Tests live next to the code as + `*_test.go`. Copy the style of the nearest existing test file. +3. **After every task, run the gate before committing:** + ``` + gofmt -l . # must print nothing + go build ./... # must succeed + go vet ./... # must be clean + go test ./... # must be green + ``` + If any step fails and you cannot fix it in ~15 min, **`git stash` or revert + that task, write a note in the task's Status cell ("BLOCKED: "), and + move to the next task.** Do not leave a broken tree. +4. **Never invent config keys, function names, or file paths.** Every new thing + copies an existing pattern named in the task. If you can't find the pattern, + mark the task BLOCKED and skip it. +5. **Tools/acts: never add a tool without `"destructive": true` unless it is + provably read-only** (see Task 2). A destructive act that runs from voice + without a confirm gate is the worst possible bug. When unsure → destructive. +6. **Do NOT attempt the "DEFERRED — needs human" section at the bottom.** Those + need hardware or protocol decisions. Touching them unsupervised will waste + the night. They are listed only so you don't rediscover them. +7. Prefer additive changes. Do not refactor existing packages. Do not touch + `cmd/mavweb/`, encryption, or the store schema unless a task says to. + +**Work top-to-bottom.** Tasks are ordered by value-per-risk: safest and most +self-contained first. If you run out of night, the earlier tasks are the ones +that matter. + +--- + +## Key facts about the codebase (so you don't have to rediscover them) + +- **Router cascade**: `internal/router/`. Intents are the constants in + `intent.go` (`act, reminder, fact, note, query, system`). Adding an intent = + add a const there + seed examples + a handler case. +- **Intent seeds**: `models/seeds/.txt`, one example per line, `#` + comments allowed. Loaded by `seedClassifier` in `cmd/mavend/voice.go`. To + teach the classifier a new phrase, add a line to the right seed file — no code + change needed. +- **Voice intent dispatch**: `cmd/mavend/voice.go`, the big `switch dec.Intent` + (search `case router.IntentQuery:` ~line 412). Each intent returns a Russian + reply string. This is where a new intent's behaviour hangs. +- **Tools/acts**: enabled allowlist lives in config `voice.tools` (see + `internal/config/config.go` `ToolConfig`). Executor: `internal/tool/tool.go`. + Args are argv, never shell. Destructive tools return `ErrNeedsConfirm`. +- **Config**: `internal/config/config.go`. Seed/prod config: `deploy/mavend.json`. +- **Store** (facts, notes, reminders, tools): `internal/store/`. CalDAV events + are written as facts with `source=caldav` plus a `calendar_busy` key (per the + poller in `mavpoll`/`mavcaldav`). +- **Embedder**: `voice.embedder` config → ONNX; nil → `router.NewHashEmbedder` + floor. Wiring is in `cmd/mavend/voice.go` ~line 144. +- **Language is Russian.** Maven refers to herself in the **feminine**. All + user-facing reply strings are RU. Copy tone from existing replies. + +--- + +## Phase 0 — Setup (do this once, first) + +- [ ] `git checkout master && git pull` (if remote), then + `git checkout -b overnight-jul6` +- [ ] Run the full gate (`go build ./... && go vet ./... && go test ./...`) on a + clean tree to confirm a green baseline **before** you change anything. If + baseline is red, STOP and record it here — do not build on a broken tree. + +--- + +## Task 1 — Embedder config validation + docs (safest, do first) + +**Goal:** make embedder misconfiguration fail loudly instead of silently +falling back to the Hash floor. + +**Files:** `internal/config/config.go` (Validate path), its `*_test.go`, +`deploy/mavend.json`, and a short note in `START.md` or `PROGRESS.md`. + +**Do:** +1. Find where `VoiceConfig` / `EmbedderConfig` is validated (look for a + `Validate()` method or the load path in `config.go`). Add a check: if + `Embedder` is non-nil, then **all three** of `ModelPath`, `TokenizerPath`, + `LibPath` must be non-empty — a partially-filled embedder block is a config + error (`return fmt.Errorf(...)`). If `Embedder` is nil, that's fine (Hash + floor) — no error. +2. In `cmd/mavend/voice.go` around the embedder wiring (~line 144–159), make the + "falling back to HashEmbedder" path an explicit `log.Printf("voice: embedder + not configured, using HashEmbedder floor")` if it isn't already. +3. Add a test to `config_test.go` covering: all-three-set → ok; one-missing → + error; nil → ok. +4. Document the `voice.embedder` block (all three paths, and "omit the block to + use the floor") in `START.md` near other config docs. + +**Done when:** new test passes, gate green, docs updated. One commit. + +--- + +## Task 2 — Seed the tool allowlist with safe homelab acts + +**Goal:** give the voice `act` path a useful, SAFE starter allowlist. + +**Files:** `deploy/mavend.json` (`voice.tools`), and `models/seeds/act.txt`. + +**Do:** +1. Add tools to `voice.tools` in `deploy/mavend.json`. Each: `name`, `cmd` + (argv prefix), `scope`, `destructive`. Classify carefully: + - **Read-only (destructive: false)** — safe to fire from voice: + `systemctl status`, `docker ps`, `uptime`, `df`, `free`, journal *reads* + (`journalctl -n 50 -u ` — note the unit comes as an arg). + - **Destructive: true** — must confirm: `systemctl restart`, `systemctl stop`, + `docker restart`, `reboot`, `docker stop`. + - When unsure → `destructive: true`. +2. Add matching spoken RU phrasings to `models/seeds/act.txt` (e.g. «покажи + статус nginx», «перезапусти nginx», «сколько места на диске») so the + classifier routes them to `act`. One per line. +3. There is **no Go change** here if the executor already reads `voice.tools`. + Verify by reading the wiring — if tools are loaded from config into the store + allowlist at boot, you're done. If not, mark BLOCKED (don't build new wiring). + +**Done when:** `go test ./...` still green (config parses), the JSON is valid +(`go run` the daemon far enough to parse, or a small config-load test). One commit. + +**Guardrail:** double-check no `restart`/`stop`/`reboot`/`rm`/`kill` entry has +`destructive: false`. This is the single most important check of the night. + +--- + +## Task 3 — Calendar event querying ("что у меня завтра?") + +**Goal:** answer calendar questions from CalDAV facts already in the store. + +**Files:** `internal/router/` (slots + a `query` sub-path, or reuse `IntentQuery` +with a calendar slot), `cmd/mavend/voice.go` (handler), `models/seeds/query.txt`, +and tests. + +**Approach (keep it simple — don't add a new intent if you can avoid it):** +1. The data is already there: CalDAV events are facts with `source=caldav`. Find + the store method that reads facts by source/date (grep `caldav` in + `internal/store/`). If none scopes by date, add a small read helper + `CalendarEvents(ctx, from, to time.Time)` next to the existing facts queries — + copy the style of an existing `store/facts.go` query, with a test. +2. Add date-scope parsing: «сегодня» → today, «завтра» → tomorrow. Put this in a + small helper in `internal/router/slots.go` (copy the RU parsing style in + `slots_ru_test.go`). Test it directly. +3. In the `IntentQuery` handler in `voice.go`, detect a calendar question (the + utterance mentions планы/календарь/завтра/сегодня + no note match, OR a + dedicated keyword check *before* the notes RAG lookup). Read events for the + scoped day, format an RU reply: empty → «на сегодня ничего нет», one/many → + list them. Keep formatting in a tested pure helper. +4. Seed `models/seeds/query.txt` with the example phrasings. + +**Done when:** helper tests + a handler-level test pass, gate green. One commit +(or two: store helper, then handler — that's fine, keep them separate). + +**If store scoping turns out hard:** ship just the date parser + formatter as +pure tested helpers and wire them to a naive "read all caldav facts, filter in +Go" — personal scale, correctness over efficiency. Do not add schema. + +--- + +## Task 4 — General-knowledge routing to the phraser + +**Goal:** route open factual questions to the phraser with an anti-hallucination +system prompt and a fallback. + +**Files:** `cmd/mavend/voice.go` (query handler), phraser call site (grep +`phraser` / `Phrase` in voice.go and `internal/phraser/`), `models/seeds/query.txt`, +tests. + +**Do:** +1. In the `IntentQuery` handler, **after** the notes-RAG lookup fails to clear + `queryMinScore` (currently returns «у меня нет заметок…»), instead of giving + up, hand the question to the phraser with a system prompt like: «Ответь кратко + из своих знаний. Если не знаешь — скажи "не знаю". Не выдумывай.» (feminine + self-reference). +2. **Fallback gate:** if the phraser returns empty, errors, or the phraser is the + Stub (not configured), return «не знаю» / the existing no-answer reply. Never + fabricate. +3. Keep the prompt construction in a small pure function so you can unit-test it + (assert the system prompt text + that empty phraser output → fallback). + +**Done when:** prompt-construction test + fallback test pass, gate green. One +commit. + +**Risk note:** the phraser is a small model and will hallucinate. The fallback is +the point of this task — test it hard. Do not remove the notes-RAG path; this is +a *fallback after* it. + +--- + +## Task 5 — Weather module skeleton (pure, no network at night) + +**Goal:** a pluggable weather provider interface + an unconfigured stub. **No +live API calls.** + +**Files:** new `internal/weather/` package, `internal/config/config.go` +(a `WeatherConfig` block, copy `PhraserConfig` shape), tests. + +**Do:** +1. `internal/weather/weather.go`: define + `type Provider interface { CurrentWeather(ctx, location string) (Weather, error) }` + and a `Weather` struct (temp, condition, location). Add a `StubProvider` that + returns a sentinel `ErrNotConfigured` (or a "погода не настроена" message). +2. Config: add `Weather *WeatherConfig` to `VoiceConfig` (fields: `Provider`, + `APIKey`, `DefaultLocation` — all omitempty). **No API key in the repo.** +3. Optionally add an Open-Meteo provider *struct that is not called at night* + (no key needed) — but if you write it, do NOT make a network call in tests; + test against a mocked HTTP round-tripper only. If that's too fiddly, ship just + the interface + stub and leave a `// TODO: open-meteo provider` — that's fine. +4. **Add a real Open-Meteo provider** (keyless — no API key needed). Endpoint: + `https://api.open-meteo.com/v1/forecast?latitude=..&longitude=..¤t_weather=true`. + Geocode via `https://geocoding-api.open-meteo.com/v1/search?name=`. + Keep the `*http.Client` injectable (a struct field) so tests use a mocked + round-tripper — **no real network call in any test.** Config selects + provider by `voice.weather.provider` ("open-meteo" | "" → stub). +5. **Wire it into voice.go.** In the `IntentQuery` handler, detect a weather + question (keywords погода/градус/температура, or a `query_weather` sub-path) + → call the configured provider with `DefaultLocation` or a parsed location → + format an RU reply. Unconfigured → the stub's «погода не настроена» message. + Seed `models/seeds/query.txt` with «какая погода», «какая погода в москве». + Bound the provider call with a context timeout (~5s) so a slow API can't hang + the voice turn. + +**Done when:** stub + mocked-Open-Meteo tests pass (round-trip against a fake +transport, unconfigured → stub message, location parsing), gate green. Split into +two commits if helpful: provider+interface, then voice wiring. + +--- + +## Task 6 — Dialogue state scaffold (pure data structures) + +**Goal:** a session/context data layer for future multi-turn. **No LLM, no +wiring into the live path unless trivial and tested.** + +**Files:** new `internal/dialogue/` package + tests only. + +**Do:** +1. `internal/dialogue/session.go`: a `Session` holding last-turn intent + slots, + a timestamp, and a TTL (default ~2min, configurable via a field). A + `SessionStore` (in-memory map keyed by session id) with `Get`, `Put`, and + TTL-based expiry. +2. A pure `InheritSlots(prev, cur Slots) Slots` helper: carry forward slots the + current turn is missing (e.g. previous had a location, current didn't → use + previous). Copy the `Slots` shape from `internal/router/intent.go`. +3. Tests: context carry-over, slot inheritance, session expiry, missing prior + session. This is the whole task — it's a tested library, not a feature. + +**Done when:** tests pass, gate green. Then (only if the library is solid and +gate is green) **wire a minimal read seam into voice.go**: on a follow-up-shaped +utterance, look up the prior session's slots and fill the current turn's missing +slots via `InheritSlots` before routing. Keep the session store's lifetime owned +by the handler struct. If wiring gets fiddly or risks the live path, ship the +tested library and mark the wiring BLOCKED — the library is the required part. +Separate commits: library, then wiring. + +--- + +## Task 7 — Long-term memory vector-store interface (pure) + +**Goal:** an interface + in-memory implementation for a future vector backend. + +**Files:** new `internal/memory/store.go` + tests only. + +**Do:** +1. `type Store interface { Insert(ctx, id string, vec []float32, meta map[string]string) error; Search(ctx, vec []float32, topK int) ([]Result, error) }`. + `Result` = id, score, meta. +2. An `InMemoryStore` implementing it with cosine similarity (copy the `cosine` + function idea from `internal/router/classifier.go` — you may factor a shared + helper, but simplest is to reimplement locally; don't refactor the router). +3. Tests: insert→search round-trip, cosine ordering (nearest first), topK + truncation, metadata filtering if you add it. In-memory only. + +**Done when:** tests pass, gate green (library commit). Then **wire the embedding +pipeline**: in the `IntentNote` handler in `voice.go`, after `WriteNote`, also +`Insert` the note's embedding + metadata (id, source, ts) into the memory Store. +Use the **same embedder** the classifier uses (already in scope as `h.embedder`). +Make the memory Store a field on the handler, defaulting to `InMemoryStore` so +nothing external is required. Wrap the Insert in its own error branch — a memory +Insert failure must **not** fail the note write (log and continue). Separate +commit for the wiring. + +--- + +## Phase Final — Verification pass (always do this last) + +- [ ] `gofmt -l .` prints nothing +- [ ] `go build ./...` succeeds +- [ ] `go vet ./...` clean +- [ ] `go test ./...` green +- [ ] `docker compose build` succeeds (all daemons compile) — if docker is + unavailable in the environment, note it and rely on `go build ./...`. +- [ ] `git log --oneline master..HEAD` — confirm one commit per completed task, + each message names its task, no "wip"/debug commits. +- [ ] Grep for accidents: `grep -rn "destructive.*false" deploy/mavend.json` and + eyeball every hit; `grep -rniE "TODO|FIXME|panic\(|fmt.Println" cmd internal` + — no stray debug prints, no new panics in live paths. +- [ ] Update the Status column of each task in this file (Done / BLOCKED:reason / + Skipped) so the human can see what happened at a glance. + +--- + +## Status board (fill this in as you go) + +| # | Task | Commit | Status | +|---|------|--------|--------| +| 1 | Embedder config validation + docs | | | +| 2 | Seed safe tool allowlist | | | +| 3 | Calendar querying | | | +| 4 | General-knowledge phraser routing | | | +| 5 | Weather skeleton (pure) | | | +| 6 | Dialogue scaffold (pure) | | | +| 7 | Memory vector interface (pure) | | | +| F | Final verification pass | | | + +--- + +## DEFERRED — needs a human, DO NOT ATTEMPT unsupervised + +These were in the original plan. They require hardware or protocol decisions and +will burn the night if attempted blind. Left here only so you don't rediscover +them and think they were forgotten. + +- **Always-on listening / wake word** (`internal/wake/`, `cmd/mavmic/`): needs a + hardware decision (USB mic vs Pi vs smart speaker) and a Porcupine license/key. + Human input required. +- **Streaming STT over WebSocket + barge-in** (`internal/voice` receive path): + changes the wire protocol (`PROTOCOL.md`) and the STT worker contract. Too + invasive to do safely unsupervised; risks breaking the working full-clip path. +- **Streaming TTS**: blocked on Piper. Out of scope. + +If you finish Tasks 1–7 with time to spare, do NOT start these. Instead: improve +test coverage on what you built, expand the seed files, and improve docs. diff --git a/cmd/mavweb/dash.html b/cmd/mavweb/dash.html index b3a6e24..189d214 100644 --- a/cmd/mavweb/dash.html +++ b/cmd/mavweb/dash.html @@ -3,21 +3,31 @@ maven · dash {{template "nav" "dash"}} -

presence

-

{{.Presence.Bucket}} — score {{printf "%.2f" .Presence.Score}} ({{ago .Presence.Updated}})

+
+
+

presence

+

{{.Presence.Bucket}} — score {{printf "%.2f" .Presence.Score}} ({{ago .Presence.Updated}})

обновляется каждые 10с
-

nudges

+
+
+

nudges

{{range .Nudges}}{{end}}
whenrulechanoutcomemessage
{{ago .Ts}}{{.Rule}}{{.Channel}}{{.Outcome}}{{.Message}}
-

facts

+
+
+

facts

{{range .Facts}}{{end}}
whenkindkeyvaluesourceconf
{{ago .Ts}}{{.Kind}}{{.Key}}{{.Value}}{{.Source}}{{printf "%.2f" .Confidence}}
-

notes

+
+
+

notes

{{range .Notes}}{{end}}
whensourcetext
{{ago .Ts}}{{.Source}}{{.Text}}
+
+
-