Skip to content

fix(task-persistence): add cross-instance lock for concurrent task history updates (#920) - #926

Closed
easonLiangWorldedtech wants to merge 5 commits into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:fix/920-concurrent-task-history-cross-instance
Closed

easonLiangWorldedtech wants to merge 5 commits into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:fix/920-concurrent-task-history-cross-instance

Conversation

@easonLiangWorldedtech

@easonLiangWorldedtech easonLiangWorldedtech commented Jul 17, 2026 •

Copy link
Copy Markdown
Contributor

[BUG] Concurrent task history updates from parallel tabs may cause lost entries (#920)

Problem

updateTaskHistory() has serialization within a single ClineProvider instance (see ClineProvider.taskHistory.spec.ts), but when two parallel tabs (different ClineProvider instances) call updateTaskHistory() concurrently, there may be a race condition that causes task history entries to be overwritten or lost.

Root Cause

PR #909 created an independent viewLocalState buffer for each ClineProvider instance, but the serialization logic in updateTaskHistory() only protects concurrent calls within the same instance. If two parallel tabs (different instances) call updateTaskHistory() simultaneously:

  1. Tab A: updateTaskHistory(taskX) → reads current state
  2. Tab B: updateTaskHistory(taskY) → reads same stale state (before Tab A's write)
  3. Tab A: writes taskX to history
  4. Tab B: writes taskY to history — may overwrite Tab A's entry

Fix

Add a global file-level lock (_history.lock) via proper-lockfile in TaskHistoryStore.withLock() so that mutations from different ClineProvider instances (parallel tabs) are serialized, preventing stale-cache overwrites.

Changes

  • Import proper-lockfile and add getGlobalLockFilePath() helper
  • Acquire global lock in withLock() before in-process serialization
  • Add 3 new cross-instance tests: concurrent updates, 5+ concurrent writes, and concurrent update + delete across tabs

Testing Requirements

  1. ✅ New test: "serializes concurrent updateTaskHistory from two parallel instances" — verifies same task updated concurrently produces consistent final state
  2. ✅ New test: "handles 5+ concurrent updates from different tabs without losing entries" — stress test with 12 concurrent writes
  3. ✅ New test: "concurrent updateTaskHistory + deleteTaskFromState across tabs" — mixed operations

Related Issues

Summary by CodeRabbit

  • Bug Fixes

    • Improved task-history consistency by serializing writes, deletions, reconciliation, and delegation-related updates across concurrent windows/processes via a shared storage lock.
    • Added lock-scoped helpers so legacy migration and drift/delegation repairs run atomically with related cache/webview updates.
    • Strengthened stale/compromised lock recovery and delegation reopen abort handling to prevent inconsistent state.
  • Tests

    • Expanded coverage for shared-lock routing across multiple provider instances, cascade deletion, legacy migration rechecks, and delegation reopen abort behavior.
    • Added unit tests for the shared lock implementation and locked mutation helpers.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto reviews are limited based on label configuration.

🏷️ Required labels (at least one) (1)
  • coderabbit-review-active

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository: Zoo-Code-Org/Zoo-Code/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 893e6565-3518-4696-8801-f42a475cf047

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

A shared singleton lock now serializes task-history mutations across provider instances and processes. Store helpers, provider workflows, migration, delegation transitions, and concurrency tests use the shared locking path.

Changes

Task History Locking

Layer / File(s) Summary
Shared lock implementation and unit coverage
src/core/task-persistence/TaskHistoryLock.ts, src/core/task-persistence/__tests__/TaskHistoryLock.spec.ts, src/shared/globalFileNames.ts
Adds the Promise queue, _history.lock handling, singleton, release guarantees, retry configuration, and lock tests.
Lock-scoped store operations
src/core/task-persistence/TaskHistoryStore.ts, src/core/task-persistence/__tests__/TaskHistoryStore.spec.ts
Adds shared-lock wrappers, non-locking core methods, and locked migration, reconciliation, deletion, and atomic-update helpers.
Provider mutation serialization
src/core/webview/ClineProvider.ts, src/__tests__/history-resume-delegation.spec.ts
Protects migration, updates, deletions, cache/webview synchronization, delegation persistence, and paired parent reopening.
Provider lock routing coverage
src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts, src/__tests__/provider-delegation.spec.ts, src/core/webview/__tests__/ClineProvider.sticky-profile.spec.ts
Verifies shared-lock routing across providers and task-history mutation scenarios.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested labels: awaiting-review

Suggested reviewers: navedmerchant

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Title is concise and accurately summarizes the cross-instance task-history lock fix.
Description check ✅ Passed Description covers the bug, root cause, fix, and testing, and it links issue #920.
Linked Issues check ✅ Passed The code adds shared locking and tests for cross-instance updates and delete paths, matching #920.
Out of Scope Changes check ✅ Passed No clearly unrelated changes stand out; the added locking and tests support the task-history concurrency fix.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 17, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.95238% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/core/task-persistence/TaskHistoryLock.ts 80.95% 3 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/core/task-persistence/TaskHistoryLock.ts`:
- Around line 14-28: The TaskHistoryLock implementation currently serializes
only callers within one process. In src/core/task-persistence/TaskHistoryLock.ts
lines 14-28, update TaskHistoryLock.withLock to acquire and release an exclusive
lock on the shared _history.lock file instead of using the module-local Promise
queue, while preserving sequential callback execution and error propagation. In
src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts lines 832-874, add
coverage that uses independent processes sharing the same storage and lock file
to verify cross-process exclusion.

In `@src/core/webview/ClineProvider.ts`:
- Around line 2096-2101: The normal task deletion path in ClineProvider must use
the same cross-instance taskHistoryLock as the fallback helper. Wrap
deleteTaskWithId’s taskHistoryStore.delete operation, including cache
invalidation and state posting, in taskHistoryLock.withLock, and ensure
deleteMany and other direct task-history store mutations use this centralized
locked boundary. Update
src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts lines 882-918 to
invoke deleteTaskWithId or otherwise verify the centralized mutation boundary.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3a1bb978-5396-4cdd-9ca6-b32b5e73cb03

📥 Commits

Reviewing files that changed from the base of the PR and between 367013f and ba61ec4.

📒 Files selected for processing (4)
  • src/core/task-persistence/TaskHistoryLock.ts
  • src/core/task-persistence/__tests__/TaskHistoryLock.spec.ts
  • src/core/webview/ClineProvider.ts
  • src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts

Comment thread src/core/task-persistence/TaskHistoryLock.ts
Comment thread src/core/webview/ClineProvider.ts Outdated
@easonLiangWorldedtech
easonLiangWorldedtech force-pushed the fix/920-concurrent-task-history-cross-instance branch from ba61ec4 to ac5c2e0 Compare July 17, 2026 17:43

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/core/task-persistence/TaskHistoryLock.ts`:
- Around line 75-83: Update the retries configuration in TaskHistoryLock so its
cumulative backoff window exceeds the 31-second stale threshold, allowing
stale-lock recovery before acquisition fails. Add a test covering a crashed lock
owner that exits without releasing the lock and verifies a subsequent writer
eventually acquires it.

In `@src/core/webview/ClineProvider.ts`:
- Around line 389-391: Update the migration flow around
taskHistoryStore.migrateFromGlobalState so the shared-lock callback reads
alreadyMigrated and legacy history, re-checks whether migration is needed,
performs migration only when necessary, and records the completion marker before
releasing the lock. Remove the pre-lock decision and state reads to prevent
stale migrations from being queued.
- Around line 2055-2057: Move the childIds traversal and allIdsToDelete
derivation inside the taskHistoryLock.withLock callback in ClineProvider, using
the locked persisted snapshot before calling deleteMany so concurrent child
additions are included. In src/core/webview/ClineProvider.ts lines 2055-2057,
update the deletion flow accordingly; in
src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts lines 876-908, add
a concurrent update-and-cascade-delete test rather than relying on the existing
normal-path test that passes false.
- Around line 3842-3866: Revalidate the parent-child relationship inside the
parent callback passed to atomicUpdatePair: require parent.awaitingChildId to
equal childTaskId and verify the parent has an allowed status before modifying
it. Apply this guard before clearing delegation fields or setting completion
metadata, preserving the existing child update and valid transition behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e5236e42-57e1-42eb-8f70-813716b85e20

📥 Commits

Reviewing files that changed from the base of the PR and between ba61ec4 and ac5c2e0.

📒 Files selected for processing (5)
  • src/core/task-persistence/TaskHistoryLock.ts
  • src/core/task-persistence/__tests__/TaskHistoryLock.spec.ts
  • src/core/webview/ClineProvider.ts
  • src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts
  • src/shared/globalFileNames.ts

Comment thread src/core/task-persistence/TaskHistoryLock.ts
Comment thread src/core/webview/ClineProvider.ts Outdated
Comment thread src/core/webview/ClineProvider.ts Outdated
Comment thread src/core/webview/ClineProvider.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/core/task-persistence/__tests__/TaskHistoryStore.spec.ts`:
- Around line 250-259: Replace the Promise.race timeout check in the
mutateLocked test with a direct await of the store.mutateLocked operation,
preserving its reconcileLocked, assertion, and deleteManyLocked steps. Remove
the 100 ms “deadlocked” fallback and rely on the test runner’s standard timeout.

In `@src/core/task-persistence/TaskHistoryStore.ts`:
- Line 264: Update the wrapper methods around deleteManyCore and the other
referenced entry points to acquire the shared storage/file lock before invoking
the instance-local withLock queue. Preserve each method’s existing core
operation and ensure all listed mutation paths serialize across different
TaskHistoryStore instances.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e1e734fc-ba88-4649-9e0c-2e1e8147e72c

📥 Commits

Reviewing files that changed from the base of the PR and between ac5c2e0 and 62b00b2.

📒 Files selected for processing (7)
  • src/__tests__/history-resume-delegation.spec.ts
  • src/core/task-persistence/TaskHistoryLock.ts
  • src/core/task-persistence/TaskHistoryStore.ts
  • src/core/task-persistence/__tests__/TaskHistoryLock.spec.ts
  • src/core/task-persistence/__tests__/TaskHistoryStore.spec.ts
  • src/core/webview/ClineProvider.ts
  • src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/core/task-persistence/TaskHistoryLock.ts
  • src/core/webview/tests/ClineProvider.taskHistory.spec.ts
  • src/core/webview/ClineProvider.ts

Comment thread src/core/task-persistence/__tests__/TaskHistoryStore.spec.ts Outdated
Comment thread src/core/task-persistence/TaskHistoryStore.ts Outdated
@easonLiangWorldedtech
easonLiangWorldedtech force-pushed the fix/920-concurrent-task-history-cross-instance branch from 62b00b2 to 0d004f7 Compare July 17, 2026 19:22

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/core/task-persistence/TaskHistoryStore.ts`:
- Around line 471-473: The mutateLocked method in
src/core/task-persistence/TaskHistoryStore.ts lines 471-473 must use only the
instance-local queue and avoid reacquiring taskHistoryLock. Update the test at
src/core/task-persistence/__tests__/TaskHistoryStore.spec.ts lines 284-292 to
remove the shared-lock acquisition expectation and verify mutateLocked succeeds
when called beneath an externally held lock.
- Around line 795-800: Update withSharedStorageLock and the read-modify-write
path using atomicReadAndUpdate to synchronize affected task records from disk
after acquiring the shared storage lock and before applying updates. Ensure each
instance refreshes its cached state within the lock scope so it cannot overwrite
newer changes, while preserving the existing queue and lock behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: dbadf991-4006-42e7-9b66-1ce17cb9681e

📥 Commits

Reviewing files that changed from the base of the PR and between 62b00b2 and 0d004f7.

📒 Files selected for processing (8)
  • src/__tests__/history-resume-delegation.spec.ts
  • src/__tests__/provider-delegation.spec.ts
  • src/core/task-persistence/TaskHistoryLock.ts
  • src/core/task-persistence/TaskHistoryStore.ts
  • src/core/task-persistence/__tests__/TaskHistoryLock.spec.ts
  • src/core/task-persistence/__tests__/TaskHistoryStore.spec.ts
  • src/core/webview/ClineProvider.ts
  • src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/core/task-persistence/TaskHistoryLock.ts
  • src/core/task-persistence/tests/TaskHistoryLock.spec.ts
  • src/core/webview/tests/ClineProvider.taskHistory.spec.ts
  • src/tests/history-resume-delegation.spec.ts
  • src/core/webview/ClineProvider.ts

Comment thread src/core/task-persistence/TaskHistoryStore.ts Outdated
Comment thread src/core/task-persistence/TaskHistoryStore.ts Outdated
@easonLiangWorldedtech

Copy link
Copy Markdown
Contributor Author

@coderabbitai resume

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Reviews resumed.

@github-actions github-actions Bot added has-conflicts PR has merge conflicts with the base branch and removed has-conflicts PR has merge conflicts with the base branch labels Sep 8, 2026
@github-actions github-actions Bot removed the has-conflicts PR has merge conflicts with the base branch label Sep 15, 2026
# Conflicts:
#	src/__tests__/provider-delegation.spec.ts
#	src/core/task-persistence/TaskHistoryStore.ts
#	src/core/webview/ClineProvider.ts
#	src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts
#	src/shared/globalFileNames.ts
@easonLiangWorldedtech

Copy link
Copy Markdown
Contributor Author

Closing as redundant.

After merging latest upstream/main (2026-09-27), this PR's effective diff reduced to the TaskHistoryLock class and its spec (194 added lines, 0 modifications) — the original #920 fix (shared-lock wrapping of store mutations) was superseded by main's per-task-file design, which is already merged:

  • per-task history_item.json files with proper-lockfile inside safeWriteJson (cross-process write serialization at the file level)
  • field-level delta merge so a peer instance's fields are preserved instead of stomped
  • disk-based status transition validation at merge time (DeltaRejectedError) with cache re-sync on rejection
  • fs watcher (debounced) + 5-minute periodic reconciliation for cross-instance cache freshness

Issue #920 is closed, and the remaining TaskHistoryLock class has zero production consumers — merging it would add dead code. Both open CodeRabbit findings were against the superseded locking revision and are addressed by the design above (see the thread replies).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Concurrent task history updates from parallel tabs may cause lost entries

2 participants