Skip to content

Commit 9af7e1e

Browse files
committed
docs(tui): state and pin the bounded ask_director stall policy
1 parent 6208ba9 commit 9af7e1e

2 files changed

Lines changed: 55 additions & 4 deletions

File tree

‎src/tui/stall-watchdog.test.ts‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,53 @@ describe("shouldAbortForStall — execution-watchdog-exempt tools do not pin for
182182
).toBe(true);
183183
});
184184

185+
// ask_director is bounded, not exempt (CL-10206): the primary never mounts
186+
// it, so the name is a guard, and a stray mount must notice, then abort,
187+
// like wait_agents. Both shapes the turn state can hold are pinned: the
188+
// announced call, and a leftover tracked only per-id after a sibling
189+
// cleared currentToolName and the shared callIdByName slot.
190+
test.each([
191+
{
192+
representation: "direct",
193+
currentToolName: "ask_director",
194+
activeToolCalls: ["ask-1"],
195+
callIdByName: { ask_director: "ask-1" },
196+
callNameById: { "ask-1": "ask_director" },
197+
},
198+
{
199+
representation: "mapping-only",
200+
currentToolName: null,
201+
activeToolCalls: ["ask-1"],
202+
callIdByName: {},
203+
callNameById: { "ask-1": "ask_director" },
204+
},
205+
])(
206+
"in-flight ask_director ($representation) is quiet, then notices, then aborts, and every surface agrees",
207+
(ask) => {
208+
const inFlightAsk = {
209+
...collect,
210+
...ask,
211+
streamingType: "tool" as const,
212+
stallNoticeMs: STALL_NOTICE_MS,
213+
repeating: false,
214+
};
215+
const points = [
216+
{ nowMs: STALL_NOTICE_MS - 1, level: "quiet" },
217+
{ nowMs: STALL_NOTICE_MS, level: "notice" },
218+
{ nowMs: STALL_TIMEOUT_MS - 1, level: "notice" },
219+
{ nowMs: STALL_TIMEOUT_MS, level: "abort" },
220+
] as const;
221+
222+
for (const { nowMs, level } of points) {
223+
const args = { ...inFlightAsk, nowMs };
224+
expect(stallLevel(args)).toBe(level);
225+
expect(shouldAbortForStall(args)).toBe(level === "abort");
226+
expect(shouldNoticeStall(args)).toBe(level === "notice");
227+
expect(isStalledForDisplay(args)).toBe(level !== "quiet");
228+
}
229+
},
230+
);
231+
185232
// tool.done of a sibling bash clears currentToolName and streamingType
186233
// while wait_agents is still in activeToolCalls. Keying only the last
187234
// name would leave that poll unbounded forever.

‎src/tui/stall-watchdog.ts‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,13 @@ function silentPastThreshold(
4949
// sub-agent's tool call finishes, even while siblings are still running.
5050
// Outstanding calls mean the run is not silent, regardless of that flag.
5151
if (args.awaitingResponse && args.activeToolCalls.length === 0) return true;
52-
// Execution-watchdog-exempt polls (collect, wait_agents, ask_director) emit
53-
// no parent stream events. That must not pin the stall clock forever: they
54-
// have no other wall-clock bound, so the stall budget is the backstop.
55-
// TUI primary does not mount wait_agents; the name is kept so a stray mount cannot pin the clock.
52+
// Policy: an in-flight wait_agents or ask_director is stall-bounded, never
53+
// exempt. The TUI primary mounts neither (wait_agents is mount-gated off and
54+
// ask_director exists only in worker loops), so both names are defensive
55+
// guards that keep a stray mount from pinning the clock. A worker's parked
56+
// ask is bounded elsewhere: an unanswered ask expires after ASK_DEADLINE_MS
57+
// via expireStaleAsks, and the director's silent ask-wake turn is aborted by
58+
// this watchdog via abortStalledWakeTurn, which re-surfaces the question.
5659
if (isStallBoundedInFlightTool(args) && args.isProcessing) return true;
5760
// Mid-stream hang: model stream stalled after first token. Ordinary in-flight
5861
// tool runs still do not emit parent stream events; leave those to the
@@ -79,6 +82,7 @@ function isStallBoundedToolName(name: string | null | undefined): boolean {
7982
return isSameTool(name, "wait_agents") || isSameTool(name, "ask_director");
8083
}
8184

85+
/** Why these names are bounded guards, not exemptions: see `silentPastThreshold`. */
8286
function isStallBoundedInFlightTool(args: ShouldAbortForStallArgs): boolean {
8387
if (isStallBoundedToolName(args.currentToolName)) return true;
8488
for (const [name, id] of Object.entries(args.callIdByName)) {

0 commit comments

Comments
 (0)