Merge master into the line B review stack (V-405)
The two open lines never met: line A landed through #168, so every pull request from #148 to #160 conflicted with master on six files. This reconciles them. Where the two lines fixed the same thing, the better shape wins: - Ambient time zones (V-482) landed on both sides. Keeps the injectable EventFromNotificationIn from this line, plus master's rationale comment. Drops master's forced n.Posted.In(time.Local), which defeated the loc argument. - tick.go: master's guardNudge call and say.CountWord edits, moved onto the split files this line created. The digest summary now declines through say.CountWord inside tick_digest.go. - voice.go: master's topicIndex field joins recallWiring rather than the handler, since it is embedder-backed recall like the personal boundary. topics.go and its test read h.recall.topics now. - mavweb: master's capability and risk columns ported into tools.html, which is where this line moved the markup. The Go const is gone. - Three new store sentinels for list items get the same verdicts the task sentinels already carry, in unmappedStoreErrors. make build: 12 binaries. make test: green. make fmt-check: clean. --no-verify: a merge of two long lines cannot fit the 300-line budget. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+111
@@ -223,6 +223,41 @@ Not alternatives — layers:
|
||||
Router contract: `[{"intent":<enum>, key?, value?, text?, verb?}, ...]` over
|
||||
7 intents (`fact, reminder, note, query, act, chat, system`).
|
||||
|
||||
#### "второй" points at the list she just read
|
||||
|
||||
Landed 2026-08-04 (Vikunja #448). The dialogue session carried the intent, the
|
||||
slots and the history, and not the list. She recited five tasks, he said
|
||||
"второй", and the word had nothing to point at.
|
||||
|
||||
`Session.Candidates` holds what she just offered, bound at the moment she speaks
|
||||
it and in the order she speaks it (`tasks.Spoken`). Binding afterwards would
|
||||
resolve the word against a fresh query, and the list changes between two turns.
|
||||
`cmd/mavend/ordinal.go` reads the position before routing and dispatches on the
|
||||
candidate's kind.
|
||||
|
||||
An ordinal with no verb is read back, not acted on — "второй" names a task, it
|
||||
does not say what to do with it. With a verb ("первую сделал", "последнюю
|
||||
убери") the task moves and the list is spent, because a second ordinal against a
|
||||
list that no longer holds closes the wrong work. A position she never read is
|
||||
answered with how many she did read, not routed as a fresh sentence.
|
||||
|
||||
#### Saying she got it wrong is a feature
|
||||
|
||||
Landed 2026-08-04 (Vikunja #455). `Router.CorrectMisroute` could always append a
|
||||
corrected utterance as a new classifier example, and until now nothing in the
|
||||
daemon called it, so the mechanism existed and the behaviour did not.
|
||||
|
||||
`cmd/mavend/repair.go` reaches it. He says she got it wrong and names what it
|
||||
should have been — "нет, это заметка", "это не напоминание, а факт" — and three
|
||||
things happen in one turn: the classifier learns the utterance under the named
|
||||
intent, the request is redone under it, and she says the correction landed. The
|
||||
utterance he is correcting TO is the one with no "не" in front of it.
|
||||
|
||||
Read before routing, next to the confirm and clarify turns, because a correction
|
||||
routed as a fresh utterance files the correction itself. One turn is correctable
|
||||
once, inside five minutes, and only turns she acted on — a clarify asked instead
|
||||
of acting, so there is nothing yet to be wrong about.
|
||||
|
||||
#### A restart expires a parked question
|
||||
|
||||
Decided 2026-08-04 (Vikunja #385). The follow-up dialogue session survives a
|
||||
@@ -317,6 +352,57 @@ don't improvise.** Destructive ones still gate behind confirm.
|
||||
Misroute correction is append-only and grows the router's examples with use —
|
||||
same shape as `nudges.outcome` tuning cooldowns, no retrain.
|
||||
|
||||
#### Risk tiers, not one boolean
|
||||
|
||||
`Destructive` on a tool row is one bit set by whoever ticked the checkbox on
|
||||
`/tools`. It is a mechanism, and it never said which acts are destructive,
|
||||
whether a confirmed act stays confirmed, or what a new tool domain inherits.
|
||||
`internal/tool/risk.go` is the policy (Vikunja #449). The tier is DERIVED from
|
||||
the row, not stored, so it can be argued with in one place instead of being
|
||||
whatever the last person to enable the tool believed.
|
||||
|
||||
| Tier | What it is | What it costs |
|
||||
|---|---|---|
|
||||
| `safe` | a read, or a change he can undo by saying the opposite | runs on first hearing |
|
||||
| `destructive` | it changes something real and undoing it takes work | one confirm turn, every time |
|
||||
| `irreversible` | the thing does not come back: a wipe, a format, a delete with no bin | voice may not authorise it at all |
|
||||
|
||||
Three rules fall out, and they are the part that was missing:
|
||||
|
||||
- **Which acts are destructive is not only the checkbox.** A house row always
|
||||
is, because there is no read-only way to turn the heating off. A row whose
|
||||
argv names one of the irreversible verbs always is, whatever the row says.
|
||||
- **A confirmed act never stays confirmed.** At any tier. A confirmation binds
|
||||
one capability, one target and one argument list, and it dies with the parked
|
||||
turn (90s). "The same act again" is a new act. A sticky confirm is a standing
|
||||
grant and nothing on the voice path may hold one.
|
||||
- **A new domain inherits `destructive`, not `safe`.** A dispatch shape the
|
||||
policy does not recognise gets the confirm turn. A domain argues its way down
|
||||
to running freely; it never has to argue its way up to being gated.
|
||||
|
||||
#### Capability ids
|
||||
|
||||
A row is also read as a dotted capability id, `scope.domain.action` — the same
|
||||
shape Hexis has always spoken, which made the local surface the odd one out
|
||||
(Vikunja #452). `homelab.docker.restart`, `house.lock.unlock`,
|
||||
`mcp_vikunja.vikunja.delete_task`.
|
||||
|
||||
Derived, not stored, for the reason the tier is: a derivation is one place to
|
||||
argue with. The name is still the primary key and nothing about lookup or
|
||||
execution changed — this is a way to READ the allowlist, not a second one.
|
||||
`/tools` groups the enabled rows by `scope.domain` and prints the id and the
|
||||
tier beside each, because a flat list stops answering "what can she do to the
|
||||
house" somewhere around fifteen rows.
|
||||
|
||||
`MatchCapability` widens one way: `house` and `house.lock` both cover
|
||||
`house.lock.unlock`, and nothing lets a narrower id claim a wider pattern.
|
||||
|
||||
The irreversible tier is refused rather than asked about, because a confirm
|
||||
turn would be theatre: everything that proposed the act — an STT guess, a
|
||||
router guess, a fuzzy allowlist match — is a guess, and a spoken "да" checks
|
||||
none of it. She names the gap and he runs it himself. The row stays enabled;
|
||||
refusing to run it from voice is not the same as taking it off the allowlist.
|
||||
|
||||
---
|
||||
|
||||
## Voice pipeline (STT / TTS)
|
||||
@@ -677,6 +763,31 @@ add a new principle; it applied the existing one at smaller and smaller scope.
|
||||
|
||||
---
|
||||
|
||||
## A list is the fourth shape
|
||||
|
||||
Facts, notes and tasks were the three append-only shapes. `list_items` is the
|
||||
fourth (Vikunja #453): an item, a status, and a list tag.
|
||||
|
||||
It is not a task. Milk is not work, nothing prioritises it, and the ranker must
|
||||
not start counting groceries as outstanding errands. It is not a fact either,
|
||||
because it claims nothing about the world. What it is, is a set that grows and
|
||||
shrinks.
|
||||
|
||||
The property that makes the separate table worth it: no predicate reads a list.
|
||||
Nothing ranks it, nothing nudges about it, the digestion worker ignores it. So
|
||||
two people adding to the same list at once cost nothing — there is no order to
|
||||
disagree about and no lifecycle past crossed-off.
|
||||
|
||||
The unique index is the tasks one, per list, and live rows only. Saying "молоко"
|
||||
twice before the shop is one line; saying it again next week, after the last one
|
||||
was crossed off, is a new line.
|
||||
|
||||
Spoken, it is four turns: add, read back, cross one item off, cross the lot off.
|
||||
All four are matched deterministically in `internal/router/list.go` and all four
|
||||
run at stage 0, because an add and a read-back are cheap and should not depend on
|
||||
the resident model having a good turn. Crossing one item off claims the turn only
|
||||
when the list holds that item, which is what keeps "купил новый ноутбук" a note.
|
||||
|
||||
## Calendar
|
||||
|
||||
Integration with **Radicale** (self-hosted CalDAV), not Nextcloud. Scope is
|
||||
|
||||
@@ -90,6 +90,13 @@ export LD_LIBRARY_PATH="$ROOT/deps/piper"
|
||||
|
||||
Without `-piper` it runs as a stub.
|
||||
|
||||
`-lexicon deploy/tts-lexicon.json` adds the pronunciation dictionary: a flat
|
||||
JSON object of name to Russian spelling, applied to the text just before piper
|
||||
reads it. It is how `Vikunja` is said as a word rather than spelled out, and how
|
||||
`SearXNG` and `homesrv` are said at all. Off unless the flag is set; a path that
|
||||
is set and unreadable stops mavttsd rather than letting it say names wrong in
|
||||
silence. Adding a name needs a restart of mavttsd and nothing else.
|
||||
|
||||
## mavweb — PWA voice bridge (WebSocket ↔ TCP)
|
||||
|
||||
No CGo, no deps; builds with stock Go.
|
||||
|
||||
Reference in New Issue
Block a user