Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
4cb0120
fix(delegation): preserve live child delegation links across extensio…
Sep 2, 2026
717e233
test(task-persistence): cover mutation edge cases for live child live…
Sep 3, 2026
289c822
test(task-persistence): kill surviving static-mutant arithmetic mutan…
Sep 3, 2026
55ab370
chore(gitignore): ignore local worktrees (.wt-*) and scratch files
Sep 3, 2026
0c35045
test(task-persistence): make liveness-boundary tests filesystem-preci…
Sep 3, 2026
85b4e16
fix(delegation): guard replayDelegationRepairIntent against live cros…
Sep 4, 2026
795da5e
fix(delegation): run delegation reconciliation on periodic reconcile …
Sep 5, 2026
d2e67f9
fix(delegation): use console.warn for live-child skip log
Sep 5, 2026
1f88b8b
test(delegation): route undefined child mtime through initialize() en…
Sep 5, 2026
7206d6f
test(lifecycle): model cross-window child liveness guard in lifecycle…
Sep 5, 2026
ccf801b
chore(gitignore): drop local worktree and scratch ignore patterns
Sep 5, 2026
df7b644
test(delegation): kill 15 surviving changed-code mutants
Sep 5, 2026
40e06ca
test(delegation): replace fixed-count timer pump with condition polling
Sep 5, 2026
d4c619b
fix(delegation): harden cross-window liveness guards per CodeRabbit r…
Sep 8, 2026
cdfc136
fix(delegation): roll back ownership claim on non-started resume path…
Sep 8, 2026
2375b10
test(delegation): add markLocallyInactive to wholesale TaskHistorySto…
Sep 8, 2026
5708994
test(delegation): kill 19 changed-code mutants in claim/rollback and …
Sep 8, 2026
05278f0
fix(delegation): close ownership race in reconciliation; CodeRabbit r…
Sep 8, 2026
1794520
test(delegation): strengthen resolved-task assertion in hookless crea…
Sep 8, 2026
8c0ff22
refactor(webview): eliminate all 12 no-explicit-any violations from C…
Sep 8, 2026
e9a1820
refactor(task): eliminate all 17 no-explicit-any violations from Task.ts
Sep 8, 2026
3f10771
test(task): kill 43 changed-code mutants surfaced by Task.ts type-cle…
Sep 8, 2026
0ce3e21
test(task): pin reasoning-block id key-absence with explicit property…
Sep 8, 2026
b9896d2
fix(delegation): release local ownership when continuation scheduling…
Sep 20, 2026
0190a54
test(delegation): cover continuation scheduling rejection and cancell…
Sep 20, 2026
ed792d3
test(task): pass threaded request model snapshot in buildCleanConvers…
Sep 20, 2026
4fc47fc
docs(lifecycle): restrict invariant 8 to boolean model behavior
Sep 20, 2026
6d05781
fix(task-persistence): drop evicted task ids from local ownership on …
Sep 22, 2026
09f7f46
fix(task-persistence): keep live delegated children during reconcile
Sep 23, 2026
5534add
Merge remote-tracking branch 'refs/remotes/upstream/main' into fix/re…
myk1yt Sep 27, 2026
623d756
Merge remote-tracking branch 'refs/remotes/upstream/main' into fix/re…
myk1yt Oct 3, 2026
64ee0a3
fix(test): import LOCK_STALE_MS from utils/fileLock after upstream move
myk1yt Oct 3, 2026
fd375d1
fix(task): keep delegated children alive and owned across the whole s…
myk1yt Oct 3, 2026
a4cdc90
fix(task-persistence): grace-guard the missing-child orphan repair an…
myk1yt Oct 3, 2026
608d913
fix(webview): claim local session ownership at every spawn path
myk1yt Oct 3, 2026
9f2acce
fix(task-persistence): re-validate child liveness under the lock befo…
myk1yt Oct 3, 2026
fe67d5c
chore(model-check): print measured action/landmark/witness counts
myk1yt Oct 3, 2026
55639e3
fix(test): stub heartbeat/ownership methods on partial Task and store…
myk1yt Oct 3, 2026
127f051
fix(task): cover the nested-delegation resume entry with the liveness…
myk1yt Oct 3, 2026
f3f7eaa
fix(task-persistence): harden local ownership claims against transien…
myk1yt Oct 3, 2026
791f724
fix(task-persistence): scope the liveness heartbeat write to live, ex…
myk1yt Oct 3, 2026
6c09dbf
fix(task-persistence): re-validate orphan repairs under the target's …
myk1yt Oct 3, 2026
a9a72c0
refactor(task-persistence): trim mutation-diff budget lines and P3 cl…
myk1yt Oct 3, 2026
9990c36
Merge remote-tracking branch 'upstream/main' into fix/returntoparent
myk1yt Oct 3, 2026
93eef1e
Merge remote-tracking branch 'upstream/main' into fix/returntoparent
myk1yt Oct 8, 2026
bc6697e
chore: retrigger CI after windows-only mutation-gate flake
myk1yt Oct 9, 2026
9eb03cd
Merge branch 'main' (upstream 4d14dbe1) into fix/returntoparent
myk1yt Oct 30, 2026
6e9d65b
fix(delegation): merge administrative repairs into the locked disk re…
myk1yt Oct 30, 2026
6105d38
refactor(delegation): shrink the repair-delta change and apply pretti…
myk1yt Oct 30, 2026
0cab3f8
style: format the four files main's new format:check rejects
myk1yt Oct 30, 2026
8c38908
refactor(delegation): cut the changed-executable-line budget to 498
myk1yt Oct 30, 2026
b267ac7
fix(delegation): close the four confirmed #1495 liveness and repair r…
myk1yt Oct 30, 2026
86715bc
fix(delegation): make the repair commit mutually exclusive with a loc…
myk1yt Oct 30, 2026
a9d4c51
fix(persistence): keep the backup when a rollback rename fails
myk1yt Oct 30, 2026
cbc020e
test(delegation): pin the child half of the parent-claim repair abort
myk1yt Oct 30, 2026
4869da7
fix(lifecycle): correct model invariant scope and share the crash-orp…
myk1yt Oct 31, 2026
957524f
fix(delegation): release the parent claim and stop the heartbeat on a…
myk1yt Oct 31, 2026
259e2a6
fix(delegation): stop the crash-orphan repair at the child half
myk1yt Oct 31, 2026
41ffaaf
test(delegation): pin the journal bytes the child-half repair writes
myk1yt Oct 31, 2026
2b60fcf
test(delegation): wire the sync mtime probe into the L566 mutation-ki…
myk1yt Oct 31, 2026
5c6229e
test(task-persistence): correct four vacuous mutation-kill titles and…
myk1yt Nov 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 21 additions & 14 deletions docs/architecture/task-lifecycle-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,23 @@ TLA+/PlusCal or Quint with TLC becomes a better fit when the lifecycle needs tem

