Skip to content

fix(task): recover stale delegated children after restart - #1210

Open
edelauna wants to merge 4 commits into
mainfrom
issue/1100
Open

fix(task): recover stale delegated children after restart#1210
edelauna wants to merge 4 commits into
mainfrom
issue/1100

Conversation

@edelauna

@edelauna edelauna commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Related GitHub Issue

Closes: #1100

Description

After an unclean VS Code or extension shutdown, a delegated child task can remain persisted as active even though no live child task exists. When the parent task is resumed, a subsequent new_task delegation is rejected because the stale child is treated as an active live child, causing the parent to loop on “Continue”.

This PR updates startup delegation reconciliation to treat persisted active awaited children as orphaned crash state. During recovery, the child is marked interrupted, the parent is restored to active, and the live delegation pointers are cleared so the parent can delegate again. Existing parent/child lineage and historical child IDs are retained. The runtime guard remains unchanged, so a genuinely active child in a live session is still protected from silent detachment.

Test Procedure

  • cd src && npx vitest run core/task-persistence/__tests__/TaskHistoryStore.reconciliation.spec.ts
  • cd src && npx vitest run __tests__/provider-delegation.spec.ts __tests__/removeClineFromStack-delegation.spec.ts
  • pnpm --dir src exec eslint --prune-suppressions --max-warnings=0 core/task-persistence/TaskHistoryStore.ts core/task-persistence/__tests__/TaskHistoryStore.reconciliation.spec.ts
  • pnpm check-types
  • git diff --check

The reconciliation regression coverage verifies in-memory and persisted recovery state, chained delegation behavior, and idempotence. Live delegation tests continue to verify that genuinely active children are rejected.

Pre-Submission Checklist

  • Issue Linked: This PR is linked to an approved GitHub Issue (see "Related GitHub Issue" above).
  • Scope: My changes are focused on the linked issue (one major feature/fix per PR).
  • Self-Review: I have performed a thorough self-review of my code.
  • Testing: New and/or updated tests have been added to cover my changes (if applicable).
  • Visual Snapshot (UI changes only): Not applicable.
  • Documentation Impact: No documentation updates are required.
  • Contribution Guidelines: I have read and agree to the Contributor Guidelines.

Visual Snapshots

Not applicable; this is a task-persistence recovery fix.

Videos (interaction / animation only)

Not applicable.

Documentation Updates

  • No documentation updates are required.

Additional Notes

The recovery is limited to startup reconciliation. The existing live-session re-delegation guard remains in place to avoid detaching a child that may still be running.

Get in Touch

Summary by CodeRabbit

  • Bug Fixes

    • Improved task recovery after interruptions or application restarts, preserving delegated task relationships and restoring tasks to the correct states.
    • Prevented stale, incomplete, or invalid recovery data from causing repeated reconciliation issues.
    • Improved provider model configuration handling across supported providers, including OpenAI Native, VS Code LM, Bedrock, and Z.ai.
  • Tests

    • Added end-to-end coverage verifying task history and conversation persistence across VS Code restarts.
    • Expanded validation for task recovery and provider model changes.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: db0cdb9b-3829-4584-92da-7b47c3e757e8

📥 Commits

Reviewing files that changed from the base of the PR and between 7b5a5d0 and 2b2e331.

📒 Files selected for processing (6)
  • .github/workflows/e2e.yml
  • apps/vscode-e2e/src/runTest.ts
  • src/core/task-persistence/TaskHistoryStore.ts
  • src/core/task-persistence/__tests__/TaskHistoryStore.reconciliation.spec.ts
  • webview-ui/src/components/settings/utils/__tests__/providerModelConfig.spec.ts
  • webview-ui/src/components/settings/utils/providerModelConfig.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • webview-ui/src/components/settings/utils/providerModelConfig.ts
  • apps/vscode-e2e/src/runTest.ts
  • webview-ui/src/components/settings/utils/tests/providerModelConfig.spec.ts
  • src/core/task-persistence/TaskHistoryStore.ts

📝 Walkthrough

Walkthrough

