fix(coding-agent): kept todo active for prewalk subagents and fixed prewalk gate deadlock

- Updated prewalk gating in `AgentSession` to key the todo gate on active tools instead of registry presence, so deactivated todo tools no longer block prewalk handoff.
- Changed subprocess tool filtering so `todo` is stripped for normal subagents but retained when prewalk is armed, and propagated the prewalk state through tool-session setup.
- Added regression tests for restricted active-tool slates and prewalk/non-prewalk subagent tool propagation to verify todo is handled correctly in each case.
This commit is contained in:
can1357
2026-07-15 05:45:59 +02:00
parent b31eccbc21
commit c32ea55ba3
9 changed files with 106 additions and 16 deletions
@@ -1750,7 +1750,7 @@ export class AgentSession {
#prewalkPlanInjected = false;
/** True once any successful `todo` call landed — opens the prewalk
* trigger gate: the switch fires at the first edit/write AFTER the todo
* list exists (sessions without a todo tool skip the gate). */
* list exists (sessions without an ACTIVE todo tool skip the gate). */
#prewalkTodoSeen = false;
#planYolo: PlanYolo | undefined;
#planYoloPreviousTools: string[] | undefined;
@@ -2270,12 +2270,14 @@ export class AgentSession {
// todo list from it and start" — so the switch waits until a todo list
// exists AND the model has actually started implementing (first
// edit/write). The todo call itself never triggers: firing there handed
// the fast model the whole implementation cold. Sessions without a todo
// tool skip the gate.
// the fast model the whole implementation cold. The gate keys on the
// ACTIVE tool set, not the registry: a registered-but-deactivated todo
// (e.g. a restricted active-tool slate) is uncallable and would
// deadlock the switch.
if (context.toolResults.some(result => result.toolName === "todo")) {
this.#prewalkTodoSeen = true;
}
const todoGateOpen = this.#prewalkTodoSeen || !this.#toolRegistry.has("todo");
const todoGateOpen = this.#prewalkTodoSeen || !this.getActiveToolNames().includes("todo");
const action = todoGateOpen
? context.toolResults.find(result => PREWALK_ACTION_TOOLS[result.toolName])
: undefined;
+6 -3
View File
@@ -2575,9 +2575,12 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
});
}
// Todos are parent-owned bookkeeping and stripped from subagents —
// except under prewalk, whose plan nudge + todo gate require the
// subagent to commit its own todo list before the hand-off.
const isParentOwnedTool = (name: string): boolean => !prewalk && name === "todo";
const subagentToolNames = session.getActiveToolNames();
const parentOwnedToolNames = new Set(["todo"]);
const filteredSubagentTools = subagentToolNames.filter(name => !parentOwnedToolNames.has(name));
const filteredSubagentTools = subagentToolNames.filter(name => !isParentOwnedTool(name));
if (filteredSubagentTools.length !== subagentToolNames.length) {
await awaitAbortable(session.setActiveToolsByName(filteredSubagentTools));
}
@@ -2635,7 +2638,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
getActiveTools: () => session.getActiveToolNames(),
getAllTools: () => session.getAllToolNames(),
setActiveTools: (toolNames: string[]) =>
session.setActiveToolsByName(toolNames.filter(name => !parentOwnedToolNames.has(name))),
session.setActiveToolsByName(toolNames.filter(name => !isParentOwnedTool(name))),
getCommands: () => getSessionSlashCommands(session),
setModel: model => runExtensionSetModel(session, model),
getThinkingLevel: () => session.thinkingLevel,
-1
View File
@@ -206,7 +206,6 @@ function renderDescription(
agents: renderedAgents,
spawningDisabled,
defaultAgent: spawnPolicy.defaultAgent,
allowedAgentsText: spawnPolicy.allowedPromptText,
isolationEnabled,
batchEnabled,
asyncEnabled,
+5 -1
View File
@@ -205,6 +205,9 @@ export interface ToolSession {
outputSchema?: unknown;
/** Whether to include the yield tool by default */
requireYieldTool?: boolean;
/** Session starts with a prewalk hand-off armed. Keeps `todo` in yield-gated
* (subagent) registries: the prewalk plan nudge + todo gate need it. */
prewalkArmed?: boolean;
/** Task recursion depth (0 = top-level, 1 = first child, etc.) */
taskDepth?: number;
/** Get shared eval executor session ID. Subagents inherit this to share JS/Python/Ruby/Julia state. */
@@ -612,7 +615,8 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
if (name === "launch") return session.settings.get("launch.enabled");
if (name === "eval") return allowEval;
if (name === "debug") return session.settings.get("debug.enabled");
if (name === "todo") return !includeYield && session.settings.get("todo.enabled");
if (name === "todo")
return (!includeYield || session.prewalkArmed === true) && session.settings.get("todo.enabled");
if (name === "glob") return session.settings.get("glob.enabled");
if (name === "grep") return session.settings.get("grep.enabled");
if (name === "github") return session.settings.get("github.enabled");