Skip to content

Commit 71790d4

Browse files
committed
docs(tui): state and pin the bounded ask_director stall policy
1 parent a86b77c commit 71790d4

2 files changed

Lines changed: 59 additions & 3 deletions

File tree

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

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,58 @@ describe("shouldAbortForStall — execution-watchdog-exempt tools do not pin for
178178
).toBe(true);
179179
});
180180

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

‎src/tui/stall-watchdog.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,13 @@ function silentPastThreshold(
4141
// A fan-out sets `awaitingResponse` when any sibling finishes; outstanding
4242
// calls mean the run is not silent regardless of that flag.
4343
if (args.awaitingResponse && args.activeToolCalls.length === 0) return true;
44-
// Watchdog-exempt polls (collect, wait_agents, ask_director) emit no parent
45-
// events; with no other wall-clock bound, the stall budget is their backstop.
44+
// Policy: an in-flight wait_agents or ask_director is stall-bounded, never
45+
// exempt. The TUI primary mounts neither (wait_agents is mount-gated off and
46+
// ask_director exists only in worker loops), so both names are defensive
47+
// guards that keep a stray mount from pinning the clock. A worker's parked
48+
// ask is bounded elsewhere: an unanswered ask expires after ASK_DEADLINE_MS
49+
// via expireStaleAsks, and the director's silent ask-wake turn is aborted by
50+
// this watchdog via abortStalledWakeTurn, which re-surfaces the question.
4651
if (isStallBoundedInFlightTool(args) && args.isProcessing) return true;
4752
// Mid-stream hang after the first token; tool runs have their own watchdog.
4853
return (
@@ -62,6 +67,7 @@ function isStallBoundedToolName(name: string | null | undefined): boolean {
6267
return isSameTool(name, "wait_agents") || isSameTool(name, "ask_director");
6368
}
6469

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

0 commit comments

Comments
 (0)