## Production mapping

| Model concept | Production concept |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| Task record and status | `HistoryItem` persisted by `TaskHistoryStore` |
| `delegate(parent, child)` | `ClineProvider.delegateParentAndOpenChild` |
| `interrupt(child)` | cancellation or eviction through `markDelegatedChildInterrupted` |
| `complete(child)` | `ClineProvider.reopenParentFromDelegation` |
| `abandon(child)` | `ClineProvider.abandonSubtask` |
| Pending-action settlement | `TaskHistoryStore.clearPendingActionIfMatching` compare-and-clear in the rejected-delegation settlement path (#1714) |
| Atomic event step | `atomicReadAndUpdate`, `atomicUpdatePair`, and per-parent delegation transition lock |
| Event interleaving | Competing completion, cancellation, abandonment, and new delegation calls |

The model has three fixed task slots, enough to cover competing siblings and a nested parent-child-grandchild chain. It explores every reachable interleaving through depth 12, deduplicating canonical states. Representative checks also exercise rejected operations that do not create a new state: a second concurrent delegation while the first child is active, stale completion after re-delegation, late completion after abandonment, completion after interruption, and nested completion. Named semantic landmarks require the graph to retain interrupted-child re-delegation and nested delegation even when the raw state total changes.
| Model concept | Production concept |
| --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Task record and status | `HistoryItem` persisted by `TaskHistoryStore` |
| `delegate(parent, child)` | `ClineProvider.delegateParentAndOpenChild` |
| `interrupt(child)` | cancellation or eviction through `markDelegatedChildInterrupted` |
| `complete(child)` | `ClineProvider.reopenParentFromDelegation` |
| `abandon(child)` | `ClineProvider.abandonSubtask` |
| Pending-action settlement | `TaskHistoryStore.clearPendingActionIfMatching` compare-and-clear in the rejected-delegation settlement path (#1714) |
| `reconcileStartup(parent)` | startup/periodic `TaskHistoryStore.reconcileDelegationStateCore` orphan repair |
| `markLiveElsewhere(child)` / `expireLiveElsewhere(child)` | child history-file mtime inside the bounded freshness window (`\|now - mtime\| < LIVE_CHILD_MTIME_THRESHOLD_MS`) vs outside it; the store's transient-stat sentinel sits one millisecond inside the future bound (abstracted; no wall clock in model) |
| `heartbeat(child)` / `expireHeartbeat(child)` | persisted `lastActivityAt` liveness heartbeat fresh vs stale against the same bounded window (production: the owning session's throttled heartbeat across the child's whole active lifetime — long streaming turns, long tool calls, and ask-idle periods alike — shared predicate `isDelegatedChildLive`) |
| Atomic event step | `atomicReadAndUpdate`, `atomicUpdatePair`, and per-parent delegation transition lock |
| Event interleaving | Competing completion, cancellation, abandonment, and new delegation calls |

The model has three fixed task slots, enough to cover competing siblings and a nested parent-child-grandchild chain, plus two abstract booleans per slot recording whether an active child's session shows life: one for ownership by another window (recent history-file mtime) and one for a fresh persisted `lastActivityAt` heartbeat from the owning session. It explores every reachable interleaving through depth 12, deduplicating canonical states. Representative checks also exercise rejected operations that do not create a new state: a second concurrent delegation while the first child is active, stale completion after re-delegation, late completion after abandonment, completion after interruption, and nested completion. Named semantic landmarks require the graph to retain interrupted-child re-delegation and nested delegation even when the raw state total changes, a delegated parent whose active child is live in another window surviving startup reconciliation unchanged, a delegated parent whose active child heartbeats through a long streaming turn (stale mtime, fresh `lastActivityAt`) likewise surviving unchanged, and a stale-mtime, stale-heartbeat (crash-orphan) active child being repaired to `interrupted` while its parent keeps its `awaitingChildId` link, only through `reconcileStartup`.

The crash-orphan repair is deliberately a single write. An earlier shape wrote the parent to `active` as a second, independently locked write, so a peer window resuming the child between the two halves was invisible to the parent-only under-lock guard, the unlink committed, and `reopenParentFromDelegation` then aborted on the `awaitingChildId` mismatch — the resumed child's result was dropped. Stopping at the child half makes every interleaving route, and it converges on the state `cancelTask` already produces mid-session: the parent stays `delegated` awaiting an `interrupted` child until that child is resumed and completes, the parent is re-delegated over the interrupted child, the child record is deleted (the missing-child repair, which is the only path that still reactivates the parent), or the user cancels.

Each task slot can also hold one of two pending `create_subtask` actions. A `stage` action mirrors `setPendingTaskAction` overwrite semantics, delegation clears the action its request carried, completion clears the child's action only when its event carries the matching action ID, and a `settle-rejected` action models the settlement that follows an authoritative delegation rejection (#1714). Production settles through the typed `LifecycleTransitionError` from the shared guards: the provider calls the disk-authoritative `TaskHistoryStore.clearPendingActionIfMatching` compare-and-clear under the per-file lock, then propagates the original rejection. Six named witnesses must remain reachable: settlement from an interrupted record after rejection, settlement through a successful active delegation, unrelated-action preservation during completion, stale-action protection where a settlement targeting one action ID leaves a replacement action intact, matching-ID completion clearing, and replacement-ID completion preservation. A mismatched pending-action request keeps its production behavior: the atomic update throws before any transition, and no settlement runs.

Expand Down Expand Up @@ -175,8 +180,10 @@ The task delegation checker currently enforces:
5. Parent-child lineage is acyclic.
6. Completed task records cannot be changed by later lifecycle events.
7. Active-child re-delegation, stale completion after ownership moves to another child, duplicate/late completion, and abandonment of a live child are rejected by the shared production guards.
8. A rejected delegation settles only the exact matching pending `create_subtask` action. Settlement preserves status, lineage, and accounting, never clears a replacement or different-kind action, and never mutates a completed record. The settlement compare-and-clear reads the persisted record under the per-file lock, so it never clears from a stale host cache.
9. A completion clears the completing child's pending action only when the completion event carries the exact matching action ID. A completion with no action ID or a different ID preserves the pending action.
8. No transition may clear a delegated parent's link to a child that is active and live, where live means either marked live-elsewhere (`ModelState.liveElsewhere` true, history-file mtime inside the bounded freshness window) or heartbeat-alive (`ModelState.heartbeatAlive` true, fresh persisted `lastActivityAt` inside the same window); reconciliation repairs the link only when both flags are false. This encodes the PR #1495 cross-window misrepair bug class — which broke delegation links so subtask completion could not return to the parent — extended by the liveness-heartbeat fix so a child streaming a long turn (minutes without any other history-file write) is likewise preserved. The window is bounded in BOTH directions and symmetric: a timestamp less than the threshold away from `now` (including small future clock skew) is fresh, and a timestamp at or beyond the threshold in either direction (clock rollback, restored archive) is not evidence of life.
9. A crash-orphan repair writes the child half only. `reconcileStartup` may transition an active child to `interrupted`, and must leave its parent `delegated` with `awaitingChildId` and `delegatedToId` unchanged. The repair is therefore a single guarded write with no cross-file window: no peer resume, claim, or completion interleaving can leave a resumed child completing into an unlinked parent. The parent is reactivated only by the missing-child repair (child record gone), by child completion through `completeDelegatedChild`, or by re-delegation over the interrupted child.
10. A rejected delegation settles only the exact matching pending `create_subtask` action. Settlement preserves status, lineage, and accounting, never clears a replacement or different-kind action, and never mutates a completed record. The settlement compare-and-clear reads the persisted record under the per-file lock, so it never clears from a stale host cache.
11. A completion clears the completing child's pending action only when the completion event carries the exact matching action ID. A completion with no action ID or a different ID preserves the pending action.

The completion persistence checker additionally enforces:

Expand Down
1 change: 1 addition & 0 deletions packages/types/src/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const historyItemSchema = z.object({
awaitingChildId: z.string().optional(), // Child currently awaited (set when delegated)
completedByChildId: z.string().optional(), // Child that completed and resumed this parent
completionResultSummary: z.string().optional(), // Summary from completed child
lastActivityAt: z.number().optional(), // Liveness heartbeat: last time the owning session persisted activity
pendingAction: pendingTaskActionSchema.optional(),
})

Expand Down
Loading
Loading