Files
Maven/docs/plans/24-no-deadline-on-the-turn-path.md
T
claude 661b5c1099 the audit write-ups, so every agent starts with them (V-638)
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>
2026-08-06 22:41:03 +04:00

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.