Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

feat: add per-task file-based history store for cross-instance safety - #11490

Merged
hannesrudolph merged 4 commits into
mainfrom
feature/task-history-cross-instance-safety
Feb 19, 2026
Merged

feat: add per-task file-based history store for cross-instance safety#11490
hannesrudolph merged 4 commits into
mainfrom
feature/task-history-cross-instance-safety

Conversation

@ghost

Copy link
Copy Markdown

When Roo Code runs in two VS Code windows simultaneously, both instances share a single taskHistory array in globalState. Every write replaces the entire array, so concurrent read-modify-write cycles cause silent data loss.

Solution

This PR introduces TaskHistoryStore, a service that stores each task's HistoryItem as an individual JSON file in its existing task directory (globalStorage/tasks/<taskId>/history_item.json). Cross-process safety comes from safeWriteJson's proper-lockfile on per-task file writes.

Key changes

  • New TaskHistoryStore class (src/core/task-persistence/TaskHistoryStore.ts)

    • Per-task file writes via safeWriteJson (source of truth)
    • Index file (_index.json) for fast startup reads
    • Reconciliation logic to detect and fix drift between instances
    • fs.watch for cross-instance reactivity
    • Debounced index writes (2s window) for streaming performance
    • In-process write lock (replaces withTaskHistoryLock from ClineProvider)
  • Migration from globalState

    • On first startup, backfills history_item.json from existing globalState("taskHistory")
    • Migration is idempotent and safe to re-run
  • Backward compatibility

    • Write-through to globalState("taskHistory") during transition period
    • Fallback lookups from globalState when store does not have an item
    • Allows safe downgrade to older versions
  • ClineProvider integration

    • updateTaskHistory(), deleteTaskFromState(), deleteTaskWithId(), getTaskWithId(), getRecentTasks(), getStateToPostToWebview(), handleModeSwitch(), persistStickyProviderProfileToCurrentTask() all use the store
    • Removed taskHistoryWriteLock and withTaskHistoryLock() (lock moved into store)

Test coverage

  • 23 unit tests for TaskHistoryStore (upsert, delete, reconcile, migration, invalidation, serialization)
  • 5 cross-instance tests (two stores on same path, reconciliation detection, delete detection)
  • All 46 existing ClineProvider tests pass (taskHistory, sticky-mode, sticky-profile)

View task on Roo Code Cloud

Implement TaskHistoryStore service that stores each task's HistoryItem
as an individual JSON file in its existing task directory. This prevents
silent data loss when multiple VS Code windows write to the shared
globalState taskHistory array concurrently.
Key changes:
- New TaskHistoryStore class with per-task file writes via safeWriteJson
- Index file (_index.json) for fast startup reads
- Reconciliation logic to detect and fix drift between instances
- fs.watch for cross-instance reactivity
- Debounced index writes (2s window) for streaming performance
- Migration from globalState on first startup
- Write-through to globalState during transition period
- Fallback lookups from globalState for backward compatibility
Files created:
- src/core/task-persistence/TaskHistoryStore.ts
- src/core/task-persistence/__tests__/TaskHistoryStore.spec.ts
- src/core/task-persistence/__tests__/TaskHistoryStore.crossInstance.spec.ts
Files modified:
- src/shared/globalFileNames.ts (added historyItem, historyIndex)
- src/core/task-persistence/index.ts (export TaskHistoryStore)
- src/core/webview/ClineProvider.ts (integrate store, remove write lock)
- Test files updated for new store-based approach
@ghost

ghost commented Feb 16, 2026

Copy link
Copy Markdown
Author

RooviewerClockSee task

All 3 concurrency issues from earlier reviews remain addressed. Commit 8941c61 debounces the globalState write-through (5s window) instead of writing on every mutation -- the debounce pattern is sound and tests are updated accordingly. No new issues found.

  • reconcile() mutates the cache without acquiring withLock, allowing races with upsert()/delete() at async boundaries (e.g., a just-deleted entry can be re-added by a concurrent reconcile)
  • taskHistoryStoreInitialized flag is set but never checked -- store consumers (getStateToPostToWebview, getRecentTasks, etc.) can read an empty cache before initialize() completes, causing a briefly empty task history on startup
  • The globalState write-through in updateTaskHistory is no longer serialized -- concurrent calls can write stale snapshots to globalState, which matters for the backward-compatibility downgrade path
Previous reviews

Mention @roomote in a comment to request specific changes to this pull request or fix all unresolved issues.

Comment threadsrc/core/task-persistence/TaskHistoryStore.ts
Comment threadsrc/core/webview/ClineProvider.ts
Comment threadsrc/core/webview/ClineProvider.ts Outdated
…rough serialization
- reconcile() now runs through withLock() to prevent interleaving with
upsert/delete at async boundaries
- Added initialized promise so callers can await store readiness before
reading (getStateToPostToWebview now awaits it)
- Write-through to globalState now happens inside the store lock via
onWrite callback, preventing concurrent call races on the transition
period fallback
- Removed separate updateGlobalState("taskHistory") calls from
ClineProvider since the onWrite callback handles it serialized
…e.spec.ts
The test mocks task-persistence with an explicit factory that was
missing the new TaskHistoryStore export, causing all 9 tests to fail
with "No TaskHistoryStore export is defined on the mock".
@RooCodeIncRooCodeInc deleted a comment Feb 16, 2026
…n every mutation
Instead of writing the entire HistoryItem[] array to globalState on
every upsert/delete (expensive with 5000+ tasks), the write-through
is now debounced with a 5-second window. Per-task file writes remain
immediate (~200 bytes each). The globalState is flushed on dispose
to ensure no data loss on shutdown.
This makes the hot path during streaming (token count updates) write
only the per-task file, not the full array.
@hannesrudolph
hannesrudolph marked this pull request as ready for review February 19, 2026 06:50
@dosubotdosubotBot added size:XXL This PR changes 1000+ lines, ignoring generated files. Enhancement New feature or request labels Feb 19, 2026
@dosubotdosubotBot added the lgtm This PR has been approved by a maintainer label Feb 19, 2026
@hannesrudolph
hannesrudolph merged commit b598efb into mainFeb 19, 2026
26 checks passed
@hannesrudolph
hannesrudolph deleted the feature/task-history-cross-instance-safety branch February 19, 2026 07:01
@ghostghost mentioned this pull request Feb 19, 2026
@github-actionsgithub-actionsBot mentioned this pull request Feb 19, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

EnhancementNew feature or requestlgtmThis PR has been approved by a maintainersize:XXLThis PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@hannesrudolph@roomote