Repository navigation
[Story 4.1] Per-call ToolExecutionContext #373
Description
Part of #359 (Epic 4: Parallel Tool Execution).
Depends on: #362 (Story 1.2 — TaskSemaphore), #363 (Story 1.3 — experiment flag).
This is a refactor only — no behavior change when the parallel flag is off.
Context
Four per-turn fields on Task assume serial execution: didRejectTool (line 385), didAlreadyUseTool (line 386), terminalProcess (line 302), and presentAssistantMessageLocked (line 345). If two tools execute concurrently, they race on these shared fields. Specifically, didRejectTool is checked globally at line 2906 and causes ALL subsequent tools in the response to skip — a hard serial dependency. terminalProcess is a single slot; two concurrent execute_command calls would overwrite each other's process handle.
Developer Notes
In src/core/task/Task.ts:
- Introduce
ToolExecutionContext:interface ToolExecutionContext { toolCallId: string didReject: boolean didAlreadyUse: boolean terminalProcess?: RooTerminalProcess }
- Replace
terminalProcess?: RooTerminalProcess(single slot) withterminalProcesses: Map<string, RooTerminalProcess>keyed bytoolCallId. - Add
activeToolContexts: Map<string, ToolExecutionContext>toTask. - Keep the existing
didRejectToolanddidAlreadyUseToolinstance fields for the serial path — the newToolExecutionContextis additive, used only when the parallel flag is on.
In src/core/assistant-message/presentAssistantMessage.ts:
- Thread
toolCallIdthrough to theaskApproval,handleError, andpushToolResultclosures so each tool's rejection state is scoped to its ownToolExecutionContext. - The
hasToolResultflag (lines 451–456) is already scoped per-closure — no change needed.
In src/core/tools/BaseTool.ts:
- Make
toolCallId: stringrequired inToolCallbacks(currently optional). Verify the exact interface name before changing.
In src/core/tools/ExecuteCommandTool.ts:
- Use
task.terminalProcesses.get(toolCallId)/task.terminalProcesses.set(toolCallId, process)rather thantask.terminalProcess.
Files: src/core/task/Task.ts, src/core/assistant-message/presentAssistantMessage.ts, src/core/tools/BaseTool.ts, src/core/tools/ExecuteCommandTool.ts
Tests (extend src/core/tools/__tests__/ExecuteCommandTool.spec.ts + new src/core/task/__tests__/ToolExecutionContext.spec.ts):
- Two
execute_commandcalls with differenttoolCallIds maintain independentterminalProcessreferences. - Rejecting one tool via mock
askApprovalsetsdidRejectonly on its ownToolExecutionContext, not on a sibling's. - All existing tool tests pass unchanged (serial path unaffected).
Acceptance Criteria
- With flag off: zero observable behavior change.
- No shared state mutation across two concurrent tool executions in tests.
grep -n "task\.terminalProcess[^s]" src/core/tools/ExecuteCommandTool.tsreturns zero matches.
Activity
DispatchStatemachine refactor #374