Skip to content

[Story 4.1] Per-call ToolExecutionContext #373

Description

@edelauna

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) with terminalProcesses: Map<string, RooTerminalProcess> keyed by toolCallId.
  • Add activeToolContexts: Map<string, ToolExecutionContext> to Task.
  • Keep the existing didRejectTool and didAlreadyUseTool instance fields for the serial path — the new ToolExecutionContext is additive, used only when the parallel flag is on.

In src/core/assistant-message/presentAssistantMessage.ts:

  • Thread toolCallId through to the askApproval, handleError, and pushToolResult closures so each tool's rejection state is scoped to its own ToolExecutionContext.
  • The hasToolResult flag (lines 451–456) is already scoped per-closure — no change needed.

In src/core/tools/BaseTool.ts:

  • Make toolCallId: string required in ToolCallbacks (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 than task.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_command calls with different toolCallIds maintain independent terminalProcess references.
  • Rejecting one tool via mock askApproval sets didReject only on its own ToolExecutionContext, 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.ts returns zero matches.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions