661b5c1099
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>
92 lines
4.4 KiB
Markdown
92 lines
4.4 KiB
Markdown
# 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.
|