Skip to content

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

Merged
hannesrudolph merged 4 commits intomainfrom
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 intomainfrom
feature/task-history-cross-instance-safety

Conversation

@roomote
Copy link
Contributor

@roomote roomote bot commented Feb 16, 2026

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
@roomote
Copy link
Contributor Author

roomote bot commented Feb 16, 2026

Rooviewer Clock   See 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.

…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".
@RooCodeInc RooCodeInc deleted a comment from roomote bot 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
@dosubot dosubot bot added size:XXL This PR changes 1000+ lines, ignoring generated files. Enhancement New feature or request labels Feb 19, 2026
@dosubot dosubot bot added the lgtm This PR has been approved by a maintainer label Feb 19, 2026
@hannesrudolph hannesrudolph merged commit b598efb into main Feb 19, 2026
26 checks passed
@hannesrudolph hannesrudolph deleted the feature/task-history-cross-instance-safety branch February 19, 2026 07:01
@github-project-automation github-project-automation bot moved this from New to Done in Roo Code Roadmap Feb 19, 2026
@roomote roomote bot mentioned this pull request Feb 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Enhancement New feature or request lgtm This PR has been approved by a maintainer size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments