Repository navigation
[lifecycle-RSK] Run-state kernel #1790
Description
Tracking
- Umbrella: [Umbrella] Task lifecycle gap remediation — LIFE-GAP-001..040 #1688
- Assignee: @edelauna
- Design doc:
docs/architecture/task-lifecycle-target-architecture.md(not yet onmain) - Ticket plan:
docs/architecture/task-lifecycle-run-state-kernel-tickets.md(not yet onmain)
Context
Task models run state with 11 run fields and 2 dispatch locks. The fields overlap in time: an ask can be pending while a stream is live or after it ends, and the abort latches stay set across both. The type system cannot tell a legal combination from an illegal one. The fix is orthogonal regions, latches, a pure transition function, derived getters, and an actor per task. This epic builds the kernel with shadow-then-strangle migration. It does not change the persisted reducers.
Scope
In scope: the in-memory run-state kernel. It replaces the run-state fields on Task with orthogonal regions (phase, stream, ask), latches, a turn sub-machine, a pure transition function, and derived getters. It uses shadow-then-strangle migration. It also fixes three code races that block the migration (#1801, #1802, #1803). The maintainer product requirements PR-1 to PR-8 in the design doc set the target behavior.
Out of scope:
- Persisted ownership and generation (P1) and crash recovery (P2). These carry the data-loss risk. They stay a separate program.
- Tool-call identity and approval correlation (
LIFE-GAP-036,037,038). These are P4 and P5 identity work. The kernel storestoolCallIdandapprovalIdas optional opaque strings. P4-010 guards detached writes with the kernelgeneration, so it depends on [lifecycle-RSK] RSK-10: Route stream, ask, and turn-field writes through events #1804. - Queued user input. The queued-input bugs come from
Task#ask, not from run state. - Concurrent sibling fan-out.
Decisions
- Use orthogonal regions of discriminated unions and a pure reducer. Do not add xstate.
- Keep the persisted reducers (
src/core/task-persistence/taskLifecycle.ts) unchanged. The kernel is a separate in-memory submodel (task-lifecycle-model.md, Extending the model). - Model
abort,abandoned,disposed, andreasonas latches. Latches never return to false within one instance.didFinishAbortingStreamis not a latch. It lives on the live stream ascleanupFinished, so every instance gets the same cancel timing (PR-2). - The kernel always runs from [lifecycle-RSK] RSK-04: Add the run field, write helpers, and the projection #1794. Only the divergence comparison is gated.
- Route every write of a field through a write helper before any read of that field flips. The helper sets the field and drives the event. The field stays authoritative until its read flip.
- There is no runtime kill switch after a read flip. Rollback of a flip is a revert of that flip PR. Each flip PR covers one field family, so a revert stays small.
- The design doc "Known code races" table is the only ignore list for the comparison. Each entry names its fix ticket or its reason to allow.
Rollout gate
The divergence comparison runs when one of these conditions is true:
process.env.PKG_RELEASE_CHANNEL === "prerelease".src/esbuild.mjs:50defines this value, and the nightly workflow sets it (.github/workflows/nightly-publish.yml:80, 89).contextProxy.extensionModeisvscode.ExtensionMode.Developmentorvscode.ExtensionMode.Test.ClineProvider.ts:1046checksDevelopmentin the same style. No non-test file insrcchecksProductiontoday.
Do not add a new define. Do not add a user setting. The gate controls only the comparison and its log. It never controls the kernel computation or the read source, so a read never sees an inert kernel.
Sub-issues in dependency order
Listed in dependency order.
- [lifecycle-RSK] RSK-01: Add the pure run-state kernel module #1791 Add the pure run-state kernel module
- [lifecycle-RSK] RSK-02: Extend the bounded model-check to the kernel #1792 Extend the bounded model-check to the kernel
- [Story 4.2a]
DispatchStatemachine refactor #374 DispatchState machine refactor ([Story 4.2a]DispatchStatemachine refactor #374, related: its parent is [Epic 4] Parallel Tool Execution #359) - [lifecycle-RSK] RSK-12: Remove the dead isPaused field #1793 Remove the dead isPaused field
- [lifecycle-RSK] RSK-19: Keep the first abort reason #1802 Keep the first abort reason
- [lifecycle-RSK] RSK-20: Check abort before an ask posts #1803 Check abort before an ask posts
- [lifecycle-RSK] RSK-04: Add the run field, write helpers, and the projection #1794 Add the run field, write helpers, and the projection
- [lifecycle-RSK] RSK-05: Add the divergence comparison and a metric #1795 Add the divergence comparison and a metric
- [lifecycle-RSK] RSK-06: Route latch and phase writes through events #1796 Route latch and phase writes through events
- [lifecycle-RSK] RSK-07: Flip abort and abortReason reads #1797 Flip abort and abortReason reads
- [lifecycle-RSK] RSK-08: Flip abandoned reads #1798 Flip abandoned reads
- [lifecycle-RSK] RSK-11: Flip isInitialized reads #1799 Flip isInitialized reads
- [lifecycle-RSK] RSK-09: Add the turn sub-machine #1800 Add the turn sub-machine
- [lifecycle-RSK] RSK-17: Reset didFinishAbortingStream for each request #1801 Reset didFinishAbortingStream for each request
- [lifecycle-RSK] RSK-10: Route stream, ask, and turn-field writes through events #1804 Route stream, ask, and turn-field writes through events
- [lifecycle-RSK] RSK-16: Flip stream and turn-field reads #1805 Flip stream and turn-field reads
- [lifecycle-RSK] RSK-13: Wrap run state in a task actor with a serialized mailbox #1806 Wrap run state in a task actor with a serialized mailbox
- [lifecycle-RSK] RSK-14: Route the provider through actor messages #1807 Route the provider through actor messages
- [lifecycle-RSK] RSK-15: Add the RunState to persisted-status consistency check #1808 Add the RunState to persisted-status consistency check
Withdrawn number: RSK-18 is not used.
- RSK-18 would have checked
abortagain before a stream starts. That early throw skips the chunk-loop catch, which marks theapi_req_startedrow cancelled and sets the reason. The kernel allows race R-2 instead, and the request guard (Task.ts:4615) stops the model call.
Field surface
Counts are Task-owned references in src at the audited commit. Tests and comment lines are excluded. The abort count excludes .abort() method calls on controllers and processes. The isInitialized count excludes 6 references on other classes. Route the writes before you flip the reads.
| Field | Writes | Reads | Home | Writes routed | Reads flipped |
|---|---|---|---|---|---|
abort |
3 | 41 | latches.abort |
#1796 | #1797 |
abortReason |
4 | 4 | latches.reason |
#1796 | #1797 |
abandoned |
3 | 22 | latches.abandoned |
#1796 | #1798 |
isInitialized |
4 | 3 | phase |
#1796 | #1799 |
didFinishAbortingStream |
2 | 1 | stream.cleanupFinished |
#1804 | #1805 |
isStreaming |
3 | 2 | stream |
#1804 | #1805 |
isWaitingForFirstChunk |
4 | 1 | turn sub-machine | #1804 | #1805 |
didRejectTool |
10 | 6 | turn sub-machine | #1804 | #1805 |
didAlreadyUseTool |
2 | 3 | turn sub-machine | #1804 | #1805 |
didToolFailInCurrentTurn |
54 | 1 | turn sub-machine | #1804 | #1805 |
didCompleteReadingStream |
2 | 2 | turn sub-machine | #1804 | #1805 |
presentAssistantMessageLocked |
5 | 1 | DispatchState |
#374 | #374 |
presentAssistantMessageHasPendingUpdates |
3 | 1 | DispatchState |
#374 | #374 |
isPaused |
0 | 1 | remove | none | #1793 |
Follow-up issues outside this epic
The product requirements create these issues. They are not RSK tickets, because they do not change run state.
| Requirement | Issue to file | Owner |
|---|---|---|
| PR-1 | Record the cancelled request row, its tokens, and its cost on a user cancel, without a wait for the stream. | New bug |
| PR-4 | LIFE-GAP-039: continuing a completed task keeps the persisted status completed. |
LIFE-BLK-P1-039 (#1689) |
| PR-5 | Add the teardown cause to the TaskAborted payload. |
P6 (#1694) |
| PR-6 | Tie each ask response to its askTs, and return an explicit tool result for a superseded approval. |
P5 (#1693), with #998 |
LIFE-GAP-002 |
Split the lifecycle fields out of the per-message save (Task.ts:1377-1411). The save writes rootTaskId and parentTaskId every time, so a late save restores abandoned lineage. |
P1 implementation issue for LIFE-BLK-P1-002 (#1689) |
| Speed | Deferred to a separate speed epic: batch the per-chunk webview messageUpdated post (#1343), add gated and sampled speed metrics, and write messages incrementally instead of rewriting the whole file. |
Not planned yet |
LIFE-GAP-039 is in the gap report register, and LIFE-BLK-P1-039 owns it in the remediation blocks.
Activity