Replies: 3 comments 1 reply
|
The report said a subagent tool call has no step or time bound. Here is a What happenedA benchmark drove a parent agent under a web-like composition. One task never The child's session log: Eight tokens, cycling ~700 times each. The child was emitting the literal Why the existing guard does not catch itWe run a Why this is worse in a child than in a parent
What would have been enoughAny one of these turns an outage into a logged failure:
(1) looks smallest: Our mitigation, and why it is not a fixWe now strip delegation tiers from that benchmark's composition, and bound each |
|
The report said a subagent tool call has no step or time bound. Here is a What happenedA benchmark drove a parent agent under a web-like composition. One task never The child's session log: Eight tokens, cycling ~700 times each. The child was emitting the literal Why the existing guard does not catch itWe run a Why this is worse in a child than in a parent
|
| bound | occurrences |
|---|---|
maxTokens (per request) |
present, and set by us |
maxSteps |
0 |
maxTurns |
0 |
maxIterations |
0 |
timeoutMs |
0 |
deadline |
0 |
So the gap is not a missing token cap. It is that nothing bounds the TURN.
What would have been enough
Any one of these turns an outage into a logged failure:
- A step or turn bound on the child — the layer
maxTokensdoes not reach.
maxDepthalready exists ontool-subagentand caps the delegation tree;
amaxStepsbeside it would need no new surface and would read naturally. - A wall-clock deadline per
tool-subagentcall. - A repetition stop inside the child's stream — n identical chunks in a row is
cheap to detect and unambiguous at 700 repeats.
(1) looks smallest, and (3) is the only one that would also catch a child
looping slowly enough to stay under a deadline.
Our mitigation, and why it is not a fix
We now strip delegation tiers from that benchmark's composition, and bound each
task externally. That stops the benchmark lying; it does nothing for a real
session, where a child can still run until a human notices.
|
Mattafaak, the point-4 decode-loop is a genuinely distinct failure mode, and I think (3) is the highest-value bound for exactly the reason you state — a tool-call repeat guard is the wrong layer because a degenerate child never makes a tool call. It degenerates inside a single assistant message, and a per-request Since I had the session log format already open, I put (3) into practice as a diagnostic rather than a fix: a degenerate-stream detector that scans the At The one thing that stands out from your fix list: a diagnostic is not a stop. The audit flags the loop after the fact; it cannot bound the turn. That is the actual gap, and it lives in the harness, not in a plugin — For anyone hitting this today, the audit at least turns an opaque "child never finished" into a named reason ("decode loop, N identical chunks"), which is the first step to catching it. |
Uh oh!
There was an error while loading. Please reload this page.
Four small ones from the same deployment (0.1.2-alpha.5, local model via
llama.cpp). Happy to split any of them out.
1. An in-flight compaction cannot be stopped, and leaves no signal that it is running
Automatic compaction runs inside the turn and takes minutes on a local model
(measured here: 131 s to generate a 2,876-token summary, then 77 s to
re-prefill). During that window the UI's stop does not cancel it, and the
process exiting does not either — the work is simply lost.
Worse for anything supervising the process: there is no signal that a
compaction is in flight. The session log is silent for its whole duration (no
tool/call, nostep/*), the projection cache does not move, and there is nochild process. A restart in that window is indistinguishable from a restart
while idle — until you notice the turn is gone.
Suggestion: let the turn's cancel signal reach it, and emit something
observable while it runs (a
compaction/progress, or simply keepcompaction/startvisible in the projection cache).2. The crash-repair closer borrows the previous event's timestamp
interruptedTurnClosers()synthesises the closing events for a turn that wasopen when the log was reopened and — per its own comment — reuses the timestamp
of the last real event:
{"type":"turn/end","data":{"turn":1,"reason":{"kind":"interrupted"}}} ts = <last real event>That is indistinguishable from a user stop at that moment. It cost me a wrong
conclusion twice: a turn killed at 19:57 appeared to have ended at 19:54:56,
which was simply the last thing it had done.
Suggestion: stamp the repair time and mark it synthetic (
"synthesized": true).3. A first prompt over the context window is sent anyway, with
max_tokensclampedA first user message larger than the model's context window is sent with
max_tokensclamped to the remainder and dies with a provider400. Compactioncannot help — there is nothing yet to compact — so the useful behaviours are to
refuse with a clear message, or to split.
Measured: a 126.5k-token prompt against a 131,072 window returned
400 status code (no body). Five sessions died this way before I set a margin inthe route's declared
contextWindow(126,976 under a 131,072 lane), which turnsit into dsh's own limit and produces a comprehensible error.
4. Nothing bounds a child turn
tool-subagenthasmaxDepth(which caps tree depth) but nothing caps achild's own turn: no step limit, no wall clock. A child that loops does so until
it exhausts the context.
I added a
PreToolUseguard that denies repeated identical calls, which makes aloop cheap but not finite — a deny reaches the model as an error and the turn
continues. In one recorded case a child emitted 1,255 calls after its guard
started denying them.
Suggestion: a
maxStepsor a deadline onagentOptions.All reactions