feat(task-lifecycle): task status transition guard and startup delegation reconciliation#692
Conversation
📝 WalkthroughWalkthroughThis PR adds task-status transition validation, repairs delegated task state during startup, exposes task-history lookup through the API, and updates delegation flows, tests, and mocks to align with the new persistence behavior. ChangesTask lifecycle fixes
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related issues
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
5e4b554 to
cc72b45
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/core/webview/ClineProvider.ts (1)
537-541: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winClear
delegatedToIdwhenever the parent returns toactive.Line 537, Line 3214, and Line 3789 all repair/resume the parent back to
active, but they leave the olddelegatedToIdbehind. That persists contradictory history (status: "active"with a stale delegated-child pointer), and the new startup reconciliation only repairs parents that are stilldelegated, so this inconsistency can survive reloads.Suggested fix
await this.updateTaskHistory({ ...parentHistory, status: "active", + delegatedToId: undefined, awaitingChildId: undefined, })await this.updateTaskHistory({ ...parentHistory, status: "active", + delegatedToId: undefined, awaitingChildId: undefined, })updatedHistory = { ...parent, status: "active" as const, completedByChildId: childTaskId, completionResultSummary, + delegatedToId: undefined, awaitingChildId: undefined, childIds, }Also applies to: 3214-3218, 3788-3796
🤖 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/webview/ClineProvider.ts` around lines 537 - 541, The parent task history is being set back to active in ClineProvider without clearing the stale delegated child pointer, which leaves contradictory state behind. Update every parent-recovery path in ClineProvider that restores status to "active" to also clear delegatedToId alongside awaitingChildId, using the same updateTaskHistory flow in the affected resume/repair branches. Make sure the reconciliation and resume paths all normalize the parent record the same way so no old delegated child reference survives reloads.
🤖 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 113-119: `TaskHistoryStore.initialize()` currently repairs
delegation state before `ClineProvider.migrateFromGlobalState(...)`, so migrated
orphaned delegated parents can remain unrepaired until a later restart. Update
the startup flow in `ClineProvider` to either run migration before calling
`taskHistoryStore.initialize()`, or invoke the same delegation-repair path again
immediately after migration; use `TaskHistoryStore.initialize()` and
`reconcileDelegationState()` as the key points to adjust.
---
Outside diff comments:
In `@src/core/webview/ClineProvider.ts`:
- Around line 537-541: The parent task history is being set back to active in
ClineProvider without clearing the stale delegated child pointer, which leaves
contradictory state behind. Update every parent-recovery path in ClineProvider
that restores status to "active" to also clear delegatedToId alongside
awaitingChildId, using the same updateTaskHistory flow in the affected
resume/repair branches. Make sure the reconciliation and resume paths all
normalize the parent record the same way so no old delegated child reference
survives reloads.
🪄 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: 9368591b-ae36-4d09-92e5-63b1f43ba982
📒 Files selected for processing (16)
src/__tests__/helpers/provider-stub.tssrc/__tests__/history-resume-delegation.spec.tssrc/__tests__/nested-delegation-resume.spec.tssrc/__tests__/provider-delegation.spec.tssrc/core/task-persistence/TaskHistoryStore.tssrc/core/task-persistence/__tests__/TaskHistoryStore.reconciliation.spec.tssrc/core/task-persistence/__tests__/TaskHistoryStore.spec.tssrc/core/task-persistence/index.tssrc/core/task/__tests__/Task.persistence.spec.tssrc/core/task/__tests__/Task.throttle.test.tssrc/core/webview/ClineProvider.tssrc/core/webview/__tests__/ClineProvider.flicker-free-cancel.spec.tssrc/core/webview/__tests__/checkpointRestoreHandler.spec.tssrc/core/webview/__tests__/webviewMessageHandler.checkpoint.spec.tssrc/core/webview/__tests__/webviewMessageHandler.delete.spec.tssrc/core/webview/__tests__/webviewMessageHandler.edit.spec.ts
| export type HistoryItemStatus = NonNullable<HistoryItem["status"]> | ||
|
|
||
| const VALID_TRANSITIONS: Record<HistoryItemStatus, HistoryItemStatus[]> = { | ||
| active: ["delegated", "completed"], |
There was a problem hiding this comment.
is active to active a valid transition? should we add that?
There was a problem hiding this comment.
It's not really a state change - it's guarded against before being called - so active -> active would be unexpected, if it happens better it throws so we can see why it's being called, to investigate as there shouldn't be any self loops. The other call sites check if this is actually a state change, or just a metadata update.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
apps/vscode-e2e/src/suite/subtasks.test.ts (1)
240-246: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueMinor: duplicated cleanup blocks could be a loop.
Two sequential
if (length > 0) { clearCurrentTask() }blocks only handle up to two leftover stack entries. Awhileloop would be more robust without added complexity.♻️ Optional cleanup loop
- if (api.getCurrentTaskStack().length > 0) { - await api.clearCurrentTask() - } - if (api.getCurrentTaskStack().length > 0) { - await api.clearCurrentTask() - } + while (api.getCurrentTaskStack().length > 0) { + await api.clearCurrentTask() + }🤖 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 `@apps/vscode-e2e/src/suite/subtasks.test.ts` around lines 240 - 246, The cleanup in subTasks test uses two repeated api.getCurrentTaskStack() checks that only remove up to two leftover entries; replace the duplicated blocks with a single loop so all remaining tasks are cleared. Update the logic in subTasks.test.ts around api.clearCurrentTask and api.getCurrentTaskStack() to keep clearing until the stack is empty, then keep the existing waitFor assertion.
🤖 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.
Nitpick comments:
In `@apps/vscode-e2e/src/suite/subtasks.test.ts`:
- Around line 240-246: The cleanup in subTasks test uses two repeated
api.getCurrentTaskStack() checks that only remove up to two leftover entries;
replace the duplicated blocks with a single loop so all remaining tasks are
cleared. Update the logic in subTasks.test.ts around api.clearCurrentTask and
api.getCurrentTaskStack() to keep clearing until the stack is empty, then keep
the existing waitFor assertion.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: a21468eb-b2ed-4e42-b61a-c113798238a8
📒 Files selected for processing (3)
apps/vscode-e2e/src/suite/subtasks.test.tspackages/types/src/api.tssrc/extension/api.ts
… [imported from upstream 7476c67]
Related GitHub Issue
Closes: #366 (part of #357 — Epic 2: Task Lifecycle Fixes)
Description
Implements Story 2.3: a status transition guard enforced at the store write boundary, plus a startup reconciliation loop to detect and repair
HistoryItemstatus corruption.Transition guard (
assertValidTransition)Adds
assertValidTransition(from, to)inTaskHistoryStore.tsthat enforces the valid state machine:active → delegated | completeddelegated → activecompleted → [](terminal)The guard is enforced at two levels:
upsertCorewrite boundary — every single-item write throughupsertoratomicReadAndUpdatevalidates the transition before touching disk. Legacy items whosestatusisundefinedare normalised to"active"before comparison so an idempotent re-write doesn't throw.atomicUpdatePairwrite boundary — the two-item atomic write (childactive → completed, parentdelegated/active → active) validates both updater outputs before either file is written, so an invalid transition in either updater aborts the entire pair atomically.Explicit
assertValidTransitioncalls are also placed inside the updater lambdas passed toatomicUpdatePairinreopenParentFromDelegationand inside theatomicReadAndUpdateupdater indelegateParentAndOpenChild,cancelTask, andremoveClineFromStack— providing a clear guard at the call site as well as at the store boundary.Partial-write resilience
reopenParentFromDelegationwrites the child record first (child→ completed) and then the parent (parent→ active) withinatomicUpdatePair. If the extension crashes after the child write but before the parent write, startup reconciliation detects the still-delegatedparent with acompletedchild and repairs it. To ensure the repair copies the real result rather than a generic fallback, the child updater now also persistscompletionResultSummaryonto the child record.Startup reconciliation (
reconcileDelegationState)Called from
TaskHistoryStore.initialize()afterreconcile(). Repairs three crash-induced inconsistencies:delegatedwith noawaitingChildId→activedelegated, child not found (orphaned) →activedelegated, child alreadycompleted(interrupted handoff) →active, copying the child'scompletionResultSummaryImplementation details:
withLockvia the privateupsertCorehelper (avoids re-entrant deadlock)do { } while (repairsInThisPass > 0)) so chained delegations (A→B→C) are resolved in one startupMapawaitingChildIdanddelegatedToIdskipTransitionCheck: trueon repair writes since these are administrative corrections, not runtime state-machine transitionsTest Procedure
93 tests across the five affected suites pass.
TaskHistoryStore.reconciliation.spec.tscovers the transition guard (valid/invalid transitions, legacyundefinedstatus,undefined → delegated), all reconciliation scenarios (orphaned delegation, interrupted handoff, fallback summary, active-child left alone, multiple parents, chained delegation, idempotency, logging), and theatomicReadAndUpdatestore-level guard.TaskHistoryStore.spec.tscovers theatomicUpdatePairstore-level guard (invalid transition rejected even without an explicit updater assertion).Pre-Submission Checklist
Summary by CodeRabbit