Startup reconciliation now uses durable repair intents to recover persisted delegated task state across crashes. A two-phase VS Code E2E scenario verifies restart persistence. Provider configuration accepts string identifiers and expands canonical registry and side-effect tests.

Changes

Delegated task recovery

Layer / File(s) Summary
Durable delegation recovery
src/core/task-persistence/TaskHistoryStore.ts, src/shared/globalFileNames.ts
Startup replays validated repair intents before delegation reconciliation. Persisted active delegated children become interrupted, while their parents return to active. Malformed or stale intents are quarantined.
Validate recovery convergence
src/core/task-persistence/__tests__/TaskHistoryStore.reconciliation.spec.ts, src/eslint-suppressions.json
Tests cover partial writes, write failures, cleanup failures, malformed and stale intents, chained delegation, persistence, and restart idempotency.

Restart persistence E2E workflow

Layer / File(s) Summary
Define restart protocol and workspace
apps/vscode-e2e/src/restart/phaseProtocol.ts, apps/vscode-e2e/src/restart/scenarioWorkspace.ts, apps/vscode-e2e/fixtures/restart-persistence.json
The E2E workflow adds validated atomic phase results and guarded temporary scenario workspaces.
Execute restart phases
apps/vscode-e2e/src/restart/vscodeCoordinator.ts, apps/vscode-e2e/src/runTest.ts, apps/vscode-e2e/src/suite/index.ts, .github/workflows/e2e.yml
Dedicated restart-persistence tests run create and verify phases with configured exit policies, workspace cleanup, test discovery rules, and a separate CI step.
Verify persisted task state
apps/vscode-e2e/src/suite/restart-persistence.test.ts
The create phase records task history and conversation data. The verify phase checks that data after restart.

Provider model configuration

Layer / File(s) Summary
Broaden provider lookups
webview-ui/src/components/settings/utils/providerModelConfig.ts
Provider lookup helpers accept arbitrary string identifiers and retain fallback behavior.
Validate provider configuration
webview-ui/src/components/settings/utils/__tests__/providerModelConfig.spec.ts
Tests use canonical provider identifiers and cover model configuration, registry keys, unknown providers, and Bedrock model-change side effects.

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

Sequence Diagram(s)

sequenceDiagram
  participant TaskHistoryStore
  participant RepairIntent
  participant ChildTask
  participant ParentTask
  TaskHistoryStore->>RepairIntent: write repair intent
  TaskHistoryStore->>ChildTask: mark interrupted
  TaskHistoryStore->>ParentTask: restore active
  TaskHistoryStore->>RepairIntent: remove completed intent
Loading
sequenceDiagram
  participant TestRunner
  participant VSCodeCreate
  participant PhaseResults
  participant VSCodeVerify
  TestRunner->>VSCodeCreate: launch create phase
  VSCodeCreate->>PhaseResults: write persistence result
  TestRunner->>VSCodeVerify: launch verify phase
  VSCodeVerify->>PhaseResults: read create result and write verify result
Loading

Possibly related PRs

Suggested labels: awaiting-review

Suggested reviewers: navedmerchant

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The provider model configuration changes and related tests are unrelated to issue #1100 and the task recovery objectives. Remove the providerModelConfig production and test changes, or link an issue that explicitly requires those changes.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary change: recovering stale delegated children after restart.
Description check ✅ Passed The description includes the linked issue, implementation details, test procedure, checklist, and documentation assessment.
Linked Issues check ✅ Passed The recovery logic and tests address issue #1100, including nested delegation, stale references, parent restoration, and live-child protection.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue/1100

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

apps/vscode-e2e/src/runTest.ts

ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.

src/core/task-persistence/TaskHistoryStore.ts

ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.

src/core/task-persistence/__tests__/TaskHistoryStore.reconciliation.spec.ts

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

  • 2 others

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.

@edelauna
edelauna marked this pull request as ready for review August 9, 2026 15:10
@edelauna edelauna changed the title Issue/1100 fix(task): recover stale delegated children after restart Aug 9, 2026

@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: 3

🤖 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 370-374: Update the persistedActiveIds construction in
TaskHistoryStore to treat an omitted HistoryItem.status as "active" by
normalizing it before the predicate. Add a regression test covering a persisted
active child with no status and verify its delegated parent is repaired.
- Around line 420-429: Make the parent-child repair in the surrounding
reconciliation flow durable by recording a recoverable repair intent before
either upsertCore call, then completing or rolling it back during startup
reconciliation if either write or onWrite fails. Ensure interrupted recovery
clears the parent’s delegated state and restores the intended statuses, and add
a fault-injection test covering failure between the child and parent writes.

In
`@webview-ui/src/components/settings/utils/__tests__/providerModelConfig.spec.ts`:
- Around line 154-155: Remove the unnecessary `as any` cast in the
`getProviderModelConfig` test and pass the `"unknown-provider"` string directly,
preserving the assertion that the function returns undefined.
🪄 Autofix

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: baf3bf9e-7eb4-4256-aab0-212622c9496a

📥 Commits

Reviewing files that changed from the base of the PR and between 4c42f91 and 5b5c686.

📒 Files selected for processing (4)
  • src/core/task-persistence/TaskHistoryStore.ts
  • src/core/task-persistence/__tests__/TaskHistoryStore.reconciliation.spec.ts
  • webview-ui/src/components/settings/utils/__tests__/providerModelConfig.spec.ts
  • webview-ui/src/components/settings/utils/providerModelConfig.ts

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

codecov Bot commented Aug 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.05691% with 11 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/core/task-persistence/TaskHistoryStore.ts 91.05% 5 Missing and 6 partials ⚠️

📢 Thoughts on this report? Let us know!

@github-actions github-actions Bot added the awaiting-review PR changes are ready and waiting for maintainer re-review label Aug 9, 2026
@github-actions github-actions Bot removed the awaiting-review PR changes are ready and waiting for maintainer re-review label Aug 9, 2026

@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

🧹 Nitpick comments (2)
src/core/task-persistence/__tests__/TaskHistoryStore.reconciliation.spec.ts (2)

377-399: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Dispose locally created stores even when an assertion fails.

replayedStore.dispose() on Line 398 runs only if every preceding assertion passes. TaskHistoryStore.initialize() starts an fs.watch handle and a 5-minute setTimeout chain. If Line 395, 396, or 397 fails, that store is never disposed. Its watcher can then fire a reconcile() against the temp directory during a later test and log errors or keep the worker handle open.

The same pattern appears at Lines 342-346, 370-374, and 498-502. Register each locally created store for cleanup instead.

♻️ Proposed cleanup pattern
+	// Near the other hooks:
+	const disposables: TaskHistoryStore[] = []
+	afterEach(() => {
+		while (disposables.length) disposables.pop()?.dispose()
+	})

Then push each store after construction:

 		const replayedStore = new TaskHistoryStore(tmpDir)
+		disposables.push(replayedStore)
 		await replayedStore.initialize()
 		expect(replayedStore.get(child.id)?.status).toBe("interrupted")
 		expect(replayedStore.get(parent.id)?.status).toBe("active")
 		await expect(fs.access(intentPath)).rejects.toThrow()
-		replayedStore.dispose()
🤖 Prompt for 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.

In `@src/core/task-persistence/__tests__/TaskHistoryStore.reconciliation.spec.ts`
around lines 377 - 399, Ensure every locally created TaskHistoryStore in the
affected tests, including replayedStore and the stores created around the other
cited cases, is registered for guaranteed cleanup immediately after construction
rather than relying on a final dispose assertion. Use the test suite’s existing
cleanup mechanism so dispose runs even when initialization or later assertions
fail, while preserving the current assertions and store behavior.

401-415: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the two remaining intent-rejection branches.

replayDelegationRepairIntent has two rejection paths that this suite does not exercise:

  • TaskHistoryStore.ts Line 494: the intent references a parentTaskId or childTaskId that has no cached record. Expect quarantine and unchanged startup.
  • TaskHistoryStore.ts Line 510: both records exist, but the parent moved to a status that is neither delegated nor the active target. Expect quarantine and no write to either task file.

The second case matters most. It leaves the child at interrupted and the parent outside the repair, so it defines the end state after a guard mismatch.

As per path instructions, "Add focused tests for UI binding and save behavior, persistence or normalization, and the value returned by getStateToPostToWebview(), including true and false/unset cases when defaults could hide omissions."

🤖 Prompt for 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.

In `@src/core/task-persistence/__tests__/TaskHistoryStore.reconciliation.spec.ts`
around lines 401 - 415, Extend the reconciliation tests around
replayDelegationRepairIntent with two focused cases: quarantine an intent whose
parentTaskId or childTaskId has no cached record while leaving unrelated startup
unchanged, and quarantine an intent where both records exist but the parent
status is neither delegated nor the active repair target. For the
status-mismatch case, assert the child remains interrupted, the parent remains
outside repair, and neither task file is written.

Source: Path instructions

🤖 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 `@apps/vscode-e2e/src/runTest.ts`:
- Around line 48-49: Update isRestartPersistenceTargetedRun to recognize
TEST_GREP selections for the “Restart persistence” scenario in addition to
testFile names containing “restart-persistence”. Ensure the helper returns true
when TEST_GREP targets that scenario so the runner uses the restart-specific
execution path.

In `@src/core/task-persistence/TaskHistoryStore.ts`:
- Around line 343-355: Scope the disk refresh in reconcile() so the full
task-file reload occurs only for the startup repair path: add the appropriate
force-refresh option and call reconcile({ forceRefresh: true }) from
initialize(), while watcher and periodic calls use change detection such as
mtime. Update changed only when a parsed item differs from the cached entry,
preventing unnecessary index writes.
- Around line 544-593: Contain failures from each repair initiated by
reconcileDelegationState so a rejected writeTaskFile, onWrite, or related
operation does not abort initialize; preserve the durable repair intent for
replay and allow startup to continue through startWatcher and
startPeriodicReconciliation while initialized resolves. Update the affected
reconciliation tests to expect initialization to resolve with the intent
retained for recovery on restart.

In `@webview-ui/src/components/settings/utils/providerModelConfig.ts`:
- Around line 89-90: Update getProviderServiceConfig and the corresponding
provider-default-model lookup to accept registry values only when the requested
key is an own property of PROVIDER_SERVICE_CONFIG or PROVIDER_DEFAULT_MODEL_IDS.
Preserve the existing fallback behavior for unknown strings, including inherited
keys such as "constructor" and "toString", and add regression coverage for those
keys.

---

Nitpick comments:
In `@src/core/task-persistence/__tests__/TaskHistoryStore.reconciliation.spec.ts`:
- Around line 377-399: Ensure every locally created TaskHistoryStore in the
affected tests, including replayedStore and the stores created around the other
cited cases, is registered for guaranteed cleanup immediately after construction
rather than relying on a final dispose assertion. Use the test suite’s existing
cleanup mechanism so dispose runs even when initialization or later assertions
fail, while preserving the current assertions and store behavior.
- Around line 401-415: Extend the reconciliation tests around
replayDelegationRepairIntent with two focused cases: quarantine an intent whose
parentTaskId or childTaskId has no cached record while leaving unrelated startup
unchanged, and quarantine an intent where both records exist but the parent
status is neither delegated nor the active repair target. For the
status-mismatch case, assert the child remains interrupted, the parent remains
outside repair, and neither task file is written.
🪄 Autofix

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: b1de36ae-c6fb-4047-a17e-d66364faa6fb

📥 Commits

Reviewing files that changed from the base of the PR and between 5b5c686 and 85a16e7.

📒 Files selected for processing (13)
  • apps/vscode-e2e/fixtures/restart-persistence.json
  • apps/vscode-e2e/src/restart/phaseProtocol.ts
  • apps/vscode-e2e/src/restart/scenarioWorkspace.ts
  • apps/vscode-e2e/src/restart/vscodeCoordinator.ts
  • apps/vscode-e2e/src/runTest.ts
  • apps/vscode-e2e/src/suite/index.ts
  • apps/vscode-e2e/src/suite/restart-persistence.test.ts
  • src/core/task-persistence/TaskHistoryStore.ts
  • src/core/task-persistence/__tests__/TaskHistoryStore.reconciliation.spec.ts
  • src/eslint-suppressions.json
  • src/shared/globalFileNames.ts
  • webview-ui/src/components/settings/utils/__tests__/providerModelConfig.spec.ts
  • webview-ui/src/components/settings/utils/providerModelConfig.ts
💤 Files with no reviewable changes (1)
  • src/eslint-suppressions.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • webview-ui/src/components/settings/utils/tests/providerModelConfig.spec.ts

Comment thread apps/vscode-e2e/src/runTest.ts Outdated
Comment on lines 343 to 355
// Task files are authoritative. Always refresh entries from disk so a stale
// index cannot overwrite a repair or another instance's newer task state.
for (const taskId of onDiskIds) {
if (!cacheIds.has(taskId)) {
try {
const item = await this.readTaskFile(taskId)
if (item) {
this.cache.set(taskId, item)
changed = true
}
} catch {
// Corrupted or missing file, skip
try {
const item = await this.readTaskFile(taskId)
if (item) {
this.cache.set(taskId, item)
changed = true
}
} catch {
// Corrupted or missing file, skip
}
}

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.

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Scope the unconditional disk refresh; it now runs on every watcher and periodic reconcile.

reconcile() is called from three places: initialize(), the fs.watch handler (500 ms debounce), and the 5‑minute periodic timer. This loop now reads and parses every task file on each of those calls, instead of only the files absent from the cache. Each single task write in the tasks directory triggers a watcher event and therefore a full O(N) re-read of all task history files.

changed is also set to true whenever any file is read, so scheduleIndexWrite() now fires on every reconcile pass and rewrites the whole index, even when nothing drifted.

The authoritative-refresh guarantee is only needed to protect the startup repair path from a stale index. Consider keeping the full refresh for the startup call and using mtime or a force flag for the watcher and periodic paths, and set changed only when the parsed item differs from the cached entry.

♻️ Sketch: gate the full refresh and the dirty flag
-	async reconcile(): Promise<void> {
+	async reconcile(options: { forceRefresh?: boolean } = {}): Promise<void> {
 		// Run through the write lock to prevent interleaving with upsert/delete
 		return this.withLock(async () => {
@@
 			for (const taskId of onDiskIds) {
 				try {
+					if (!options.forceRefresh && this.cache.has(taskId)) {
+						continue
+					}
 					const item = await this.readTaskFile(taskId)
-					if (item) {
+					if (item && JSON.stringify(item) !== JSON.stringify(this.cache.get(taskId))) {
 						this.cache.set(taskId, item)
 						changed = true
 					}
 				} catch {

Then call this.reconcile({ forceRefresh: true }) from initialize().

🤖 Prompt for 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.

In `@src/core/task-persistence/TaskHistoryStore.ts` around lines 343 - 355, Scope
the disk refresh in reconcile() so the full task-file reload occurs only for the
startup repair path: add the appropriate force-refresh option and call
reconcile({ forceRefresh: true }) from initialize(), while watcher and periodic
calls use change detection such as mtime. Update changed only when a parsed item
differs from the cached entry, preventing unnecessary index writes.

Comment thread src/core/task-persistence/TaskHistoryStore.ts
Comment thread webview-ui/src/components/settings/utils/providerModelConfig.ts Outdated
@github-actions github-actions Bot added awaiting-review PR changes are ready and waiting for maintainer re-review and removed awaiting-review PR changes are ready and waiting for maintainer re-review labels Aug 9, 2026
@github-actions github-actions Bot added the awaiting-review PR changes are ready and waiting for maintainer re-review label Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting-review PR changes are ready and waiting for maintainer re-review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Task lifecycle: unable to create/delegate subtasks (multi level) after interruption

1 participant