The watchdog is blind while the model generates a tool call — Ollama sends ZERO chunks for that entire phase, so Keepalive cannot fire and productive turns are killed #279
Loading…
Reference in a new issue
No description provided.
Delete branch "claude/wi-019fb44f-the-watchdog-is-blind-while-the-model-ge"
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?
Anima work item
019fb44f-27ca-7292-ac94-3892ba166eb9.MEASURED ROOT CAUSE (2026-07-30) — supersedes the framing below
Ollama sends no chunks at all while the model generates tool-call arguments.
Not "chunks that carry no visible delta" — no chunks. Measured against
https://ollama.com/api/chat,glm-5.2,stream:true, onewrite_filetool,prompt asking for ~600 words in the
contentargument:92% of that generation was total provider silence. The model was working the
whole time.
Why this breaks everything
akh-medu's idle watchdog resets on emitted messages.
#276(merged302fb347,deployed as
61c532f3a9ae) addedStreamEvent::Keepaliveto reset it on anychunk received — but during tool-call generation there is no chunk to
receive. The keepalive is a no-op for precisely the case that causes the stall.
#276remains a genuine improvement for the silent-fallback path; it does notfix this.
Scale it: 600 words of tool argument = 6.9s of silence. A real code edit is
thousands of tokens of
content, and a coding agent's most substantial work isalways inside a tool call. The bigger and more useful the edit, the more likely
the turn is killed. That is why every stall logged
last_step="narrative"—narrative is simply the last thing emitted before the model starts producing the
tool call.
This is the mechanism behind the 2026-07-28→29 run: 22 tecton sessions, ~19h,
zero credited work, while the turns were in fact producing complete answers
(8157 and 5898 characters — see Anima WI-430).
Required change
The watchdog must not depend on the provider emitting anything.
flight, the akh should emit its own liveness on a timer — it knows a request is
outstanding. Chunk arrival is a provider property and cannot be relied on.
"no request outstanding and nothing emitted for N seconds" are different faults
and want different timeouts. Today one number conflates them, and the
legitimate long-tool-call case gets the strictest reading.
a result, that result must still be delivered and scored — today it is
concatenated onto a stall error and reported
outcome=empty.#276's Keepalive against this measurement: add a test that a turngenerating a large tool call survives, since the current one cannot pass.
Acceptance
idle_secsgenerating a single large tool callcompletes normally.
asserting the turn is not killed.
empty.Original filing (2026-07-30, superseded above but the eliminations still hold)
The turn succeeded and then hung — session
019fb40c-b448-7170-be64-df5e059a0e2c, workspacetecton:Causes eliminated by measurement (do not re-litigate)
akh-thothreturned 3611 chunks, max inter-chunk gap 2s.
through the silence.
Arc<Mutex<Agent>>and kept ticking. (The minute the turn actually worked,17:23, has zero daemon ticks and 17:24 shows a catch-up burst of 9 — real
contention, but not this hang.)
now explained: no chunks arrive during tool-call generation.
One candidate was checked and cleared: the
Arc<ChannelSink>cloned into theToolExecutionTracker(mod.rs:754) is torn down correctly bySessionToolGuard::drop(mod.rs:909).Related: Anima WI-430 (refusal/empty mislabelling), Anima WI-436 (deliverables
never recorded as Documents).
REVIEW ROUND 1 — CHANGES REQUESTED (2026-07-30, PR #278 @
76b72dfb)Keep this diff. It is not wrong — it is incomplete, because the root cause was
superseded ~20 minutes after that session was dispatched.
What PR #278 gets right (do not revert)
signal_completion()from the worker, aDrainEventselect in place of relyingon every
ChannelSinksender clone being dropped, draining remaining messagesafter completion, and a
drain_loop_terminates_promptly_when_worker_finishestest. That closes the "worker finished, drain loop waits until idle_secs" path
structurally, which is exactly what the original brief asked for.
Why it cannot fix the item as now scoped
The dominant failure is not a turn hanging after it finishes — it is a turn
killed while the model is still working. See the measurement in the section
above: 6.9 of 7.5 seconds of a tool-call generation were total provider
silence, zero chunks, then one chunk with the whole
tool_callspayload.Nothing in
src/acp/mod.rscan observe a tool call being generated. A changeconfined to that file therefore cannot prevent this failure, no matter how
correct it is.
Required to close
timer for as long as an LLM request is outstanding, in the
agentic_chat.rs/ollama.rslayer. The akh knows it has a request open;chunk arrival is a provider property we do not control and must not depend on.
request outstanding and nothing emitted for N seconds" are different faults.
One
idle_secsapplies the strictest reading to the legitimate long-tool-callcase.
result must still deliver and score that result. 8157 characters of assistant
output is not
outcome=empty.argument and assert the turn survives past
idle_secs. The#276Keepalivetest cannot cover this — it assumes chunks keep arriving.
Verdict entered by hand on the primary DB: the operator PAT lacks
workitem.submit_review, sosubmit_review_verdictwas not callable. Theci_greencompletion condition was reset to unmet, since the rejected diff is nolonger certified work — that is what returns the item to the implementer queue.
WIP: The watchdog is blind while the model generates a tool call — Ollama sends ZERO chunks for that entire phase, so Keepalive cannot fire and productive turns are killedto The watchdog is blind while the model generates a tool call — Ollama sends ZERO chunks for that entire phase, so Keepalive cannot fire and productive turns are killed