Skip to content

[Story 4.2c] File-level write serialization #376

@edelauna

Description

@edelauna

Part of #359 (Epic 4: Parallel Tool Execution).

Depends on: Story 4.2b (Core parallel dispatch). Must land before PARALLEL_TOOL_EXECUTION is promoted from false to true as default.

Context

Story 4.2b dispatches write_to_file, apply_diff, and insert_code_block as fully parallel — safe for writes to different files, but a filesystem race if two tools target the same path in one response. This story adds per-path serialization using VS Code's SequencerByKey pattern.

This story must land before PARALLEL_TOOL_EXECUTION is promoted from false to true as a default.

Developer Notes

File-level serialization in src/core/assistant-message/presentAssistantMessage.ts:

  • Move write_to_file, apply_diff, insert_code_block from the fully-parallelizable bucket into a "parallelizable with file-level serialization" bucket.

  • Implement a keyed sequencer using normalizePath(path.resolve(cline.cwd, toolArgs.path)) from src/utils/path.ts for key normalization. For case-insensitive comparison on the map key, use the existing arePathsEqual() pattern (applies .toLowerCase() on Windows, case-sensitive on POSIX):

    const fileWriteQueues = new Map<string, Promise<void>>()
    
    function enqueueWrite(filePath: string, fn: () => Promise<void>): Promise<void> {
      const prev = fileWriteQueues.get(filePath) ?? Promise.resolve()
      const next = prev.then(fn)
      fileWriteQueues.set(filePath, next.catch(() => {})) // prevent queue stall on error
      return next
    }

    This is VS Code's SequencerByKey pattern. The fileWriteQueues map is local to each presentAssistantMessageParallel call (not shared across turns).

Logging: Add structured logs at dispatch start and end with mode (parallel/serial), tool counts, and duration. Follow the existing outputChannel.appendLine() pattern used elsewhere in the file.

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

Tests (extend src/core/assistant-message/__tests__/presentAssistantMessageParallel.spec.ts):

File-level serialization:

  • Two write_to_file to same path → second starts only after first completes (controlled promises).
  • Two write_to_file to different paths → both start concurrently.
  • One write_to_file + one apply_diff to same path → serialized. Different paths → concurrent.
  • Write error doesn't stall the queue for the same path on subsequent turns (.catch(() => {}) guard).

Acceptance Criteria

  • Two write tools targeting the same file path never execute concurrently.
  • Two write tools targeting different paths execute concurrently.
  • Queue stall guard: a write error does not permanently block subsequent writes to the same path.
  • Structured logs emitted at dispatch start and end.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions