Files
Maven/SESSION-06-07-2026.md
T

348 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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-title> (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: <reason>"), 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/<intent>.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 144159), 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 <unit>` — 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=..&current_weather=true`.
Geocode via `https://geocoding-api.open-meteo.com/v1/search?name=<location>`.
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 | `b778f0b` | Done |
| 2 | Seed safe tool allowlist | `3b8fb69` | Done |
| 3 | Calendar querying | `cf066bd` | Done |
| 4 | General-knowledge phraser routing | `428af3f` | Done |
| 5 | Weather skeleton (pure) | `e030466` | Done |
| 6 | Dialogue scaffold (pure) | `79eb43e` | Done |
| 7 | Memory vector interface (pure) | `880715f` | Done |
| F | Final verification pass | `d52f60c` | Done — all gates green |
---
## 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 17 with time to spare, do NOT start these. Instead: improve
test coverage on what you built, expand the seed files, and improve docs.