Skip to content

Commit ede3f14

Browse files
bwachmanTheGreatAxios
authored andcommitted
docs(tui): state and pin the bounded ask_director stall policy
1 parent 4298f25 commit ede3f14

2 files changed

Lines changed: 56 additions & 4 deletions

File tree

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

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,54 @@ 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
182-
// flight; keying only that name would leave it unbounded forever.
181+
// ask_director is bounded, not exempt (CL-10206): the primary never mounts
182+
// it, so the name is a guard, and a stray mount must notice, then abort,
183+
// like wait_agents. Both shapes the turn state can hold are pinned: the
184+
// announced call, and a leftover tracked only per-id after a sibling
185+
// cleared currentToolName and the shared callIdByName slot.
186+
test.each([
187+
{
188+
representation: "direct",
189+
currentToolName: "ask_director",
190+
activeToolCalls: ["ask-1"],
191+
callIdByName: { ask_director: "ask-1" },
192+
callNameById: { "ask-1": "ask_director" },
193+
},
194+
{
195+
representation: "mapping-only",
196+
currentToolName: null,
197+
activeToolCalls: ["ask-1"],
198+
callIdByName: {},
199+
callNameById: { "ask-1": "ask_director" },
200+
},
201+
])(
202+
"in-flight ask_director ($representation) is quiet, then notices, then aborts",
203+
(ask) => {
204+
const inFlightAsk = {
205+
...collect,
206+
...ask,
207+
streamingType: "tool" as const,
208+
stallNoticeMs: STALL_NOTICE_MS,
209+
repeating: false,
210+
};
211+
const points = [
212+
{ nowMs: STALL_NOTICE_MS - 1, level: "quiet" },
213+
{ nowMs: STALL_NOTICE_MS, level: "notice" },
214+
{ nowMs: STALL_TIMEOUT_MS - 1, level: "notice" },
215+
{ nowMs: STALL_TIMEOUT_MS, level: "abort" },
216+
] as const;
217+
218+
for (const { nowMs, level } of points) {
219+
const args = { ...inFlightAsk, nowMs };
220+
expect(stallLevel(args)).toBe(level);
221+
expect(shouldAbortForStall(args)).toBe(level === "abort");
222+
}
223+
},
224+
);
225+
226+
// A sibling tool.done (e.g. bash) clears currentToolName and streamingType
227+
// while wait_agents is still in activeToolCalls. Keying only the last
228+
// announced name would leave that poll unbounded forever.
183229
test("sibling tool.done while collect is in-flight still aborts at the stall budget", () => {
184230
const afterSiblingDone = {
185231
...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)