A repo-wide sweep on 06-08-2026 at 06c1cf2. Three docs, three tasks.
docs/plans/24-no-deadline-on-the-turn-path.md (V-638). Nothing between a
mavweb handler and llama-server can be cancelled, and one hop has a timeout.
Replier takes no context, the ipc client sets no conn deadline and checks ctx
once, and the ipc server dispatches under Background. Four commits, and the
pattern to copy is already in internal/voice/client.go:101.
docs/plans/25-the-two-boot-paths.md (V-639). The passkey-unlock path starts
seven workers outside the WaitGroup that shutdown waits on, shadows that
WaitGroup at main.go:529, and builds a daemonAPI with no nexus and no
getMCPServers. Latent, because db_key_env means the box boots unlocked.
docs/evals/2026-08-06-routing-trajectory.md (V-464). The deterministic path
and the cascade now score the same 69/91, and the cascade has not been
re-measured since V-626 and V-627. Either the model still earns its place or
it is costing 1.17s a turn for nothing. Dated, so it is not edited later.
Committed with --no-verify, on the owner's instruction of 06-08-2026. The
pre-commit hook refuses master and the alternative was three PRs for three
markdown files. Markdown is already exempt from the size cap for the same
reason: docs land as one batch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4.4 KiB
No deadline on the turn path
Last verified: 06-08-2026 @ 06c1cf2
V-638. Sibling of V-607, which is the same class of bug in internal/worker.
Reads with docs/offload.md and docs/protocol.md.
What is missing
A chat turn starts in a mavweb HTTP handler and ends at llama-server. Nothing between those two points can be cancelled, and one hop has a timeout.
Four places, all on the same path.
voice.Replier.Reply takes no context (internal/voice/replier.go:41). So llmReplier
calls PhraseReply(context.Background(), d) at cmd/mavend/replier_llm.go:42. The turn
cannot deadline its own reply. The only bound is phraser.timeout, 60s in deploy.
ipc.Client.roundtrip sets no connection deadline (internal/ipc/client.go:202). A daemon
that stops answering parks the caller for as long as the socket stays open.
ipc.Client.call checks the context once, before sending (client.go:149), then blocks in
roundtrip. Cancelling mid-call does nothing.
ipc.Server.serveConn dispatches under context.Background() (internal/ipc/server.go:253).
A client that hangs up does not cancel the turn, and neither does Server.Close.
And every call queues behind the slowest one
ipc.Client serialises on one connection and one mutex. mavweb routes /api/chat and
/api/ptt through the shared client, so one turn blocks all 28 handlers while it runs.
Worst case is a 60s page load.
This is understood for exactly one route already. cmd/mavweb/main.go:57 opens a second
connection for /models, and the comment there says why. A model swap is a multi-minute
call, and sharing the connection would freeze every other page.
The pattern is already in the repo
internal/voice/client.go:101 derives a connection deadline from the caller's context,
falls back to 120s, and clears it with a defer. internal/ipc/client.go never learned it.
Copy that rather than inventing a second convention.
The work
One commit each.
Context on the reply seam. phraser.Replier.PhraseReply already takes a context and the
interface has two implementations, so this is small. Change Reply to take a context, have
StubReplier ignore it, and pass it through llmReplier to PhraseReply. Both call sites
already hold one: cmd/mavend/voice.go:461 and cmd/mavend/clarify.go:574.
Deadlines and cancellation on the client. Pass the context into roundtrip and set
SetDeadline from it. For cancellation mid-call, a watchdog goroutine that calls c.drop()
on ctx.Done() is enough. drop exists, and the retry split already separates a lost write
from a lost read. So a cancelled call lands in errReadLost and is never retried for a
mutation. Check that against internal/ipc/maperr_test.go.
A request context on the server. serveConn should derive from a server-scoped context
so Close cancels a dispatch in flight. Server already carries done and a conn registry
for this class of problem. The registry comment records what the last version of it cost:
eleven days of stale ciphertext.
Stop serialising mavweb. Give /api/chat and /api/ptt their own connection, the way
/models has one. Roughly ten lines, and it changes no shared code.
A connection pool inside ipc.Client is the general form and is deliberately not the first
step. Each connection is already its own request and response stream. So a pool preserves
frame pairing by construction. It still has to keep re-dial on drop, the
errWriteLost and errReadLost split, and Close. Do the narrow fix, measure, and reach
for the pool only if a second module turns out to queue.
How it is judged
make test stays green. It is green at 06c1cf2.
Nothing here changes routing or recall, so make eval-router and make eval-recall are
unchanged rather than re-measured.
By hand: load /dash while a chat turn is in flight. Before the change it waits for the
length of the turn.
There is no test today that a cancelled context aborts an in-flight ipc.Client call. That
absence is why two of these four went unnoticed, so the test is part of the work.
What is not done here
The store is still SetMaxOpenConns(1) (internal/store/store.go:99) under WAL. WAL is
built for concurrent readers against one writer, and the cap makes every read queue.
Store.DB(ctx) hands the digestion worker a read transaction on that same connection. This
plan does not touch it. It is measurable first and should be measured before it is changed.