jul31 session: five small fixes plus the voice.go/ipc decomposition #50
Closed
kami
wants to merge 0 commits from
integration/small-batch into master
pull from: integration/small-batch
merge into: kami:master
kami:master
kami:task/725-capability-ledger-and-empirical-baseline
kami:task/692-heads-path-may-equal-model-path-and-noth
kami:task/694-staticcheck-and-deadcode-are-still-not-i
kami:task/682-go-1-25-5-and-x-text-0-14-0-carry-20-rea
kami:task/674-caveats
kami:task/673-mavgpud-serves-the-model-to-the-whole-la
kami:task/487-capture-device-doc
kami:task/487-capture-device
kami:task/487-wake-word-deploy
kami:task/487-wake-word-threshold
kami:task/487-wake-word-stage-two
kami:task/671-mavwaked-registers-as-a-voice-consumer-i
kami:task/670-cut-claude-md-to-200-lines
kami:task/515-deploy-mavwaked-workpc
kami:task/669-prune-claude-md
kami:task/668-e4b-phrasing
kami:task/668-title-capital
kami:task/668-kiwix-answers-a-question-it-cannot-answe
kami:task/666-only-a-stage-0-grammar-may-take-the-pers
kami:task/487-mavwaked-has-no-wake-word-only-an-energy
kami:task/486-deploy-the-workstation-transcriber
kami:task/486-move-stt-and-tts-to-the-workstation-wher
kami:task/665-crisperwhisper-2-russian
kami:task/664-routing-heads-in-go
kami:task/662-usage-harness-source-badge
kami:task/661-post-merge-usage-rerun
kami:task/661-routing-heads-step-3-train-the-multi-hea
kami:task/660-router-prompt-destination
kami:task/659-destination-fixture
kami:task/655-query-source-is-a-routing-decision-made
kami:task/654-a-pending-clarify-has-no-way-out-neither
kami:task/654-week-of-usage-eval-docs
kami:task/649-needs-kami-telegram-is-the-only-reach-an
kami:task/643-memorystore-search-decodes-and-unmarshal
kami:task/641-two-maps-grow-for-the-process-lifetime-w
kami:task/644-mavcaldav-is-built-documented-as-running
kami:task/642-the-store-caps-sqlite-at-one-connection
kami:task/647-factenrichmentworker-walks-the-pending-q
kami:task/646-v-637-follow-up-telegram-intake-has-no-d
kami:task/638-no-deadline-survives-the-turn-path-from
kami:task/637-inbound-telegram-turns-and-corrections-f
kami:task/636-correcting-a-turn-from-telegram-and-from
kami:task/634-an-act-alias-resolves-the-verb-but-not-t
kami:task/630-one-gesture-correction-on-chat-v-628
kami:task/629-persist-the-routing-trace-and-record-it
kami:task/631-mode-inventory-written-from-the-handlers
kami:task/586-defaultfactparser-uses-hand-written-russ
kami:task/633-reconcile-the-seed-labels-with-the-handl
kami:task/627-reminder-verbs-has-no-alarm-verb-so-an-a
kami:task/626-the-classifier-seeds-teach-an-older-inte
kami:task/546-route-with-a-fine-tuned-e5-small-instead
kami:task/586-measure-the-fact-parser
kami:fix/gofmt-ecosystem-acts
kami:task/584-media-store-a-failed-write-leaks-its-bud
kami:task/518-no-write-path-for-a-backdated-event-so-t
kami:task/287-qa-voice-session-quality-polish
kami:task/492-qa-plan-reconcile
kami:task/530-sweep-tail-four-files-the-russian-sweep
kami:task/405-score-how-often-a-real-utterance-reaches
kami:task/529-money-and-list-pick-a-mechanism
kami:task/528-sweep-tail-the-three-files-on-467
kami:task/527-embedder-open-set-phrasings-stop-being-r
kami:task/526-morphology-a-dictionary-answers-the-gram
kami:task/525-lexicons-the-finite-russian-sets-move-to
kami:task/524-entity-reference-ask-nexus-about-every-l
kami:task/523-risk-tiers-take-hexis-s-tier-for-a-hexis
kami:task/521-review-pr-111-query-strings-declension-h
kami:task/491-llama-server-core-dumps-on-every-sigterm
kami:task/479-bug-an-unconfigured-capability-does-not
kami:task/467-bug-spoken-task-capture-is-dead-the-rout
kami:task/463-deploy-mavwaked-and-mavenclient-run-nowh
kami:task/480-hearing-no-shipped-client-can-start-a-re
kami:task/432-ambient-calendar-intake-is-fragile-and-p
kami:task/431-board-surface-maven-holds-the-work-board
kami:task/433-reactivehandler-has-30-fields-and-is-pas
kami:task/371-swap-the-embedder-for-an-asymmetric-retr
kami:task/408-review-31-07-split-the-30-method-coreapi
kami:task/410-review-31-07-hand-rolled-string-enums-st
kami:task/423-review-pr50-split-internal-ipc-server-go
kami:task/422-review-pr50-split-cmd-mavend-tick-go-860
kami:task/409-review-31-07-finish-moving-mavweb-markup
kami:task/482-ambient-ingest-reads-a-notification-s-ti
kami:task/444-kuma-a-fact-per-monitor-so-she-can-name
kami:task/452-capability-model-homelab-docker-restart
kami:task/449-destructive-confirm-policy-risk-tiers-no
kami:task/453-grocery-list-items-table-fourth-append-o
kami:task/399-run-the-persona-checks-inside-the-daemon
kami:task/448-bounded-follow-up-state-pending-candidat
kami:task/455-conversation-repair-name-the-misroute-co
kami:task/454-go-mod-tidy
kami:task/458-pronunciation-dictionary-for-piper
kami:task/456-command-history-read-only-query-over-exi
kami:task/457-clarification-templates-for-the-router-s
kami:task/474-query-source-ordering-feeds-and-calendar
kami:task/469-reminders-spelled-out-times-fail-the-bod
kami:task/475-bug-the-praxis-attention-capability-is-u
kami:task/481-bug-a-transient-complaint-is-stored-as-a
kami:task/476-bug-the-router-transliterates-latin-enti
kami:task/385-decide-whether-a-parked-clarify-question
kami:task/377-backfill-routines
kami:task/421-weather-geocoder
kami:task/390-no-read-path-for-delivery-attempts
kami:task/386-recall-fixture-filler-note-ids
kami:task/473-bug-morning-item-has-no-required-flag
kami:task/465-bug-make-simulate-routes-with-an-empty
kami:task/467-bug-spoken-task-capture-is-dead
kami:task/466-bug-a-pending-clarify-is-global-so-one-u
kami:task/468-bug-pattern-detect-has-no-minimum-interv
kami:task/462-bug-checkfeminine-flags-second-person-ma
kami:task/443-safekey-drops-cyrillic-so-russian-calend
kami:task/471-bug-agendaquerygrammars-covers-today-but
kami:task/383-slottext-in-clarify-answer-would-clobber
kami:task/323-qa-phraser-coverage-is-65-3-but-the-llam
kami:task/498-bug-and-x-reach-the-model-with-no-determ
kami:task/506-strings-family-6-summaries-and-reports-i
kami:task/504-strings-family-4-act-and-smart-home-repl
kami:task/503-strings-family-3-query-answers-and-gaps
kami:task/502-strings-family-2-capture-acknowledgement
kami:task/501-strings-family-1-phrasing-fallbacks-into
kami:task/397-phrasechat-and-phrasequery-hide-model-fa
kami:task/396-the-reply-path-can-t-be-tested-llmreplie
kami:task/496-recall-a-cross-language-question-loses-i
kami:task/495-bug-x-escapes-the-personal-boundary-and
kami:task/499-llama-server-holds-7-9gb-rss-for-a-1-1gb
kami:task/470-bug-a-question-writes-invented-knowledge
kami:task/493-bug-the-memory-index-stores-the-raw-utte
kami:task/490-name-the-gap-world-questions-through-the
kami:task/485-run-the-big-model-on-the-workstation-wit
kami:task/489-workstation-deploy-mavgpud-on-workpc-and
kami:task/488-workstation-a-supervisor-that-keeps-llam
kami:task/483-docs-offload-design
kami:task/483-design-offload-ml-to-the-workstation-kee
kami:task/459-docs-refresh-the-qa-plan-against-the-liv
kami:task/446-doc-reorg-tier-the-tree-retire-the-three
kami:fix/367-voice-parks-routine-accept
kami:task/365-dialogue-slots-and-router-slots-are-hand
kami:task/364-snooze-does-nothing-at-runtime-the-gate
kami:task/447-retire-progress-md-the-backlog-and-the-f
kami:task/445-session-workflow
kami:overnight/eco-versioned-traces
kami:overnight/eco-entity-refs
kami:overnight/eco-degraded-suite
kami:overnight/netscan
kami:overnight/smarthome
kami:overnight/replay-simulator
kami:overnight/event-envelope
kami:overnight/coldstart-unlock
kami:overnight/voice-barge-in
kami:overnight/stt-golden-audio
kami:overnight/senses-speaker
kami:overnight/senses-hearing
kami:overnight/senses-media-vision
kami:overnight/mcp-tools
kami:overnight/mcp-client
kami:overnight/self-update
kami:overnight/model-swap
kami:overnight/web-crawler
kami:overnight/rss-feeds
kami:overnight/email-poller
kami:overnight/email-extract
kami:overnight/email-imap
kami:overnight/money-zenmoney
kami:overnight/task-priority
kami:overnight/task-capture
kami:overnight/behavior-profile
kami:overnight/day-plan
kami:overnight/ambient-calendar
kami:overnight/local-calendar
kami:overnight/memory-eval
kami:overnight/proactive-proposals
kami:overnight/split-voice-quiet
kami:overnight/nginx-maven-block
kami:overnight/stepup-chat-surface
kami:docs/fix-drift
kami:fix/ru-wording
kami:integration/jul31
kami:overnight/resident-1.7b
kami:overnight/nudge-templates
kami:overnight/kiwix-rewrite
kami:overnight/eval-writeup
kami:overnight/fix-truncation
kami:overnight/kiwix-client
kami:overnight/ru-prompts
kami:overnight/external-data
kami:overnight/phrasing-grammar
kami:overnight/talk-eval
kami:overnight/prompt-context
kami:overnight/prompt-address
kami:overnight/eval-label-kill
kami:overnight/delivery-boundary
kami:overnight/address-check
kami:overnight/system-replies-pr
kami:overnight/clock-intent-pr
kami:overnight/embedder-backfill-pr
kami:overnight/embedder-marker-pr
kami:overnight/note-recall-pr
kami:overnight/thinking-off-pr
kami:overnight/dialogue-persist-pr
kami:overnight/persona-2p-pr
kami:overnight/clarify-expiry-pr
kami:overnight/clarify-rework
kami:overnight/phrasing
kami:overnight/bakeoff
kami:overnight/recall-margin
kami:overnight/router-on
kami:overnight/slot-extract
kami:overnight/embedder-e5
kami:overnight/router-refusal
kami:overnight/eval-rerun
kami:overnight/eval-harnesses
kami:overnight/eval-rerun-base
kami:overnight/fmt-gate
kami:overnight/routines-fire
kami:overnight/router-prompt
kami:overnight/away-leak
kami:overnight/recall-eval
kami:overnight/snooze-works
kami:overnight/clarify-wiring
kami:overnight/delivery-tests
kami:overnight/routine-accept
kami:overnight/llm-router-flag
kami:overnight/clarify-data-layer
kami:overnight/loop-rule-tests
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "integration/small-batch"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
One branch, two kinds of change: five self-contained fixes off the board, and a
mechanical decomposition of the two files that had grown past reading.
Fixes
deploy/ecosystem/nginx.confboundlisten 80, allinterfaces, while the live host is LAN/WireGuard-scoped. Now matches
cmd/mavweb/nginx.confverbatim: explicit10.42.0.1+192.168.1.104listens,allow/deny all. Deploy was unaffected either way — nginx holds the LAN port andproxies to loopback.
-addrdefaults to127.0.0.1:9200instead of:9200.POST /api/chatis the one mutating handler with no session check, so anaccidental all-interfaces default was the wrong side to fail on. compose passes
-addr :9201explicitly, so deploy behaviour is unchanged.Confidence: 1.0was hardcoded, and the LLM branch never consulted
r.thresholdat all, so a correctlow confidence would have been discarded anyway.
gateLLMDecisionnow feeds threestructural signals into the same stage-3 gate the classifier path already used.
Prompt untouched, so
check_prompt_parity.pystill passes. Re-measured on the77-case fixture: missed clarify 6/6 -> 1, costing 3 false clarifies and 2.6pt of
full accuracy. Two of the three false clarifies are acts the model mis-routed and the
gate caught — asking beats wrongly executing. The third,
поужинал, is a realdefect: the single-token rule is an English intuition and does not transfer to
Russian. Noted on the task, not fixed here.
over
DistinctEventPairs. Dedupe and dismissal were already free at the store layer(
UNIQUE(action, object)+ON CONFLICT DO NOTHING, dismissal flips status in place).digest_entriestable (migration 13).Suppressed sev2 nudges are queued and resurfaced instead of dropped; sev1 still drops,
high severity never digests. Only
quiet_hours/calendar_busy/presencequalify —cooldown and snooze do not. 24h expiry, max 3 spoken items.
Decomposition
cmd/mavend/voice.go1923 -> 491, in six move-only slices:What remains in voice.go is the handler and nothing else:
reactiveHandler,HandlePushToTalk,handleText,applyAction,detectPattern,resolveQuietToggle,replySystem,chatHistory,reply.Every slice was verified move-only by diffing each non-blank removed line against the
new file and requiring zero unmatched, plus zero lines added to voice.go — not by
trusting the diff stat. As a whole-refactor check, all 40 top-level funcs in
master:voice.gowere enumerated and confirmed still present somewhere incmd/mavend/. One is intentionally absent:jsonStringImpl, a one-line passthroughcollapsed into
jsonString.Two structural changes beyond pure moves:
applyAction's 300-line intent switch is now amap[router.Intent]func(...)table.All 7 intents present. The destructive-act confirm gate and the enabled-tool
allowlist turned out not to be cross-cutting — they only ever fire inside
IntentAct— so they stayed inside
actionAct, andclarify.go's invariant holds becausefinishClarifiedreaches the same handler through the same table.internal/ipc/server.go's 42-arm dispatcher is table-driven, net -65 lines.Accounting checked: 30 CoreAPI methods -> 30 table entries; 33
Methodconstants =30 + the 3 that bypass CoreAPI (
AssertStepUp,StoreEncryptionKey,Unlock).s.Checkstill runs before the table lookup, so locked mode is unchanged.wire.go/client.go/api.godiffs are empty.The confirm/park gate is security-relevant, so it got a stronger check than move-only:
classifyConfirmandresolveConfirmbodies diff byte-identical against master.confirmTTLis still 90s and the verdict ordering is unchanged at all three switchsites.
Also: dead
lockedAPI(~90 lines) deleted —srv.Checkis default-deny in locked mode,so it was unreachable. Replaced with a new
ipc.UnimplementedCoreAPI, which also letthe test doubles drop 55 stub methods. And
mavwaked, an 11 MB build artifact trackedin
master, is deleted and gitignored (.gitignorehad 7 of the 8 binaries).make build: 8 binaries.gofmtsilent,go vetclean,make test: 38 packages, noFAIL, no RACE.
🤖 Generated with Claude Code
https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
Confidence was hardcoded to 1.0 for every LLM decision, and the LLM branch in Router.Route returned straight from fillSlots without ever touching the stage-3 threshold gate — so the LLM path could not produce a Clarify no matter what confidence a model reported. That is why all 6 want_clarify cases in the 77-case RU fixture were missed by every model in the bake-off. Fix reads structural signal instead of changing the (parity-locked) router prompt: a single-token utterance ("вода", "бэкап") is flagged thin evidence in llmrouter.go; a fact left keyless or an act that never resolves to an allowlisted fn, checked after fillSlots so the deterministic parsers get first crack, is flagged in router.go's new gateLLMDecision. Anything below config.DefaultRouterThreshold (0.55) now sets Clarify=true through the same path the classifier already uses. Added unit tests with a stubbed Completer proving both directions: thin cases clarify, clean multi-word/resolved-slot cases stay confident. The 77-case fixture re-run against a live llama-server is still needed to confirm the 6/6 moves — not done here, no llama-server on this box. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ@@ -0,0 +59,4 @@router.IntentQuery: (*reactiveHandler).actionQuery,}func (h *reactiveHandler) actionFact(ctx context.Context, dec router.Decision) string {seems like each action should have it's own component in the mavend/actions/ dir.
@@ -0,0 +13,4 @@// confirm. The confirmation is bound to the resolved capability + canonical// target entity so a later "да" can only execute exactly what was proposed// (ecosystem invariant: protected actions require bound confirmation).type pendingHexisExec struct {can't we move the types in one .go file and reference them in other .go file with functions?
@@ -0,0 +67,4 @@// so a routine confirm doesn't get eaten by a stale tool pending).pr := h.pendingRoutineif pr != nil && !h.now().After(pr.expiry) {switch classifyConfirm(text) {feels like this is the same pattern all over again.
we can also make separate .go files with the concrete implementations and just call them in this layer (higher one) - instead of repeating the same switch-case over and over again.
@@ -0,0 +24,4 @@// Map verbs and Russian aliases to Praxis tool calls.// Each case: if the verb matches, call the tool and return a user-facing reply.switch fn {this too feels like an unnecessary switch-case.
@@ -366,3 +276,2 @@} else {// locked mode: dummy CoreAPI that returns errLocked for everythingcoreAPI = &lockedAPI{}// locked mode: no real store yet, so there's no meaningful CoreAPI todo we have the task for the store implementation?
@@ -345,0 +530,4 @@log.Printf("tick: drain digest entries: %v", err)}}this file also feels too long.
@@ -0,0 +21,4 @@// extractWeatherLocation parses a location from the utterance, or falls back// to the configured default. Very basic: just checks for known city names.func extractWeatherLocation(u, defaultLoc string) string {feels fucking useless, because it should take the temperature for only current location specified by the user.
@@ -501,6 +501,239 @@ func (s *Server) safeDispatch(ctx context.Context, req Request) (result json.Rawreturn s.dispatch(ctx, req)}this one is also too long.
is there nothing like putting routes in separate files and referencing them (including) in another file in Go?
@@ -0,0 +11,4 @@// Digest entry statuses. pending = enqueued, waiting for a drain. drained =// spoken as part of a bundle. expired = the tick loop's expiry sweep found it// past its expires_ts before a drain happened — dropped, not delivered late.const (there're no enums in Go? again.
keep in mind there's mavwaked binary in this PR - might need to be removed.
Already handled — that's what commit
7d676ebdoes.mavwakedis tracked inmaster(11 MB blob) and this PR deletes it; the diff stat showsmavwaked | Bin 11505880 -> 0 bytes. It is not present at the PR head.Root cause:
.gitignorelisted 7 of the 8 built binaries and was simply missing/mavwaked, so it got committed at some point. The line is added in the same commit, somake buildnow leaves the tree clean instead of re-staging it.Verified just now:
git cat-file -e HEAD:mavwakedfails (not tracked),git check-ignoreresolves it to.gitignore:9, andgit statusis clean after a fullmake build.Replies to the nine line comments.
actions.go— one component per action inmavend/actions/. A subdirectory means a new package, and that is not free here. Every handler is a method onreactiveHandler, which has 26 unexported fields (stt,tools,phraser,memStore,pending, …) and handlers touch most of them. Moving them out means either exporting all 26 or inventing an interface to pass through — real plumbing, and it would not be a move-only change. Go's unit of encapsulation is the package, not the file, soactions_fact.go/actions_query.go/ … insidemainbuys the same navigability at zero cost. Happy to do that split if 362 lines is still too long; the package extraction I would rather do deliberately, not bolted onto this PR.confirm.go— types in one file, functions in another. Yes, Go allows that freely: any file in a package sees every type in it. No objection.confirm.go— 'the same pattern all over again'. You are right and this is the real one.resolveConfirmhas three near-identical blocks —pendingRoutine,pendingHexis,pending— each doing expiry check →classifyConfirm→ yes/no/unknown → clear the slot. Three copies of one shape. That collapses to a single generic pending-confirm helper. I deliberately did not touch it in this PR because the confirm gate is the destructive-act path and I wanted it byte-identical to master;classifyConfirmandresolveConfirmdiff identical right now, which is the whole reason this slice is safe to merge. Filed separately.ecosystem_acts.go— unnecessary switch-case. Agreed, and it is the same fixapplyActionjust got:switch fnover capability names + aliases becomes a map. Filed.main.go— do we have a task for the store implementation? Yes, Vikunja #14 (cold-start unlock: the-wrapped-key-fileseam exists, the passkey→L3 half does not). Note this PR deleteslockedAPIas dead code —srv.Checkis already default-deny in locked mode with a 2-method allowlist, so it was unreachable. I have updated #14 so it no longer points at a type that stopped existing.tick.goalso too long. 860 lines, correct. Largest aretick(106),flushDigest(65),maybeDrainDigest(59),fireAcceptedRoutines(53). It splits cleanly along digest / routines / morning. Filed. It grew partly because #43 and #281 in this very PR both appended to it.weatherq.go— 'feels fucking useless'. Agreed, and it is worse than useless.extractWeatherLocationcarries a hardcoded 6-city lookup table and, when nothing matches and no default is configured, returns "Moscow" — a made-up answer presented as fact. The configuredvoice.weather.default_locationshould be the only source, with no reply at all when it is unset. Filed. This PR only moved the function; I did not want a behaviour change hiding inside a move-only slice.internal/ipc/server.gotoo long — can Go include routes from other files? Yes, exactly like the split this PR does to voice.go: same package, more files, no imports or indirection needed. The 882 lines here are two unrelated things —storeAPI, the sqlite-backedCoreAPIimplementation (~400 lines of thin passthroughs), and the dispatcher table. Those should bestoreapi.goandserver.go. Filed.digest.go— no enums in Go? again. Correct, Go has no enum type. The idiom is a defined type plus constants, whichconfirm.goin this PR already does (type confirmVerdict int,confirmYes/No/Unknown).digest.gouses bare untyped strings, so nothing stops"drained"being passed where a status is expected. Should betype DigestStatus string. Filed.Read the whole batch. The decomposition is faithful.
voice.goshrinks by 1300 lines, and the moved blocks (voicewire.go,actions_*.go,confirm.go,patterns.go) match what they replace. Theipcmethod table andUnimplementedCoreAPIbeat the 42-arm switch and the hand-writtenlockedAPI. A new CoreAPI method now fails loudly instead of going silently unguarded.store/meta.gotreating a marker-less DB that already holds vectors as a mismatch is the right call, and the reason is written down.Three things worth changing, none blocking.
1. A reminder missing both slots dies instead of asking again.
cmd/mavend/clarify.go:31declaresIntentReminder: {SlotText, SlotTime}. ButaskClarifyparksMissing: []dialogue.Slot{slot}, only the first gap. Say "напомни" with no subject and no time. She asks "О чём напомнить?". He answers "позвонить маме".StillMissingover the one-elementMissingis now empty, soresolveClarifyAnswerrebuilds the decision and hands it toapplyAction. That has no time and replies "не получилось разобрать время напоминания." The comment says she asks about one thing on purpose, which is right for one turn. The second gap should still re-enter the clarify loop rather than fall out as an error. Re-park with the remainingwantedSlotsafter a successful fill.2. The expiry notice is dropped on a confirm turn. In
runTurn, step 1 returnsresolveConfirm's reply directly, andexpiredNoticeis only computed at step 2. So she asks a question, he walks off, the question expires, he comes back and says "да" to a still-parked confirm. The confirm answers and he never hears that the older request was let go. Every other exit inrunTurngoes throughwithNotice. Move theclarifyExpiredNotice()call above the confirm check and wrap that return.3.
clarifyExpiredVariantsis prose the eval never sees. Five hand-written lines, with feminine self-reference ("ждала", "отпустила", "не стала ждать") and a plain imperative. None of it runs throughCheckAddressorCheckFeminine. It reads correct to me. It is also exactly the kind of string someone later edits reaching for a synonym. A table test asserting the checks pass overclarifyExpiredVariantsandclarifyGaveUpcosts ten lines.Non-blocking notes:
internal/store/backfill.goembeds every note and vector inside one write transaction. Correct for crash safety, and it only runs under-reembedbefore serving, so the lock hold cannot block a live daemon. Worth a line in the flag help saying the daemon does not answer until it finishes.internal/persona/persona.gois the right shape. One block, five prompts, rules in code rather than config.can()listing only configured capabilities is the detail that makes it honest.Landed on master. The stack was one linear chain, so #84 carried every commit from #50 up, and master now contains this branch in full. Merging this PR on its own is an empty diff, so it is closed rather than merged. The review findings for it were fixed in the 2026-08-01 pass and are on master as commits on the stack tip, not on this branch.
Pull request closed