From f4abbdee79850433ad75bfac874db4f49c9b8700 Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 13 Sep 2026 01:52:04 +0000 Subject: [PATCH 01/17] test(lifecycle): formalize remaining issue protocols --- docs/architecture/task-lifecycle-model.md | 23 +- package.json | 2 +- scripts/check-provider-handoff-scheduler.ts | 58 +++-- scripts/check-task-fanout-protocol.ts | 224 ++++++++++++++++++++ 4 files changed, 283 insertions(+), 24 deletions(-) create mode 100644 scripts/check-task-fanout-protocol.ts diff --git a/docs/architecture/task-lifecycle-model.md b/docs/architecture/task-lifecycle-model.md index 9266d49987..c2c0341d5c 100644 --- a/docs/architecture/task-lifecycle-model.md +++ b/docs/architecture/task-lifecycle-model.md @@ -6,14 +6,15 @@ Zoo Code checks task lifecycle protocols through one compositional verification pnpm lifecycle:model-check ``` -The command runs six independent bounded submodels in sequence: +The command runs seven independent bounded submodels in sequence: 1. the persisted task delegation lifecycle; 2. shared-store concurrency across task-history hosts; 3. production-backed provider handoff and scheduler ordering; -4. the task cleanup protocol; -5. request-stream parser scoping; and -6. completion persistence. +4. the task fan-out protocol; +5. the task cleanup protocol; +6. request-stream parser scoping; and +7. completion persistence. This umbrella command is the single model-check entry point in the `compile` CI job after type checking. Command-level composition does not merge the submodels' state spaces: each checker retains its own bounds, transitions, invariant ownership, reachability requirements, and counterexample format. In particular, parser state is not part of the persisted lifecycle graph. The focused parser checker remains directly runnable with `pnpm parser-scope:model-check` for debugging. @@ -88,6 +89,14 @@ Provider locking, paused-child/current-task publication, and semaphore admission Six injected legacy transition policies must produce deterministic shortest counterexamples through the same explorer: start before commit, resume before permit release, redelegation before permit release, empty current-task publication, two stale provider commits from competing snapshots, and releasing parent-transition serialization immediately after publication. The last witness must causally include first-child completion and parent publication, a second-child commit, release of the first child's scheduler permit, and then the stale first-child continuation. The checker prints the distinct reachable-state count, complete scenario/action/landmark coverage, bounds, and each named counterexample trace. It deliberately does not add a WAL, global profile projection, or scheduler state to persisted `HistoryItem` records. +For #921, the execution-context matrix also exhaustively combines saved, unsaved, and locked profile selection with both possible focused views before and after the immutable handoff snapshot. Focus changes are deliberately environment inputs rather than lifecycle state: every schedule must retain the selected task-local mode, profile identity, and cloned configuration while leaving the parent context unchanged. + +## Task fan-out protocol model + +`scripts/check-task-fanout-protocol.ts` is a separate bounded protocol model for the #369/#372 fan-out safety contract. It explores a live parent with two sibling slots, a two-permit scheduler, independent result readiness, explicit child-to-parent delivery, parent loss, orphan cancellation, and permit release. It checks scheduler capacity and exact permit ownership, one writer and at-most-once delivery per child, readiness before delivery, and no result routing after parent loss. Named landmarks require concurrent siblings beneath a live parent, out-of-order result delivery, parent loss while work is running, and complete orphan cleanup to remain reachable. Injected unsafe states confirm those invariants reject wrong writers, early and duplicate delivery, post-parent-loss routing, and scheduler over-allocation. + +This model verifies the intended composition boundary without claiming that concurrent sibling fan-out is enabled in production. `TaskScheduler` already provides bounded permits and guaranteed release, and completion APIs route by explicit parent and child IDs; focused tests cover those adapters. Raising production concurrency still requires live-parent result integration and extension-host coverage before fan-out can ship. Keeping this state space separate preserves the serial handoff checker's one-child ownership invariants. + ## Completion persistence model `scripts/check-completion-persistence.ts` models the completion-readiness protocol that protects the public `TaskCompleted` event. It starts from both standalone and delegated tasks and exhaustively interleaves: @@ -135,9 +144,9 @@ The following map separates issue observations from the architectural interpreta | [#1469](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1469): the issue report states that a barrier-controlled two-host run reproduced an old child completion clearing a newer handoff 25/25 times. | Completion is conditional on the parent still awaiting that exact child; a live-linked child must remain owned by its parent. | `completeDelegatedChild` rejects stale authoritative input. The lifecycle explorer checks that reducer rule, while the shared-store explorer reproduces the cross-host stale-cache counterexample with an exact causal witness. | | [#1021](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1021): an in-flight `saveClineMessages` can restore parent/root IDs after abandonment cleared them. | Detachment should be monotonic: later lifecycle work must not reattach an abandoned child. | `abandonDelegatedChild` clears both sides. The shared-store explorer proves the detach commit occurs, then reproduces a refreshed-cache delta that preserves interrupted status while restoring stale live-task lineage. | | [#1453](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1453), under user report [#1279](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1279): CI observed `TaskCompleted` before restart-visible API history once; 120 local repetitions did not reproduce it. | Completion implies restart-visible assistant history. Delayed or failed writes keep completion pending, and cancellation settles readiness without starting stale retries or emitting completion. | The completion persistence explorer checks the bounded event-ordering and cancellation contract for standalone and delegated tasks. Focused `Task` and `AttemptCompletionTool` tests cover the production adapter; `restart-persistence.test.ts` verifies visibility through a fresh extension host. | -| [#921](https://github.com/Zoo-Code-Org/Zoo-Code/issues/921): delegation across parallel tabs lacks coverage for different view-local mode/profile state. | Delegation must bind an explicit immutable execution-context snapshot rather than read whichever view is focused later. | The persisted ownership transition is covered; mode/profile snapshot isolation is outside this state model and belongs in a production adapter/model-based test. | +| [#921](https://github.com/Zoo-Code-Org/Zoo-Code/issues/921): delegation across parallel tabs lacks coverage for different view-local mode/profile state. | Delegation must bind an explicit immutable execution-context snapshot rather than read whichever view is focused later. | `selectHandoffExecutionContext` creates the production snapshot. The provider handoff checker exhaustively combines saved, unsaved, and locked profiles with focus changes before and after selection; focused selector and provider tests cover lookup fallback and child creation. | | [#920](https://github.com/Zoo-Code-Org/Zoo-Code/issues/920): issue analysis identifies a missing cross-instance history-update test and potential lost writes. | Distinct task writes must not overwrite one another, and same-task conflicts need an explicit merge/ownership rule. | The shared-store explorer checks distinct-task writes and same-record independent deltas. Cross-instance store tests retain production API coverage, and the synchronized real-filesystem smoke test exercises the actual lock/write path without claiming exhaustive filesystem proof. | -| [#369](https://github.com/Zoo-Code-Org/Zoo-Code/issues/369) and [#372](https://github.com/Zoo-Code-Org/Zoo-Code/issues/372): planned fan-out keeps a parent live while a child runs and requires completion routing by explicit parent ID, single-writer result readiness, permit release, and orphan cleanup. | Persisted `delegated` status is ownership, not proof that the parent instance is suspended. Completion must route by IDs; scheduler resources and live-instance state need separate invariants. | Nested and sibling lifecycle ownership are covered. Scheduler permits, live/suspended parent selection, orphan cancellation, and single-writer message readiness must be added when fan-out lands; they should not be folded into `HistoryItem` fields prematurely. | +| [#369](https://github.com/Zoo-Code-Org/Zoo-Code/issues/369) and [#372](https://github.com/Zoo-Code-Org/Zoo-Code/issues/372): planned fan-out keeps a parent live while a child runs and requires completion routing by explicit parent ID, single-writer result readiness, permit release, and orphan cleanup. | Persisted `delegated` status is ownership, not proof that the parent instance is suspended. Completion must route by IDs; scheduler resources and live-instance state need separate invariants. | The fan-out protocol checker models a live parent, two siblings, per-child permits/readiness, explicit delivery, parent loss, and orphan cleanup. `TaskScheduler` and ID-routed completion tests cover shipped primitives; production fan-out remains disabled pending live-parent integration and E2E. | | [#1468](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1468): a late chunk from one request combined tool identity with arguments from another request; rerun passed. | Every stream accumulator needs a request/task generation key, and late events cannot mutate another scope. | Separate protocol. The [native tool-call parser request-scope model](./native-tool-call-parser-scoping-model.md), whose source of truth is `scripts/check-native-tool-call-parser-scoping.ts`, exhaustively replays bounded production-parser interleavings without adding fields to this lifecycle model. | | [#612](https://github.com/Zoo-Code-Org/Zoo-Code/issues/612): the CLI copied a status union and omitted `interrupted`. | Lifecycle vocabulary should have one type owner. | `HistoryItemStatus` is derived from `HistoryItem`, and production/checker transitions share `taskLifecycle.ts`; consumers should import rather than copy the union. | @@ -153,7 +162,7 @@ When production lifecycle behavior changes: 4. Increase depth or task slots only when the new scenario requires it. Keep the state budget explicit and ensure CI completes quickly. 5. Convert any discovered counterexample into a focused production regression test as well as retaining the architectural invariant. -Completion-readiness changes belong in `scripts/check-completion-persistence.ts`; shared-store interleavings belong in `scripts/check-task-store-concurrency.ts`. Do not weaken bounds or remove an invariant merely to make CI pass. If state growth becomes difficult to control, split independent protocols or move the model to TLC/Quint with an implementation trace adapter rather than silently sampling the state space. +Completion-readiness changes belong in `scripts/check-completion-persistence.ts`; shared-store interleavings belong in `scripts/check-task-store-concurrency.ts`; fan-out permit, result-routing, and orphan-cleanup changes belong in `scripts/check-task-fanout-protocol.ts`. Do not weaken bounds or remove an invariant merely to make CI pass. If state growth becomes difficult to control, split independent protocols or move the model to TLC/Quint with an implementation trace adapter rather than silently sampling the state space. Parser request scoping is one such independent bounded submodel within the umbrella suite. Extend `scripts/check-native-tool-call-parser-scoping.ts` and its focused architecture document instead of adding parser state or transitions to `taskLifecycle.ts` or the persisted lifecycle state graph. diff --git a/package.json b/package.json index 1fd9ddc8fe..e534572620 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "check-types": "turbo check-types --log-order grouped --output-logs new-only", "test": "turbo test --log-order grouped --output-logs new-only", "test:mutation-ci": "node --test scripts/stryker-diff.test.mjs", - "lifecycle:model-check": "tsx scripts/check-task-lifecycle.ts && tsx scripts/check-task-store-concurrency.ts && tsx scripts/check-provider-handoff-scheduler.ts && pnpm cleanup-protocol:model-check && pnpm parser-scope:model-check && tsx scripts/check-completion-persistence.ts", + "lifecycle:model-check": "tsx scripts/check-task-lifecycle.ts && tsx scripts/check-task-store-concurrency.ts && tsx scripts/check-provider-handoff-scheduler.ts && tsx scripts/check-task-fanout-protocol.ts && pnpm cleanup-protocol:model-check && pnpm parser-scope:model-check && tsx scripts/check-completion-persistence.ts", "cleanup-protocol:model-check": "tsx scripts/check-task-cleanup-protocol.ts", "parser-scope:model-check": "node scripts/run-native-tool-call-parser-scoping.mjs", "mcp:integration-check": "tsx scripts/check-mcp-oauth-integration.ts", diff --git a/scripts/check-provider-handoff-scheduler.ts b/scripts/check-provider-handoff-scheduler.ts index 3fbba11b35..8eebf865ce 100644 --- a/scripts/check-provider-handoff-scheduler.ts +++ b/scripts/check-provider-handoff-scheduler.ts @@ -111,6 +111,11 @@ const LEGACY_POLICIES: Array = [ const parentConfiguration: ProviderSettings = { apiProvider: "anthropic", consecutiveMistakeLimit: 3 } const savedConfiguration: ProviderSettings = { apiProvider: "openrouter", consecutiveMistakeLimit: 7 } const parentContext = { mode: "code", apiConfigName: undefined, apiConfiguration: parentConfiguration } +const otherViewContext = { + mode: "debug", + apiConfigName: "other-view", + apiConfiguration: { apiProvider: "openai", consecutiveMistakeLimit: 99 } satisfies ProviderSettings, +} const PROFILE_SCENARIOS = [ { name: "unsaved", locked: false, saved: undefined, expectedName: undefined, expectedLimit: 3 }, { @@ -129,22 +134,43 @@ const PROFILE_SCENARIOS = [ }, ] as const +const FOCUSED_VIEWS = ["parent", "other"] as const for (const scenario of PROFILE_SCENARIOS) { - const selected = selectHandoffExecutionContext( - parentContext, - "ask", - parentContext.mode, - scenario.locked, - scenario.saved, - ) - assert.equal(selected.mode, "ask", `${scenario.name}: requested mode must remain task-local`) - assert.equal(selected.apiConfigName, scenario.expectedName, `${scenario.name}: profile identity`) - assert.equal( - selected.apiConfiguration.consecutiveMistakeLimit, - scenario.expectedLimit, - `${scenario.name}: profile config`, - ) - assert.equal(parentContext.apiConfiguration.consecutiveMistakeLimit, 3, `${scenario.name}: parent context mutated`) + for (const focusedBefore of FOCUSED_VIEWS) { + const viewContexts = { parent: structuredClone(parentContext), other: structuredClone(otherViewContext) } + const taskContext = viewContexts.parent + assert.equal( + viewContexts[focusedBefore].apiConfigName, + focusedBefore === "parent" ? undefined : "other-view", + `${scenario.name}/${focusedBefore}: focused view setup`, + ) + const selected = selectHandoffExecutionContext( + taskContext, + "ask", + taskContext.mode, + scenario.locked, + scenario.saved, + ) + for (const focusedAfter of FOCUSED_VIEWS) { + viewContexts[focusedAfter].apiConfiguration.consecutiveMistakeLimit = 101 + assert.equal(selected.mode, "ask", `${scenario.name}/${focusedBefore}->${focusedAfter}: task-local mode`) + assert.equal( + selected.apiConfigName, + scenario.expectedName, + `${scenario.name}/${focusedBefore}->${focusedAfter}: profile identity`, + ) + assert.equal( + selected.apiConfiguration.consecutiveMistakeLimit, + scenario.expectedLimit, + `${scenario.name}/${focusedBefore}->${focusedAfter}: profile config`, + ) + } + assert.equal( + parentContext.apiConfiguration.consecutiveMistakeLimit, + 3, + `${scenario.name}: parent context mutated`, + ) + } } const fixed = explore(FIXED_POLICY, false) @@ -156,7 +182,7 @@ const counterexamples = LEGACY_POLICIES.map((policy) => { }) console.log( - `Provider handoff/scheduler model check passed: ${fixed.states} distinct reachable states, ${PROFILE_SCENARIOS.length}/${PROFILE_SCENARIOS.length} profile scenarios, ${fixed.actions.size}/${EXPECTED_ACTIONS.length} actions, ${fixed.landmarks.size}/${Object.keys(LANDMARKS).length} landmarks, depth <= ${MAX_DEPTH}, states <= ${MAX_STATES}, ${counterexamples.length}/${LEGACY_POLICIES.length} legacy counterexamples`, + `Provider handoff/scheduler model check passed: ${fixed.states} distinct reachable states, ${PROFILE_SCENARIOS.length * FOCUSED_VIEWS.length * FOCUSED_VIEWS.length}/${PROFILE_SCENARIOS.length * FOCUSED_VIEWS.length * FOCUSED_VIEWS.length} profile/focus schedules, ${fixed.actions.size}/${EXPECTED_ACTIONS.length} actions, ${fixed.landmarks.size}/${Object.keys(LANDMARKS).length} landmarks, depth <= ${MAX_DEPTH}, states <= ${MAX_STATES}, ${counterexamples.length}/${LEGACY_POLICIES.length} legacy counterexamples`, ) for (const counterexample of counterexamples) { console.log( diff --git a/scripts/check-task-fanout-protocol.ts b/scripts/check-task-fanout-protocol.ts new file mode 100644 index 0000000000..3ae0b4ab96 --- /dev/null +++ b/scripts/check-task-fanout-protocol.ts @@ -0,0 +1,224 @@ +import assert from "node:assert/strict" + +const CHILDREN = ["a", "b"] as const +type Child = (typeof CHILDREN)[number] +type ChildState = "idle" | "running" | "ready" | "delivered" | "cancelled" + +type ModelState = { + parentLive: boolean + children: Record + permitOwners: Child[] + resultWriters: Partial> + deliveries: Child[] + deliveryAfterParentLoss: boolean +} + +type Transition = { name: string; kind: string; next: ModelState } +type TraceStep = { action: string; state: ModelState } + +const MAX_DEPTH = 10 +const MAX_STATES = 500 +const EXPECTED_ACTIONS = ["launch", "finish", "deliver", "lose-parent", "cancel-orphan", "release"] as const +const LANDMARKS = { + "live-parent-with-two-children": (state: ModelState) => + state.parentLive && CHILDREN.every((child) => state.children[child] === "running"), + "out-of-order-results": (state: ModelState) => state.deliveries.join(",") === "b,a", + "single-writer-results": (state: ModelState) => + CHILDREN.every((child) => state.resultWriters[child] === undefined || state.resultWriters[child] === child), + "parent-loss-with-running-child": (state: ModelState) => + !state.parentLive && CHILDREN.some((child) => state.children[child] === "running"), + "orphan-cleanup": (state: ModelState) => + !state.parentLive && CHILDREN.every((child) => !["running", "ready"].includes(state.children[child])), +} satisfies Record boolean> + +const start = initialState() +const queue: Array<{ state: ModelState; trace: TraceStep[] }> = [ + { state: start, trace: [{ action: "initial", state: start }] }, +] +const visited = new Set([canonical(start)]) +const actions = new Set() +const landmarks = new Set() +const frontier: ModelState[] = [] + +const KNOWN_BAD_STATES: Array<{ name: string; state: ModelState; expected: string }> = [ + { + name: "wrong-result-writer", + state: { + ...initialState(), + children: { a: "ready", b: "idle" }, + permitOwners: ["a"], + resultWriters: { a: "b" }, + }, + expected: "a: result has the wrong writer", + }, + { + name: "early-delivery", + state: { ...initialState(), children: { a: "delivered", b: "idle" }, deliveries: ["a"] }, + expected: "a: result delivered before readiness", + }, + { + name: "duplicate-delivery", + state: { + ...initialState(), + children: { a: "delivered", b: "idle" }, + resultWriters: { a: "a" }, + deliveries: ["a", "a"], + }, + expected: "a: result delivered more than once", + }, + { + name: "post-parent-loss-delivery", + state: { ...initialState(), parentLive: false, deliveryAfterParentLoss: true }, + expected: "result routed after parent loss", + }, + { + name: "scheduler-over-allocation", + state: { ...initialState(), permitOwners: ["a", "b", "a"] }, + expected: "scheduler capacity exceeded", + }, +] + +for (const unsafe of KNOWN_BAD_STATES) { + assert.ok(invariantViolations(unsafe.state).includes(unsafe.expected), `${unsafe.name}: invariant did not fire`) +} + +for (let index = 0; index < queue.length; index++) { + const node = queue[index]! + for (const [name, predicate] of Object.entries(LANDMARKS)) { + if (predicate(node.state)) landmarks.add(name) + } + const violations = invariantViolations(node.state) + assert.deepEqual(violations, [], formatViolation(violations, node.trace)) + if (node.trace.length - 1 === MAX_DEPTH) { + frontier.push(node.state) + continue + } + + for (const transition of transitions(node.state)) { + actions.add(transition.kind) + const trace = [...node.trace, { action: transition.name, state: transition.next }] + const nextViolations = invariantViolations(transition.next) + assert.deepEqual(nextViolations, [], formatViolation(nextViolations, trace)) + const key = canonical(transition.next) + if (visited.has(key)) continue + visited.add(key) + queue.push({ state: transition.next, trace }) + assert.ok(visited.size <= MAX_STATES, `exceeded ${MAX_STATES}-state budget`) + } +} + +const missingActions = EXPECTED_ACTIONS.filter((action) => !actions.has(action)) +assert.deepEqual(missingActions, [], `unreachable actions: ${missingActions.join(", ")}`) +const missingLandmarks = Object.keys(LANDMARKS).filter((name) => !landmarks.has(name)) +assert.deepEqual(missingLandmarks, [], `unreachable landmarks: ${missingLandmarks.join(", ")}`) +const unseen = frontier.flatMap(transitions).find(({ next }) => !visited.has(canonical(next))) +assert.equal(unseen, undefined, `depth ${MAX_DEPTH} has unseen successor ${unseen?.name}`) + +console.log( + `Task fan-out protocol model check passed: ${visited.size} distinct reachable states, ${actions.size}/${EXPECTED_ACTIONS.length} actions, ${landmarks.size}/${Object.keys(LANDMARKS).length} landmarks, ${KNOWN_BAD_STATES.length}/${KNOWN_BAD_STATES.length} unsafe counterexamples, depth <= ${MAX_DEPTH}, states <= ${MAX_STATES}`, +) + +function transitions(state: ModelState): Transition[] { + const result: Transition[] = [] + for (const child of CHILDREN) { + if (state.parentLive && state.children[child] === "idle" && state.permitOwners.length < 2) { + result.push( + action(`launch(${child})`, "launch", state, (next) => { + next.children[child] = "running" + next.permitOwners.push(child) + }), + ) + } + if (state.children[child] === "running") { + result.push( + action(`finish(${child})`, "finish", state, (next) => { + next.children[child] = "ready" + next.resultWriters[child] = child + }), + ) + } + if (state.parentLive && state.children[child] === "ready" && state.resultWriters[child] === child) { + result.push( + action(`deliver(${child}, parent)`, "deliver", state, (next) => { + next.children[child] = "delivered" + next.deliveries.push(child) + }), + ) + } + if (!state.parentLive && ["running", "ready"].includes(state.children[child])) { + result.push( + action(`cancel-orphan(${child})`, "cancel-orphan", state, (next) => { + next.children[child] = "cancelled" + }), + ) + } + if (state.permitOwners.includes(child) && ["delivered", "cancelled"].includes(state.children[child])) { + result.push( + action(`release(${child})`, "release", state, (next) => { + next.permitOwners = next.permitOwners.filter((owner) => owner !== child) + }), + ) + } + } + if (state.parentLive && CHILDREN.some((child) => state.children[child] !== "idle")) { + result.push( + action("lose-parent", "lose-parent", state, (next) => { + next.parentLive = false + }), + ) + } + return result +} + +function invariantViolations(state: ModelState): string[] { + const violations: string[] = [] + if (new Set(state.permitOwners).size !== state.permitOwners.length) violations.push("duplicate permit owner") + if (state.permitOwners.length > 2) violations.push("scheduler capacity exceeded") + if (state.deliveryAfterParentLoss) violations.push("result routed after parent loss") + for (const child of CHILDREN) { + const active = ["running", "ready"].includes(state.children[child]) + if (active && !state.permitOwners.includes(child)) violations.push(`${child}: active without permit ownership`) + if (state.children[child] === "idle" && state.permitOwners.includes(child)) { + violations.push(`${child}: idle child owns a permit`) + } + if (state.resultWriters[child] !== undefined && state.resultWriters[child] !== child) { + violations.push(`${child}: result has the wrong writer`) + } + if (state.deliveries.filter((delivered) => delivered === child).length > 1) { + violations.push(`${child}: result delivered more than once`) + } + if (state.children[child] === "delivered" && state.resultWriters[child] !== child) { + violations.push(`${child}: result delivered before readiness`) + } + } + return violations +} + +function initialState(): ModelState { + return { + parentLive: true, + children: { a: "idle", b: "idle" }, + permitOwners: [], + resultWriters: {}, + deliveries: [], + deliveryAfterParentLoss: false, + } +} + +function action(name: string, kind: string, state: ModelState, update: (next: ModelState) => void): Transition { + const next = structuredClone(state) + update(next) + return { name, kind, next } +} + +function canonical(state: ModelState): string { + return JSON.stringify({ ...state, permitOwners: [...state.permitOwners].sort() }) +} + +function formatViolation(violations: string[], trace: TraceStep[]): string { + return [ + violations.join("; "), + `Bounds: depth=${MAX_DEPTH}, states=${MAX_STATES}`, + ...trace.map((step, index) => `${index}. ${step.action}\n ${canonical(step.state)}`), + ].join("\n") +} From fed91bb86aab589c93f12e5fb5755b8ca36d629f Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 13 Sep 2026 01:56:15 +0000 Subject: [PATCH 02/17] docs(lifecycle): qualify delegated mode coverage --- docs/architecture/task-lifecycle-model.md | 24 +++---- scripts/check-provider-handoff-scheduler.ts | 70 +++++++++------------ 2 files changed, 41 insertions(+), 53 deletions(-) diff --git a/docs/architecture/task-lifecycle-model.md b/docs/architecture/task-lifecycle-model.md index c2c0341d5c..21d931f1d9 100644 --- a/docs/architecture/task-lifecycle-model.md +++ b/docs/architecture/task-lifecycle-model.md @@ -89,7 +89,9 @@ Provider locking, paused-child/current-task publication, and semaphore admission Six injected legacy transition policies must produce deterministic shortest counterexamples through the same explorer: start before commit, resume before permit release, redelegation before permit release, empty current-task publication, two stale provider commits from competing snapshots, and releasing parent-transition serialization immediately after publication. The last witness must causally include first-child completion and parent publication, a second-child commit, release of the first child's scheduler permit, and then the stale first-child continuation. The checker prints the distinct reachable-state count, complete scenario/action/landmark coverage, bounds, and each named counterexample trace. It deliberately does not add a WAL, global profile projection, or scheduler state to persisted `HistoryItem` records. -For #921, the execution-context matrix also exhaustively combines saved, unsaved, and locked profile selection with both possible focused views before and after the immutable handoff snapshot. Focus changes are deliberately environment inputs rather than lifecycle state: every schedule must retain the selected task-local mode, profile identity, and cloned configuration while leaving the parent context unchanged. +For #921, the execution-context matrix verifies saved, unsaved, and locked profile selection at the handoff boundary. This proves only that delegation writes the requested task-local mode and cloned configuration into the child context. It does not prove that every downstream consumer reads that context. The checker retains a divergent-mode witness in which the child task mode differs from the shared provider mode so reader refinements can demonstrate that choosing the wrong source is observable. + +Issue #1623 exposed that missing refinement: `getEnvironmentDetails`, `presentAssistantMessage` tool validation, and custom-tool execution still read shared provider mode after the handoff snapshot became task-local. PR #1625 owns the production fix and adds a dedicated delegated-mode reader check plus focused adapter tests. Until that runtime PR is merged, the selector model establishes the write-side premise and the divergent-mode witness traces the unresolved read-side obligation; it must not be cited as complete #921 coverage by itself. ## Task fan-out protocol model @@ -139,16 +141,16 @@ These are safety claims within the documented bounds. The checks do not claim li The following map separates issue observations from the architectural interpretation encoded here. Open issues can change after this document is written; follow each link for current status. -| Issue and directly observed evidence | Derived protocol rule | Production transition and current check | -| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [#1469](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1469): the issue report states that a barrier-controlled two-host run reproduced an old child completion clearing a newer handoff 25/25 times. | Completion is conditional on the parent still awaiting that exact child; a live-linked child must remain owned by its parent. | `completeDelegatedChild` rejects stale authoritative input. The lifecycle explorer checks that reducer rule, while the shared-store explorer reproduces the cross-host stale-cache counterexample with an exact causal witness. | -| [#1021](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1021): an in-flight `saveClineMessages` can restore parent/root IDs after abandonment cleared them. | Detachment should be monotonic: later lifecycle work must not reattach an abandoned child. | `abandonDelegatedChild` clears both sides. The shared-store explorer proves the detach commit occurs, then reproduces a refreshed-cache delta that preserves interrupted status while restoring stale live-task lineage. | -| [#1453](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1453), under user report [#1279](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1279): CI observed `TaskCompleted` before restart-visible API history once; 120 local repetitions did not reproduce it. | Completion implies restart-visible assistant history. Delayed or failed writes keep completion pending, and cancellation settles readiness without starting stale retries or emitting completion. | The completion persistence explorer checks the bounded event-ordering and cancellation contract for standalone and delegated tasks. Focused `Task` and `AttemptCompletionTool` tests cover the production adapter; `restart-persistence.test.ts` verifies visibility through a fresh extension host. | -| [#921](https://github.com/Zoo-Code-Org/Zoo-Code/issues/921): delegation across parallel tabs lacks coverage for different view-local mode/profile state. | Delegation must bind an explicit immutable execution-context snapshot rather than read whichever view is focused later. | `selectHandoffExecutionContext` creates the production snapshot. The provider handoff checker exhaustively combines saved, unsaved, and locked profiles with focus changes before and after selection; focused selector and provider tests cover lookup fallback and child creation. | -| [#920](https://github.com/Zoo-Code-Org/Zoo-Code/issues/920): issue analysis identifies a missing cross-instance history-update test and potential lost writes. | Distinct task writes must not overwrite one another, and same-task conflicts need an explicit merge/ownership rule. | The shared-store explorer checks distinct-task writes and same-record independent deltas. Cross-instance store tests retain production API coverage, and the synchronized real-filesystem smoke test exercises the actual lock/write path without claiming exhaustive filesystem proof. | -| [#369](https://github.com/Zoo-Code-Org/Zoo-Code/issues/369) and [#372](https://github.com/Zoo-Code-Org/Zoo-Code/issues/372): planned fan-out keeps a parent live while a child runs and requires completion routing by explicit parent ID, single-writer result readiness, permit release, and orphan cleanup. | Persisted `delegated` status is ownership, not proof that the parent instance is suspended. Completion must route by IDs; scheduler resources and live-instance state need separate invariants. | The fan-out protocol checker models a live parent, two siblings, per-child permits/readiness, explicit delivery, parent loss, and orphan cleanup. `TaskScheduler` and ID-routed completion tests cover shipped primitives; production fan-out remains disabled pending live-parent integration and E2E. | -| [#1468](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1468): a late chunk from one request combined tool identity with arguments from another request; rerun passed. | Every stream accumulator needs a request/task generation key, and late events cannot mutate another scope. | Separate protocol. The [native tool-call parser request-scope model](./native-tool-call-parser-scoping-model.md), whose source of truth is `scripts/check-native-tool-call-parser-scoping.ts`, exhaustively replays bounded production-parser interleavings without adding fields to this lifecycle model. | -| [#612](https://github.com/Zoo-Code-Org/Zoo-Code/issues/612): the CLI copied a status union and omitted `interrupted`. | Lifecycle vocabulary should have one type owner. | `HistoryItemStatus` is derived from `HistoryItem`, and production/checker transitions share `taskLifecycle.ts`; consumers should import rather than copy the union. | +| Issue and directly observed evidence | Derived protocol rule | Production transition and current check | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [#1469](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1469): the issue report states that a barrier-controlled two-host run reproduced an old child completion clearing a newer handoff 25/25 times. | Completion is conditional on the parent still awaiting that exact child; a live-linked child must remain owned by its parent. | `completeDelegatedChild` rejects stale authoritative input. The lifecycle explorer checks that reducer rule, while the shared-store explorer reproduces the cross-host stale-cache counterexample with an exact causal witness. | +| [#1021](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1021): an in-flight `saveClineMessages` can restore parent/root IDs after abandonment cleared them. | Detachment should be monotonic: later lifecycle work must not reattach an abandoned child. | `abandonDelegatedChild` clears both sides. The shared-store explorer proves the detach commit occurs, then reproduces a refreshed-cache delta that preserves interrupted status while restoring stale live-task lineage. | +| [#1453](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1453), under user report [#1279](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1279): CI observed `TaskCompleted` before restart-visible API history once; 120 local repetitions did not reproduce it. | Completion implies restart-visible assistant history. Delayed or failed writes keep completion pending, and cancellation settles readiness without starting stale retries or emitting completion. | The completion persistence explorer checks the bounded event-ordering and cancellation contract for standalone and delegated tasks. Focused `Task` and `AttemptCompletionTool` tests cover the production adapter; `restart-persistence.test.ts` verifies visibility through a fresh extension host. | +| [#921](https://github.com/Zoo-Code-Org/Zoo-Code/issues/921): delegation across parallel tabs lacks coverage for different view-local mode/profile state. Follow-up [#1623](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1623) proves a correct snapshot is insufficient when downstream readers use shared provider mode. | Delegation must bind an explicit immutable execution-context snapshot, and every mode-sensitive consumer must read the child task's context rather than whichever provider/view is focused. | This checker proves the write-side selector matrix and retains a divergent task/provider-mode witness. Runtime reader correctness is intentionally owned by [PR #1625](https://github.com/Zoo-Code-Org/Zoo-Code/pull/1625), its delegated-mode reader check, and focused environment/tool tests. | +| [#920](https://github.com/Zoo-Code-Org/Zoo-Code/issues/920): issue analysis identifies a missing cross-instance history-update test and potential lost writes. | Distinct task writes must not overwrite one another, and same-task conflicts need an explicit merge/ownership rule. | The shared-store explorer checks distinct-task writes and same-record independent deltas. Cross-instance store tests retain production API coverage, and the synchronized real-filesystem smoke test exercises the actual lock/write path without claiming exhaustive filesystem proof. | +| [#369](https://github.com/Zoo-Code-Org/Zoo-Code/issues/369) and [#372](https://github.com/Zoo-Code-Org/Zoo-Code/issues/372): planned fan-out keeps a parent live while a child runs and requires completion routing by explicit parent ID, single-writer result readiness, permit release, and orphan cleanup. | Persisted `delegated` status is ownership, not proof that the parent instance is suspended. Completion must route by IDs; scheduler resources and live-instance state need separate invariants. | The fan-out protocol checker models a live parent, two siblings, per-child permits/readiness, explicit delivery, parent loss, and orphan cleanup. `TaskScheduler` and ID-routed completion tests cover shipped primitives; production fan-out remains disabled pending live-parent integration and E2E. | +| [#1468](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1468): a late chunk from one request combined tool identity with arguments from another request; rerun passed. | Every stream accumulator needs a request/task generation key, and late events cannot mutate another scope. | Separate protocol. The [native tool-call parser request-scope model](./native-tool-call-parser-scoping-model.md), whose source of truth is `scripts/check-native-tool-call-parser-scoping.ts`, exhaustively replays bounded production-parser interleavings without adding fields to this lifecycle model. | +| [#612](https://github.com/Zoo-Code-Org/Zoo-Code/issues/612): the CLI copied a status union and omitted `interrupted`. | Lifecycle vocabulary should have one type owner. | `HistoryItemStatus` is derived from `HistoryItem`, and production/checker transitions share `taskLifecycle.ts`; consumers should import rather than copy the union. | The issue-derived cases intentionally map to bug classes rather than issue-specific flags. In particular, stale event ownership, monotonic terminal/detached state, explicit scope, and single-writer boundaries generalize to future concurrent task work. diff --git a/scripts/check-provider-handoff-scheduler.ts b/scripts/check-provider-handoff-scheduler.ts index 8eebf865ce..529643e5c0 100644 --- a/scripts/check-provider-handoff-scheduler.ts +++ b/scripts/check-provider-handoff-scheduler.ts @@ -111,11 +111,6 @@ const LEGACY_POLICIES: Array = [ const parentConfiguration: ProviderSettings = { apiProvider: "anthropic", consecutiveMistakeLimit: 3 } const savedConfiguration: ProviderSettings = { apiProvider: "openrouter", consecutiveMistakeLimit: 7 } const parentContext = { mode: "code", apiConfigName: undefined, apiConfiguration: parentConfiguration } -const otherViewContext = { - mode: "debug", - apiConfigName: "other-view", - apiConfiguration: { apiProvider: "openai", consecutiveMistakeLimit: 99 } satisfies ProviderSettings, -} const PROFILE_SCENARIOS = [ { name: "unsaved", locked: false, saved: undefined, expectedName: undefined, expectedLimit: 3 }, { @@ -134,45 +129,36 @@ const PROFILE_SCENARIOS = [ }, ] as const -const FOCUSED_VIEWS = ["parent", "other"] as const for (const scenario of PROFILE_SCENARIOS) { - for (const focusedBefore of FOCUSED_VIEWS) { - const viewContexts = { parent: structuredClone(parentContext), other: structuredClone(otherViewContext) } - const taskContext = viewContexts.parent - assert.equal( - viewContexts[focusedBefore].apiConfigName, - focusedBefore === "parent" ? undefined : "other-view", - `${scenario.name}/${focusedBefore}: focused view setup`, - ) - const selected = selectHandoffExecutionContext( - taskContext, - "ask", - taskContext.mode, - scenario.locked, - scenario.saved, - ) - for (const focusedAfter of FOCUSED_VIEWS) { - viewContexts[focusedAfter].apiConfiguration.consecutiveMistakeLimit = 101 - assert.equal(selected.mode, "ask", `${scenario.name}/${focusedBefore}->${focusedAfter}: task-local mode`) - assert.equal( - selected.apiConfigName, - scenario.expectedName, - `${scenario.name}/${focusedBefore}->${focusedAfter}: profile identity`, - ) - assert.equal( - selected.apiConfiguration.consecutiveMistakeLimit, - scenario.expectedLimit, - `${scenario.name}/${focusedBefore}->${focusedAfter}: profile config`, - ) - } - assert.equal( - parentContext.apiConfiguration.consecutiveMistakeLimit, - 3, - `${scenario.name}: parent context mutated`, - ) - } + const selected = selectHandoffExecutionContext( + parentContext, + "ask", + parentContext.mode, + scenario.locked, + scenario.saved, + ) + assert.equal(selected.mode, "ask", `${scenario.name}: requested mode must remain task-local`) + assert.equal(selected.apiConfigName, scenario.expectedName, `${scenario.name}: profile identity`) + assert.equal( + selected.apiConfiguration.consecutiveMistakeLimit, + scenario.expectedLimit, + `${scenario.name}: profile config`, + ) + assert.equal(parentContext.apiConfiguration.consecutiveMistakeLimit, 3, `${scenario.name}: parent context mutated`) } +const downstreamConsumerWitness = selectHandoffExecutionContext( + { ...parentContext, mode: "orchestrator" }, + "code", + "orchestrator", + false, +) +assert.notEqual( + downstreamConsumerWitness.mode, + "orchestrator", + "#921/#1623 witness requires task-local and shared provider modes to diverge", +) + const fixed = explore(FIXED_POLICY, false) const counterexamples = LEGACY_POLICIES.map((policy) => { const result = explore(policy, true) @@ -182,7 +168,7 @@ const counterexamples = LEGACY_POLICIES.map((policy) => { }) console.log( - `Provider handoff/scheduler model check passed: ${fixed.states} distinct reachable states, ${PROFILE_SCENARIOS.length * FOCUSED_VIEWS.length * FOCUSED_VIEWS.length}/${PROFILE_SCENARIOS.length * FOCUSED_VIEWS.length * FOCUSED_VIEWS.length} profile/focus schedules, ${fixed.actions.size}/${EXPECTED_ACTIONS.length} actions, ${fixed.landmarks.size}/${Object.keys(LANDMARKS).length} landmarks, depth <= ${MAX_DEPTH}, states <= ${MAX_STATES}, ${counterexamples.length}/${LEGACY_POLICIES.length} legacy counterexamples`, + `Provider handoff/scheduler model check passed: ${fixed.states} distinct reachable states, ${PROFILE_SCENARIOS.length}/${PROFILE_SCENARIOS.length} profile scenarios, 1/1 downstream shared-mode witness, ${fixed.actions.size}/${EXPECTED_ACTIONS.length} actions, ${fixed.landmarks.size}/${Object.keys(LANDMARKS).length} landmarks, depth <= ${MAX_DEPTH}, states <= ${MAX_STATES}, ${counterexamples.length}/${LEGACY_POLICIES.length} legacy counterexamples`, ) for (const counterexample of counterexamples) { console.log( From 70d7ac60a8d8b77b150c6d8252d37cb1e43b34a4 Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 13 Sep 2026 02:12:00 +0000 Subject: [PATCH 03/17] docs(lifecycle): classify verification coverage --- README.md | 3 + .../native-tool-call-parser-scoping-model.md | 2 +- .../task-cleanup-protocol-model.md | 2 + docs/architecture/task-lifecycle-model.md | 69 ++++++++++++++----- 4 files changed, 57 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index cce7fa9cf3..8b898d11df 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,9 @@ Learn more: [Using Modes](https://docs.zoocode.dev/basic-usage/using-modes) • - **[Documentation](https://docs.zoocode.dev):** The official guide to installing, configuring, and mastering Zoo Code. +- **[Task lifecycle architecture and issue traceability](docs/architecture/task-lifecycle-model.md#open-issue-traceability):** + Bounded model checks, production mappings, known-unsafe witnesses, and open + lifecycle obligations. - **[Discord Server](https://discord.gg/VxfP4Vx3gX):** Join the community for real-time help and discussion. - **[Reddit Community](https://www.reddit.com/r/ZooCode/):** Share your diff --git a/docs/architecture/native-tool-call-parser-scoping-model.md b/docs/architecture/native-tool-call-parser-scoping-model.md index ba7fbedd29..6533b8038f 100644 --- a/docs/architecture/native-tool-call-parser-scoping-model.md +++ b/docs/architecture/native-tool-call-parser-scoping-model.md @@ -12,7 +12,7 @@ For focused debugging, run this submodel directly with: pnpm parser-scope:model-check ``` -The command is composed into the same verification suite, but this remains a separate protocol and state space from the persisted task lifecycle model and shared-store concurrency model. It owns its parser-scoping invariants and adds no parser state to `HistoryItem` or `taskLifecycle.ts`; instead, it replays the public production `NativeToolCallParser` APIs using two independent scope objects. +The command runs this check sequentially with the other lifecycle checks, but this remains a separate protocol and state space from the persisted task lifecycle model and shared-store concurrency model. It owns its parser-scoping invariants and adds no parser state to `HistoryItem` or `taskLifecycle.ts`; instead, it replays the public production `NativeToolCallParser` APIs using two independent scope objects. The authoritative [lifecycle coverage audit and issue tracker](./task-lifecycle-model.md#coverage-audit) records its evidence class and cross-model limits. ## Bounds and replay diff --git a/docs/architecture/task-cleanup-protocol-model.md b/docs/architecture/task-cleanup-protocol-model.md index e46991365b..bd401ff159 100644 --- a/docs/architecture/task-cleanup-protocol-model.md +++ b/docs/architecture/task-cleanup-protocol-model.md @@ -14,6 +14,8 @@ pnpm cleanup-protocol:model-check This is a separate child model from the persisted task lifecycle and shared-store concurrency models. It follows the native tool-call parser model pattern: keep an independent bounded state space for an independent protocol, require every action and semantic landmark to remain reachable, and connect the abstract claims to focused production tests. +The authoritative [lifecycle coverage audit and issue tracker](./task-lifecycle-model.md#coverage-audit) classifies how this abstract model relates to production and other submodels. + ## Bounds and environment actions The model uses two tasks and explores every reachable interleaving through depth 20, with an explicit 100,000-state budget. Abort, disposal, final-save, provider abort/drain phases, and shutdown-cursor state are modeled directly. Independent abort and disposal calls may interleave freely, while provider-initiated calls are gated to the current shutdown task. Cleanup and editor-reversion settlement or rejection are environment actions, so the explorer does not assume they eventually occur. diff --git a/docs/architecture/task-lifecycle-model.md b/docs/architecture/task-lifecycle-model.md index 21d931f1d9..2981640784 100644 --- a/docs/architecture/task-lifecycle-model.md +++ b/docs/architecture/task-lifecycle-model.md @@ -1,6 +1,6 @@ # Task lifecycle model-check suite -Zoo Code checks task lifecycle protocols through one compositional verification suite. Run the complete suite locally with: +Zoo Code checks task lifecycle protocols through one umbrella command for independent bounded checks. Run the complete suite locally with: ```sh pnpm lifecycle:model-check @@ -10,7 +10,7 @@ The command runs seven independent bounded submodels in sequence: 1. the persisted task delegation lifecycle; 2. shared-store concurrency across task-history hosts; -3. production-backed provider handoff and scheduler ordering; +3. production-backed handoff reducers with an abstract provider/scheduler protocol; 4. the task fan-out protocol; 5. the task cleanup protocol; 6. request-stream parser scoping; and @@ -20,6 +20,8 @@ This umbrella command is the single model-check entry point in the `compile` CI An individual checker fails if it finds an invariant violation, a modeled action becomes unreachable, a named semantic landmark disappears, or exploration exceeds its declared state budget. A lifecycle violation includes the shortest breadth-first event trace, every intermediate state, and the active bounds so the sequence can be replayed as a focused regression test. +A checker printing `passed` means only that its configured bounded invariants, expected witnesses, reachability requirements, and state budget succeeded. It does not close a linked issue, prove arbitrary-task correctness, or establish refinement for production consumers that the checker does not execute. + Executable cross-model composition should be added only when a correctness claim genuinely spans two or more submodels and there is an explicit, production-grounded boundary mapping between their events or state. That composition must state a bounded joint exploration strategy and own cross-model invariants that cannot be proved within either child model alone. Shared command orchestration or conceptual adjacency is not sufficient reason to multiply independent state spaces. ## Why an executable TypeScript model @@ -64,7 +66,7 @@ The same `pnpm lifecycle:model-check` command also runs a second bounded explore - successful pair-operation cache entries publish together after both file writes; if the second write fails, the cache publishes only the first committed record; - cache refresh is explicit and may occur after an external live-task snapshot was captured. -There is no production record version or compare-and-swap token today. The model therefore does not invent one. It universally checks host-mutex and file-lock ownership, whole-file delta rejection, disk-field preservation, `childIds` union, and pair write order. Six scenarios, including distinct-task writes from #920 and a second-write pair failure, and all seven phases (`read`, `prepare`, `revalidate`, `commit`, `refresh`, `reject`, and `fail`) must remain reachable without exceeding the state/depth budgets. Positive semantic landmarks additionally require a stale cache beside newer disk state, the first pair write committed while the second is pending, and the same committed prefix retained after the second write fails. +There is no production record version or compare-and-swap token today. The model therefore does not invent one. It checks host-mutex and file-lock ownership, whole-file delta rejection, disk-field preservation, `childIds` union, and pair write order in every state reachable within six bounded scenarios. Those scenarios include distinct-task writes from #920 and a second-write pair failure, and all seven phases (`read`, `prepare`, `revalidate`, `commit`, `refresh`, `reject`, and `fail`) must remain reachable without exceeding the state/depth budgets. Positive semantic landmarks additionally require a stale cache beside newer disk state, the first pair write committed while the second is pending, and the same committed prefix retained after the second write fails. Two desired properties are currently false and remain issue-keyed shortest-witness ratchets rather than silently allowed assertion failures: @@ -83,13 +85,13 @@ The umbrella command also runs a separate bounded child model for in-memory abor ## Provider handoff and scheduler model -`scripts/check-provider-handoff-scheduler.ts` is a separate bounded adapter model for the runtime boundary that the persisted lifecycle graph does not represent. Its breadth-first explorer normalizes provider-keyed records and owner arrays before deduplicating canonical states, then exhaustively explores enabled action orderings through depth 15 with a 20,000-state budget. It imports `selectHandoffExecutionContext` and the existing `delegateTaskToChild` and `completeDelegatedChild` reducers. A direct saved, unsaved, and locked-profile matrix verifies task-local configuration isolation. Stale provider lookup is caught before this pure selector, so focused provider tests verify the failed lookup, contextual log, and fallback. The protocol state then models two provider instances, their claims and parent snapshots, authoritative parent/child records, current task publication, commit/start ownership, the child scheduler permit, queued and resumed parent state, and one bounded redelegation generation. +`scripts/check-provider-handoff-scheduler.ts` is a separate bounded adapter model for the runtime boundary that the persisted lifecycle graph does not represent. Its breadth-first explorer normalizes provider-keyed records and owner arrays before deduplicating canonical states, then exhaustively explores enabled action orderings through depth 15 with a 20,000-state budget. It imports `selectHandoffExecutionContext` and the existing `delegateTaskToChild` and `completeDelegatedChild` reducers. A direct saved, unsaved, and locked-profile matrix checks task-local configuration selection within those cases. Stale provider lookup is caught before this pure selector, so focused provider tests check the failed lookup, contextual log, and fallback. The protocol state then models two provider instances, their claims and parent snapshots, authoritative parent/child records, current task publication, commit/start ownership, the child scheduler permit, queued and resumed parent state, and one bounded redelegation generation. Provider locking, paused-child/current-task publication, and semaphore admission/release are explicit model abstractions rather than imported production code. Focused provider and `TaskScheduler` tests cover those concrete adapters. Lifecycle commits and completion use the real reducers. Parent publication and its queued continuation share an explicit transition owner: the fixed policy retains that ownership through matching resume invocation, then models the resumed run settling outside transition ownership. This permits a new delegation generation to begin while the prior resumed run remains active without allowing a stale continuation to start across the newer transition. The fixed policy checks every successor for continuous publication, one child start and commit per generation, exact commit-before-start ownership, permit release before parent resume or redelegation, matching parent transition/continuation ownership at resume invocation, and consistent final child/parent publication. It also requires both resume phases, every other action, and named semantic landmarks to remain reachable and fails if the depth boundary has an unseen successor. Six injected legacy transition policies must produce deterministic shortest counterexamples through the same explorer: start before commit, resume before permit release, redelegation before permit release, empty current-task publication, two stale provider commits from competing snapshots, and releasing parent-transition serialization immediately after publication. The last witness must causally include first-child completion and parent publication, a second-child commit, release of the first child's scheduler permit, and then the stale first-child continuation. The checker prints the distinct reachable-state count, complete scenario/action/landmark coverage, bounds, and each named counterexample trace. It deliberately does not add a WAL, global profile projection, or scheduler state to persisted `HistoryItem` records. -For #921, the execution-context matrix verifies saved, unsaved, and locked profile selection at the handoff boundary. This proves only that delegation writes the requested task-local mode and cloned configuration into the child context. It does not prove that every downstream consumer reads that context. The checker retains a divergent-mode witness in which the child task mode differs from the shared provider mode so reader refinements can demonstrate that choosing the wrong source is observable. +For #921, the execution-context matrix checks saved, unsaved, and locked profile selection at the handoff boundary. For that bounded matrix, it establishes only that delegation writes the requested task-local mode and cloned configuration into the child context. It does not prove that every downstream consumer reads that context. The checker retains a divergent-mode witness in which the child task mode differs from the shared provider mode so reader refinements can demonstrate that choosing the wrong source is observable. Issue #1623 exposed that missing refinement: `getEnvironmentDetails`, `presentAssistantMessage` tool validation, and custom-tool execution still read shared provider mode after the handoff snapshot became task-local. PR #1625 owns the production fix and adds a dedicated delegated-mode reader check plus focused adapter tests. Until that runtime PR is merged, the selector model establishes the write-side premise and the divergent-mode witness traces the unresolved read-side obligation; it must not be cited as complete #921 coverage by itself. @@ -97,7 +99,7 @@ Issue #1623 exposed that missing refinement: `getEnvironmentDetails`, `presentAs `scripts/check-task-fanout-protocol.ts` is a separate bounded protocol model for the #369/#372 fan-out safety contract. It explores a live parent with two sibling slots, a two-permit scheduler, independent result readiness, explicit child-to-parent delivery, parent loss, orphan cancellation, and permit release. It checks scheduler capacity and exact permit ownership, one writer and at-most-once delivery per child, readiness before delivery, and no result routing after parent loss. Named landmarks require concurrent siblings beneath a live parent, out-of-order result delivery, parent loss while work is running, and complete orphan cleanup to remain reachable. Injected unsafe states confirm those invariants reject wrong writers, early and duplicate delivery, post-parent-loss routing, and scheduler over-allocation. -This model verifies the intended composition boundary without claiming that concurrent sibling fan-out is enabled in production. `TaskScheduler` already provides bounded permits and guaranteed release, and completion APIs route by explicit parent and child IDs; focused tests cover those adapters. Raising production concurrency still requires live-parent result integration and extension-host coverage before fan-out can ship. Keeping this state space separate preserves the serial handoff checker's one-child ownership invariants. +This model checks the intended abstract composition boundary without claiming that concurrent sibling fan-out is enabled in production. `TaskScheduler` already provides bounded permits and guaranteed release, and completion APIs route by explicit parent and child IDs; focused tests cover those adapters. Raising production concurrency still requires live-parent result integration and extension-host coverage before fan-out can ship. Keeping this state space separate preserves the serial handoff checker's one-child ownership invariants. ## Completion persistence model @@ -137,20 +139,51 @@ The completion persistence checker additionally enforces: These are safety claims within the documented bounds. The checks do not claim liveness, fairness, power-loss durability, filesystem-lock correctness, or exhaustive coverage of arbitrary task counts or retry counts. The completion explorer specifies the event contract rather than importing `Task` or `AttemptCompletionTool`; focused unit tests and the restart E2E verify that concrete production paths implement the modeled guards. Delegated reopen is abstracted as one success-or-failure event after durable child history; fallback from a failed reopen into the normal standalone completion flow remains production-test coverage rather than part of this model. The lifecycle checker also does not distinguish a delayed pre-interruption completion from a legitimate post-resume completion for the same child ID; that requires a persisted attempt/generation token before it can become a sound invariant. +## Coverage audit + +| Protocol area | Coverage status | Production/model relationship | Explicit limits and open points | +| ------------------------------ | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Delegation lifecycle | Production-backed bounded universal | The explorer calls the four production reducers for three task slots through depth 12. | Excludes provider instances, persistence failures, scheduler state, most live `Task` behavior, and generation identity for delayed pre-interruption completion. Recovery-compatible active-parent completion is test-only. | +| Shared-store concurrency | Production-backed bounded scenarios plus known-unsafe witnesses | The explorer imports production delta/merge functions and reducers; a real-filesystem test is a smoke check. | Does not prove crash safety, filesystem/lock semantics, arbitrary processes, or loss-free same-field merging. #1469 and #1021 remain unsafe. | +| Provider handoff and scheduler | Mixed: production-backed reducers/selector plus abstract bounded protocol | Commits use production reducers; provider ownership, publication, transition locks, and permits are model abstractions through depth 15. | Selector correctness does not refine all downstream readers. Scheduler tests cover concrete permit behavior separately. | +| Fan-out | Planned-only abstract bounded protocol over partial shipped primitives | The model has two sibling slots and two abstract permits; production ships a scheduler and ID-routed serial completion primitives. | Production fan-out, live-parent result integration, concurrent sibling E2E, and orphan discovery are absent. | +| Cleanup | Abstract bounded universal plus adapter tests | Abort, disposal, settlement, rejection, and provider shutdown are modeled as protocol/environment actions. | No direct execution of all production cleanup methods, filesystem/editor promises, timing liveness, fairness, or arbitrary task counts. | +| Parser request scope | Production-backed bounded schedule replay | The checker executes production parser APIs across 924 order-preserving schedules for two scopes. | Assumes callers stop invoking a finalized scope; transport behavior, arbitrary request counts, indices, and malformed histories are outside the claim. | +| Completion persistence | Abstract bounded universal plus production tests and one fresh-host E2E path | The model abstracts persistence as a durable phase with at most two write starts; production guards and retry paths are tested separately. | Production permits more retries; no power-loss/filesystem proof, fairness, arbitrary retry count, complete delegated fallback, provider status metadata, or downstream event-consumer model. | + +The production mapping above names primary lifecycle transitions, not every mutation or consumer. Generic store upserts, reconciliation, repair replay, migrations, tool entry points, webview/public API abandonment, provider status updates, and public `TaskCompleted` re-emission remain outside the persisted reducer graph unless explicitly named by a submodel or focused test. For task-local mode, the consumers named under #921/#1623 are a confirmed set, not an exhaustive repository-wide inventory; other mode-sensitive tools must be audited before claiming universal reader isolation. + +CI runs `pnpm lifecycle:model-check` in `.github/workflows/code-qa.yml` after lint and type checking. Extension-host subtask and restart-persistence E2E run separately; a green umbrella command therefore says nothing about an omitted E2E boundary or an unmodeled downstream consumer. + +### Open audit points + +- No executable refinement currently composes persisted lifecycle ownership with provider publication, parser scope, completion durability, or cleanup. Their independent checks must not be combined into a stronger end-to-end claim. +- Generic history upserts, reconciliation, repair replay, and migrations need an explicit mutation inventory before the four lifecycle reducers can be called the only status/lineage writers. +- Public `TaskCompleted` re-emission and its downstream consumers are not part of the completion-persistence state space. +- Task-local mode readers need a repository-wide consumer inventory or enforceable API boundary before #921 can be treated as universal reader isolation; #1623/#1625 cover the confirmed regression paths only. +- Production fan-out needs its own live-parent integration and E2E evidence before the planned abstract model can be promoted to production-backed coverage. + ## Open-issue traceability -The following map separates issue observations from the architectural interpretation encoded here. Open issues can change after this document is written; follow each link for current status. - -| Issue and directly observed evidence | Derived protocol rule | Production transition and current check | -| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [#1469](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1469): the issue report states that a barrier-controlled two-host run reproduced an old child completion clearing a newer handoff 25/25 times. | Completion is conditional on the parent still awaiting that exact child; a live-linked child must remain owned by its parent. | `completeDelegatedChild` rejects stale authoritative input. The lifecycle explorer checks that reducer rule, while the shared-store explorer reproduces the cross-host stale-cache counterexample with an exact causal witness. | -| [#1021](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1021): an in-flight `saveClineMessages` can restore parent/root IDs after abandonment cleared them. | Detachment should be monotonic: later lifecycle work must not reattach an abandoned child. | `abandonDelegatedChild` clears both sides. The shared-store explorer proves the detach commit occurs, then reproduces a refreshed-cache delta that preserves interrupted status while restoring stale live-task lineage. | -| [#1453](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1453), under user report [#1279](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1279): CI observed `TaskCompleted` before restart-visible API history once; 120 local repetitions did not reproduce it. | Completion implies restart-visible assistant history. Delayed or failed writes keep completion pending, and cancellation settles readiness without starting stale retries or emitting completion. | The completion persistence explorer checks the bounded event-ordering and cancellation contract for standalone and delegated tasks. Focused `Task` and `AttemptCompletionTool` tests cover the production adapter; `restart-persistence.test.ts` verifies visibility through a fresh extension host. | -| [#921](https://github.com/Zoo-Code-Org/Zoo-Code/issues/921): delegation across parallel tabs lacks coverage for different view-local mode/profile state. Follow-up [#1623](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1623) proves a correct snapshot is insufficient when downstream readers use shared provider mode. | Delegation must bind an explicit immutable execution-context snapshot, and every mode-sensitive consumer must read the child task's context rather than whichever provider/view is focused. | This checker proves the write-side selector matrix and retains a divergent task/provider-mode witness. Runtime reader correctness is intentionally owned by [PR #1625](https://github.com/Zoo-Code-Org/Zoo-Code/pull/1625), its delegated-mode reader check, and focused environment/tool tests. | -| [#920](https://github.com/Zoo-Code-Org/Zoo-Code/issues/920): issue analysis identifies a missing cross-instance history-update test and potential lost writes. | Distinct task writes must not overwrite one another, and same-task conflicts need an explicit merge/ownership rule. | The shared-store explorer checks distinct-task writes and same-record independent deltas. Cross-instance store tests retain production API coverage, and the synchronized real-filesystem smoke test exercises the actual lock/write path without claiming exhaustive filesystem proof. | -| [#369](https://github.com/Zoo-Code-Org/Zoo-Code/issues/369) and [#372](https://github.com/Zoo-Code-Org/Zoo-Code/issues/372): planned fan-out keeps a parent live while a child runs and requires completion routing by explicit parent ID, single-writer result readiness, permit release, and orphan cleanup. | Persisted `delegated` status is ownership, not proof that the parent instance is suspended. Completion must route by IDs; scheduler resources and live-instance state need separate invariants. | The fan-out protocol checker models a live parent, two siblings, per-child permits/readiness, explicit delivery, parent loss, and orphan cleanup. `TaskScheduler` and ID-routed completion tests cover shipped primitives; production fan-out remains disabled pending live-parent integration and E2E. | -| [#1468](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1468): a late chunk from one request combined tool identity with arguments from another request; rerun passed. | Every stream accumulator needs a request/task generation key, and late events cannot mutate another scope. | Separate protocol. The [native tool-call parser request-scope model](./native-tool-call-parser-scoping-model.md), whose source of truth is `scripts/check-native-tool-call-parser-scoping.ts`, exhaustively replays bounded production-parser interleavings without adding fields to this lifecycle model. | -| [#612](https://github.com/Zoo-Code-Org/Zoo-Code/issues/612): the CLI copied a status union and omitted `interrupted`. | Lifecycle vocabulary should have one type owner. | `HistoryItemStatus` is derived from `HistoryItem`, and production/checker transitions share `taskLifecycle.ts`; consumers should import rather than copy the union. | +The following map separates issue observations from the architectural interpretation encoded here. Open issues can change after this document is written; follow each link for current status. Coverage status uses these evidence classes: + +- **Production-backed bounded:** exhaustive only for the declared state space while executing production functions. +- **Abstract bounded:** exhaustive only for model-authored transitions; refinement depends on separate adapter tests. +- **Known-unsafe witness:** CI preserves a reproducible violation and does not claim the property holds. +- **Proxy/partial:** evidence covers a premise, adapter, or representative path, not the full claim. +- **Planned-only:** specifies behavior not enabled in production. +- **Type/static convention:** centralized typing or guidance without repository-wide enforcement. + +| Issue and observed evidence | Coverage status | Derived protocol rule | Production transition, current check, and remaining gap | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [#1469](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1469): an old child completion can clear a newer handoff across hosts. | **Known-unsafe witness** plus a production-backed reducer guard. | Completion is conditional on the parent still awaiting that exact child. | `completeDelegatedChild` rejects stale authoritative input, but store revalidation lacks exact-child ownership. CI ratchets the shortest cross-host violation; the issue is not closed by a passing checker. | +| [#1021](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1021): an in-flight save can restore lineage after abandonment. | **Known-unsafe witness** plus a production-backed detach reducer. | Detachment should be monotonic. | `abandonDelegatedChild` clears both sides. The model requires that detach commit before reproducing stale lineage restoration; it does not prove detachment is monotonic across all stale tasks or hosts. | +| [#1453](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1453), under [#1279](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1279): completion preceded restart-visible history once. | **Abstract bounded** ordering plus **proxy/partial** production tests and fresh-host E2E. | Completion implies restart-visible assistant history; cancellation prevents later retry/emission. | The model checks at most two write starts and abstracts durability/reopen. Focused `Task` and `AttemptCompletionTool` tests cover guards; one fresh-host E2E covers visibility. Production retry count, power loss, and all downstream event consumers are not modeled. | +| [#921](https://github.com/Zoo-Code-Org/Zoo-Code/issues/921), with follow-up [#1623](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1623): a correct child snapshot can coexist with shared-provider-mode reads. | Write side: **production-backed bounded matrix**. Read side: **proxy/partial and known unsafe** pending #1625. | Delegation must snapshot task-local context, and every mode-sensitive consumer must read it. | The selector matrix and divergent witness cover only the premise. [PR #1625](https://github.com/Zoo-Code-Org/Zoo-Code/pull/1625) owns fixes/tests for confirmed environment, validation, and custom-tool readers. Those readers are not claimed exhaustive. | +| [#920](https://github.com/Zoo-Code-Org/Zoo-Code/issues/920): cross-instance updates can lose writes. | **Production-backed bounded scenarios** plus a **proxy/partial** filesystem smoke test. | Distinct task writes must not overwrite one another; same-task conflicts need explicit merge rules. | The explorer checks six scenarios using production delta/merge functions. Same-field conflicts remain last-writer-wins; crash safety, filesystem semantics, lock staleness, and arbitrary processes are outside scope. | +| [#369](https://github.com/Zoo-Code-Org/Zoo-Code/issues/369) and [#372](https://github.com/Zoo-Code-Org/Zoo-Code/issues/372): planned fan-out needs live-parent routing, result ownership, permits, and orphan cleanup. | **Planned-only abstract bounded** protocol with partial shipped primitives. | Scheduler resources, result readiness, live-instance state, and persisted ownership need separate invariants. | The two-sibling model checks intended safety rules but imports no production fan-out transition. Production fan-out remains disabled; live-parent integration, concurrent sibling E2E, and orphan discovery remain open. | +| [#1468](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1468): late chunks crossed request scope. | **Production-backed bounded schedule replay**. | Every stream accumulator needs request/task scope; late events cannot mutate another scope. | The parser checker runs production APIs for two scopes and 924 schedules. It assumes no calls after finalization and excludes transport behavior, arbitrary indices/request counts, and malformed histories. | +| [#612](https://github.com/Zoo-Code-Org/Zoo-Code/issues/612): a copied status union omitted `interrupted`. | **Type/static convention**. | Lifecycle vocabulary should have one type owner. | `HistoryItemStatus` derives from `HistoryItem`, and shared reducers use it. No repository-wide rule prevents a consumer from copying another string union, so universal ownership is not enforced. | The issue-derived cases intentionally map to bug classes rather than issue-specific flags. In particular, stale event ownership, monotonic terminal/detached state, explicit scope, and single-writer boundaries generalize to future concurrent task work. From 48644f7143cc1f8d820465b933a5731a5c6f6221 Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 13 Sep 2026 02:23:57 +0000 Subject: [PATCH 04/17] docs(lifecycle): separate open and historical issues --- docs/architecture/task-lifecycle-model.md | 32 ++++++++++++++--------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/docs/architecture/task-lifecycle-model.md b/docs/architecture/task-lifecycle-model.md index 2981640784..dcf76c4157 100644 --- a/docs/architecture/task-lifecycle-model.md +++ b/docs/architecture/task-lifecycle-model.md @@ -160,12 +160,12 @@ CI runs `pnpm lifecycle:model-check` in `.github/workflows/code-qa.yml` after li - No executable refinement currently composes persisted lifecycle ownership with provider publication, parser scope, completion durability, or cleanup. Their independent checks must not be combined into a stronger end-to-end claim. - Generic history upserts, reconciliation, repair replay, and migrations need an explicit mutation inventory before the four lifecycle reducers can be called the only status/lineage writers. - Public `TaskCompleted` re-emission and its downstream consumers are not part of the completion-persistence state space. -- Task-local mode readers need a repository-wide consumer inventory or enforceable API boundary before #921 can be treated as universal reader isolation; #1623/#1625 cover the confirmed regression paths only. +- Task-local mode readers need a repository-wide consumer inventory or enforceable API boundary before universal reader isolation can be claimed; open #1623 and PR #1625 cover the confirmed regression paths only, while closed #921 records the write-side snapshot history. - Production fan-out needs its own live-parent integration and E2E evidence before the planned abstract model can be promoted to production-backed coverage. ## Open-issue traceability -The following map separates issue observations from the architectural interpretation encoded here. Open issues can change after this document is written; follow each link for current status. Coverage status uses these evidence classes: +The following map contains only issues confirmed open on GitHub as of 2026-09-13 that still have a production, model-refinement, or enforcement obligation. Closed reports and their still-relevant verification limits are retained separately under [Verified history and limitations](#verified-history-and-limitations). Coverage status uses these evidence classes: - **Production-backed bounded:** exhaustive only for the declared state space while executing production functions. - **Abstract bounded:** exhaustive only for model-authored transitions; refinement depends on separate adapter tests. @@ -174,16 +174,24 @@ The following map separates issue observations from the architectural interpreta - **Planned-only:** specifies behavior not enabled in production. - **Type/static convention:** centralized typing or guidance without repository-wide enforcement. -| Issue and observed evidence | Coverage status | Derived protocol rule | Production transition, current check, and remaining gap | -| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [#1469](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1469): an old child completion can clear a newer handoff across hosts. | **Known-unsafe witness** plus a production-backed reducer guard. | Completion is conditional on the parent still awaiting that exact child. | `completeDelegatedChild` rejects stale authoritative input, but store revalidation lacks exact-child ownership. CI ratchets the shortest cross-host violation; the issue is not closed by a passing checker. | -| [#1021](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1021): an in-flight save can restore lineage after abandonment. | **Known-unsafe witness** plus a production-backed detach reducer. | Detachment should be monotonic. | `abandonDelegatedChild` clears both sides. The model requires that detach commit before reproducing stale lineage restoration; it does not prove detachment is monotonic across all stale tasks or hosts. | -| [#1453](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1453), under [#1279](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1279): completion preceded restart-visible history once. | **Abstract bounded** ordering plus **proxy/partial** production tests and fresh-host E2E. | Completion implies restart-visible assistant history; cancellation prevents later retry/emission. | The model checks at most two write starts and abstracts durability/reopen. Focused `Task` and `AttemptCompletionTool` tests cover guards; one fresh-host E2E covers visibility. Production retry count, power loss, and all downstream event consumers are not modeled. | -| [#921](https://github.com/Zoo-Code-Org/Zoo-Code/issues/921), with follow-up [#1623](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1623): a correct child snapshot can coexist with shared-provider-mode reads. | Write side: **production-backed bounded matrix**. Read side: **proxy/partial and known unsafe** pending #1625. | Delegation must snapshot task-local context, and every mode-sensitive consumer must read it. | The selector matrix and divergent witness cover only the premise. [PR #1625](https://github.com/Zoo-Code-Org/Zoo-Code/pull/1625) owns fixes/tests for confirmed environment, validation, and custom-tool readers. Those readers are not claimed exhaustive. | -| [#920](https://github.com/Zoo-Code-Org/Zoo-Code/issues/920): cross-instance updates can lose writes. | **Production-backed bounded scenarios** plus a **proxy/partial** filesystem smoke test. | Distinct task writes must not overwrite one another; same-task conflicts need explicit merge rules. | The explorer checks six scenarios using production delta/merge functions. Same-field conflicts remain last-writer-wins; crash safety, filesystem semantics, lock staleness, and arbitrary processes are outside scope. | -| [#369](https://github.com/Zoo-Code-Org/Zoo-Code/issues/369) and [#372](https://github.com/Zoo-Code-Org/Zoo-Code/issues/372): planned fan-out needs live-parent routing, result ownership, permits, and orphan cleanup. | **Planned-only abstract bounded** protocol with partial shipped primitives. | Scheduler resources, result readiness, live-instance state, and persisted ownership need separate invariants. | The two-sibling model checks intended safety rules but imports no production fan-out transition. Production fan-out remains disabled; live-parent integration, concurrent sibling E2E, and orphan discovery remain open. | -| [#1468](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1468): late chunks crossed request scope. | **Production-backed bounded schedule replay**. | Every stream accumulator needs request/task scope; late events cannot mutate another scope. | The parser checker runs production APIs for two scopes and 924 schedules. It assumes no calls after finalization and excludes transport behavior, arbitrary indices/request counts, and malformed histories. | -| [#612](https://github.com/Zoo-Code-Org/Zoo-Code/issues/612): a copied status union omitted `interrupted`. | **Type/static convention**. | Lifecycle vocabulary should have one type owner. | `HistoryItemStatus` derives from `HistoryItem`, and shared reducers use it. No repository-wide rule prevents a consumer from copying another string union, so universal ownership is not enforced. | +| Open issue | Coverage status | Remaining production/model obligation | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [#1469](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1469): stale completion can clear a newer handoff across hosts. | **Known-unsafe witness** plus a production-backed reducer guard. | `completeDelegatedChild` rejects stale authoritative input, but store revalidation lacks exact-child ownership. CI ratchets the shortest violation; a passing checker confirms the witness, not safety. | +| [#1021](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1021): an in-flight save can restore lineage after abandonment. | **Known-unsafe witness** plus a production-backed detach reducer. | `abandonDelegatedChild` clears both sides, but stale tasks or hosts can reattach lineage. Monotonic detachment remains unproved and unsafe. | +| [#1623](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1623): delegated consumers can read shared provider mode despite a correct child snapshot. | Write-side premise: **production-backed bounded matrix**. Confirmed readers: **proxy/partial and known unsafe** pending #1625. | [PR #1625](https://github.com/Zoo-Code-Org/Zoo-Code/pull/1625) remains open and owns fixes/tests for environment, validation, and custom-tool readers. A repository-wide mode-consumer inventory or enforceable boundary is still absent. | +| [#369](https://github.com/Zoo-Code-Org/Zoo-Code/issues/369) and [#372](https://github.com/Zoo-Code-Org/Zoo-Code/issues/372): fan-out needs live-parent isolation, result ownership/routing, permits, and orphan cleanup. | **Planned-only abstract bounded** protocol with partial shipped primitives. | The two-sibling model imports no production fan-out transition. Production fan-out, rollback/isolation, live-parent injection, concurrent sibling E2E, and orphan handling remain open. | +| [#612](https://github.com/Zoo-Code-Org/Zoo-Code/issues/612): the CLI copied lifecycle status vocabulary. | Repaired symptom plus unresolved **type/static convention**. | CLI unions now include `interrupted`, but `HistoryTrigger.tsx` still duplicates the union. No shared imported owner or repository-wide rule prevents future drift. | + +## Verified history and limitations + +These closed issues remain useful provenance for regression checks and documented limits, but they are not open work items. Moving them out of open-issue traceability does not upgrade their evidence class or erase adjacent risks. + +| Closed issue | Resolved/historical evidence | Coverage and limitations retained | +| --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [#1453](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1453) under [#1279](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1279) | Completion readiness is guarded in production, with focused `Task`/`AttemptCompletionTool` tests and a fresh-host restart E2E. Both issues are closed. | The ordering model remains **abstract bounded**: at most two write starts, abstract durability/reopen, no power-loss claim, and no model of every downstream `TaskCompleted` consumer. | +| [#921](https://github.com/Zoo-Code-Org/Zoo-Code/issues/921) | Explicit handoff snapshots and the selector matrix cover the historical parallel-view write-side problem; the issue is closed. | This is **production-backed bounded** write-side evidence, not universal consumer isolation. The distinct open reader regression is tracked by #1623 and PR #1625. | +| [#920](https://github.com/Zoo-Code-Org/Zoo-Code/issues/920) | Production delta/merge behavior, bounded cross-instance scenarios, focused tests, and a real-filesystem smoke test cover distinct-task preservation; the issue is closed. | Same-field conflicts remain last-writer-wins; crash/filesystem/lock semantics and arbitrary processes are outside scope. Concrete unsafe same-record races remain tracked by open #1469 and #1021. | +| [#1468](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1468) | The production parser is request-scoped and the checker replays 924 bounded two-scope schedules; the issue is closed. | Coverage remains **production-backed bounded** and assumes no calls after finalization; transport behavior, arbitrary indices/request counts, and malformed histories are excluded. | The issue-derived cases intentionally map to bug classes rather than issue-specific flags. In particular, stale event ownership, monotonic terminal/detached state, explicit scope, and single-writer boundaries generalize to future concurrent task work. From c83d504d7e4b12a02a783fad017530f943090ec3 Mon Sep 17 00:00:00 2001 From: Elliott de Launay Date: Sun, 13 Sep 2026 01:20:51 +0000 Subject: [PATCH 05/17] fix(delegation): read task-local mode in getEnvironmentDetails and validateToolUse --- package.json | 2 +- scripts/check-delegated-mode-readers.ts | 170 ++++++++++++++++++ ...resentAssistantMessage-custom-tool.spec.ts | 1 + .../presentAssistantMessage-images.spec.ts | 1 + ...tantMessage-tool-usage-attribution.spec.ts | 40 +++++ ...esentAssistantMessage-unknown-tool.spec.ts | 1 + .../presentAssistantMessage.ts | 9 +- .../__tests__/getEnvironmentDetails.spec.ts | 26 +++ src/core/environment/getEnvironmentDetails.ts | 5 +- 9 files changed, 249 insertions(+), 6 deletions(-) create mode 100644 scripts/check-delegated-mode-readers.ts diff --git a/package.json b/package.json index e534572620..4c72ff252c 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "check-types": "turbo check-types --log-order grouped --output-logs new-only", "test": "turbo test --log-order grouped --output-logs new-only", "test:mutation-ci": "node --test scripts/stryker-diff.test.mjs", - "lifecycle:model-check": "tsx scripts/check-task-lifecycle.ts && tsx scripts/check-task-store-concurrency.ts && tsx scripts/check-provider-handoff-scheduler.ts && tsx scripts/check-task-fanout-protocol.ts && pnpm cleanup-protocol:model-check && pnpm parser-scope:model-check && tsx scripts/check-completion-persistence.ts", + "lifecycle:model-check": "tsx scripts/check-task-lifecycle.ts && tsx scripts/check-task-store-concurrency.ts && tsx scripts/check-provider-handoff-scheduler.ts && tsx scripts/check-task-fanout-protocol.ts && pnpm cleanup-protocol:model-check && pnpm parser-scope:model-check && tsx scripts/check-completion-persistence.ts && tsx scripts/check-delegated-mode-readers.ts", "cleanup-protocol:model-check": "tsx scripts/check-task-cleanup-protocol.ts", "parser-scope:model-check": "node scripts/run-native-tool-call-parser-scoping.mjs", "mcp:integration-check": "tsx scripts/check-mcp-oauth-integration.ts", diff --git a/scripts/check-delegated-mode-readers.ts b/scripts/check-delegated-mode-readers.ts new file mode 100644 index 0000000000..bd77de29c1 --- /dev/null +++ b/scripts/check-delegated-mode-readers.ts @@ -0,0 +1,170 @@ +// check-delegated-mode-readers.ts +// +// Refinement check for the delegated-child mode-reader invariant (issue #1623). +// +// check-provider-handoff-scheduler.ts verifies the write side: that +// selectHandoffExecutionContext stores the task-local mode correctly. +// This script verifies the read side: that the mode observable by +// tool-validation readers is the task-local mode, not the shared provider mode. +// +// The VS Code-dependent readers (getEnvironmentDetails, +// presentAssistantMessage) are covered by their vitest regression tests. +// This script covers the pure-TS parts of the invariant chain and proves +// that the two sources of mode are observably different, so any reader +// that uses the wrong source silently produces wrong behavior. +// +// Invariant: for any delegated child task C with taskMode = M, +// toolAllowedForMode(tool, M) ≠ toolAllowedForMode(tool, providerMode) +// whenever M ≠ providerMode and the two modes differ on the tool's group. + +import assert from "node:assert/strict" + +import { DEFAULT_MODES } from "../packages/types/src/mode" + +import { TOOL_GROUPS, ALWAYS_AVAILABLE_TOOLS, TOOL_ALIASES } from "../src/shared/tools" +import { selectHandoffExecutionContext, type TaskExecutionContext } from "../src/core/task/providerHandoff" + +// --------------------------------------------------------------------------- +// Minimal inline mode-allows-tool check. +// Avoids importing src/shared/modes.ts, which pulls in VS Code. +// Only covers built-in modes (no custom modes, no file-regex options). +// That is enough to prove the behavioral divergence this check needs. +// --------------------------------------------------------------------------- + +type ModeConfig = (typeof DEFAULT_MODES)[number] +type GroupEntry = ModeConfig["groups"][number] + +function groupName(entry: GroupEntry): string { + return Array.isArray(entry) ? entry[0] : (entry as string) +} + +function toolAllowedForMode(tool: string, modeSlug: string): boolean { + const resolvedTool = (TOOL_ALIASES as Record)[tool] ?? tool + if ((ALWAYS_AVAILABLE_TOOLS as readonly string[]).includes(resolvedTool)) return true + const mode = DEFAULT_MODES.find((m) => m.slug === modeSlug) + if (!mode) return false + for (const entry of mode.groups) { + const groupTools = (TOOL_GROUPS as Record)[groupName(entry)]?.tools ?? [] + if (groupTools.includes(resolvedTool)) return true + } + return false +} + +// --------------------------------------------------------------------------- +// Scenario: parent in "orchestrator" mode delegates child to "code". +// Regression behavior: both readers used providerMode ("orchestrator"). +// Correct behavior: readers use taskMode ("code"). +// +// orchestrator groups: [] → apply_diff blocked +// code groups: [...edit] → apply_diff allowed +// --------------------------------------------------------------------------- + +const parentCtx: TaskExecutionContext = { + mode: "orchestrator", + apiConfigName: undefined, + apiConfiguration: { apiProvider: "anthropic", consecutiveMistakeLimit: 3 }, +} + +// 1. Handoff stores the task-local mode, not the parent mode. +const childCtx = selectHandoffExecutionContext(parentCtx, "code", parentCtx.mode, false, undefined) +assert.equal(childCtx.mode, "code", "handoff must store the requested task-local mode") +assert.notEqual(childCtx.mode, parentCtx.mode, "test scenario requires divergent provider and task modes") + +// 2. The two modes produce observably different tool-validation outcomes. +assert.equal(toolAllowedForMode("apply_diff", "orchestrator"), false, "orchestrator has no edit group") +assert.equal(toolAllowedForMode("apply_diff", "code"), true, "code has the edit group") + +// 3. Regression claim: a reader that consumes providerMode rejects apply_diff; +// a reader that consumes taskMode correctly allows it. +const viaProviderMode = toolAllowedForMode("apply_diff", parentCtx.mode) // "orchestrator" — wrong source +const viaTaskMode = toolAllowedForMode("apply_diff", childCtx.mode) // "code" — correct source +assert.equal(viaProviderMode, false, "stale provider mode rejects apply_diff (regression behavior)") +assert.equal(viaTaskMode, true, "task-local mode allows apply_diff (correct behavior)") + +// 4. Additional mode pairs that show the same divergence. +const DIVERGENT_PAIRS: Array<{ + label: string + providerMode: string + taskMode: string + probe: string + blockedInProvider: boolean + allowedInTask: boolean +}> = [ + // orchestrator → code: edit tools blocked at provider level, allowed at task level + { + label: "orchestrator→code apply_diff", + providerMode: "orchestrator", + taskMode: "code", + probe: "apply_diff", + blockedInProvider: true, + allowedInTask: true, + }, + // orchestrator → code: command tools blocked at provider level, allowed at task level + { + label: "orchestrator→code execute_command", + providerMode: "orchestrator", + taskMode: "code", + probe: "execute_command", + blockedInProvider: true, + allowedInTask: true, + }, + // code → ask: edit tools allowed at provider level, blocked at task level + { + label: "code→ask apply_diff", + providerMode: "code", + taskMode: "ask", + probe: "apply_diff", + blockedInProvider: false, + allowedInTask: false, + }, + // ask → code: edit tools blocked at provider level, allowed at task level + { + label: "ask→code write_to_file", + providerMode: "ask", + taskMode: "code", + probe: "write_to_file", + blockedInProvider: true, + allowedInTask: true, + }, +] + +for (const pair of DIVERGENT_PAIRS) { + const ctx = selectHandoffExecutionContext( + { ...parentCtx, mode: pair.providerMode }, + pair.taskMode, + pair.providerMode, + false, + undefined, + ) + assert.equal(ctx.mode, pair.taskMode, `${pair.label}: handoff must store task-local mode`) + assert.equal( + toolAllowedForMode(pair.probe, pair.providerMode), + !pair.blockedInProvider, + `${pair.label}: wrong provider-mode result`, + ) + assert.equal( + toolAllowedForMode(pair.probe, pair.taskMode), + pair.allowedInTask, + `${pair.label}: wrong task-mode result`, + ) + // The two sources disagree, so using the wrong one is always observable. + assert.notEqual( + toolAllowedForMode(pair.probe, pair.providerMode), + toolAllowedForMode(pair.probe, pair.taskMode), + `${pair.label}: provider and task mode must differ on this probe tool`, + ) +} + +// 5. For every built-in mode as a delegation target: selectHandoffExecutionContext +// always stores the requested mode, regardless of parent mode. +for (const mode of DEFAULT_MODES) { + const ctx = selectHandoffExecutionContext(parentCtx, mode.slug, parentCtx.mode, false, undefined) + assert.equal(ctx.mode, mode.slug, `handoff must store ${mode.slug}, not parent mode ${parentCtx.mode}`) +} + +console.log( + `Delegated mode reader check passed: ` + + `regression scenario verified, ` + + `${DIVERGENT_PAIRS.length} divergent-mode pairs checked, ` + + `${DEFAULT_MODES.length}/${DEFAULT_MODES.length} built-in modes verified`, +) diff --git a/src/core/assistant-message/__tests__/presentAssistantMessage-custom-tool.spec.ts b/src/core/assistant-message/__tests__/presentAssistantMessage-custom-tool.spec.ts index 1ef25e852b..39553a2ddc 100644 --- a/src/core/assistant-message/__tests__/presentAssistantMessage-custom-tool.spec.ts +++ b/src/core/assistant-message/__tests__/presentAssistantMessage-custom-tool.spec.ts @@ -77,6 +77,7 @@ describe("presentAssistantMessage - Custom Tool Recording", () => { }), }), }, + getTaskMode: vi.fn().mockResolvedValue("code"), say: vi.fn().mockResolvedValue(undefined), ask: vi.fn().mockResolvedValue({ response: "yesButtonClicked" }), } diff --git a/src/core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts b/src/core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts index fcf778b8f8..7cb4c427d8 100644 --- a/src/core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts +++ b/src/core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts @@ -57,6 +57,7 @@ describe("presentAssistantMessage - Image Handling in Native Tool Calling", () = }), }), }, + getTaskMode: vi.fn().mockResolvedValue("code"), say: vi.fn().mockResolvedValue(undefined), ask: vi.fn().mockResolvedValue({ response: "yesButtonClicked" }), } diff --git a/src/core/assistant-message/__tests__/presentAssistantMessage-tool-usage-attribution.spec.ts b/src/core/assistant-message/__tests__/presentAssistantMessage-tool-usage-attribution.spec.ts index c75eb6ee18..afcb7e8960 100644 --- a/src/core/assistant-message/__tests__/presentAssistantMessage-tool-usage-attribution.spec.ts +++ b/src/core/assistant-message/__tests__/presentAssistantMessage-tool-usage-attribution.spec.ts @@ -73,6 +73,7 @@ interface MockTask { say: ReturnType ask: ReturnType pushToolResultToUserContent: ReturnType + getTaskMode: ReturnType } describe("presentAssistantMessage - tool usage attribution", () => { @@ -115,6 +116,7 @@ describe("presentAssistantMessage - tool usage attribution", () => { say: vi.fn().mockResolvedValue(undefined), ask: vi.fn().mockResolvedValue({ response: "yesButtonClicked" }), pushToolResultToUserContent: vi.fn(), + getTaskMode: vi.fn().mockResolvedValue("code"), } mockTask.pushToolResultToUserContent = vi @@ -316,4 +318,42 @@ describe("presentAssistantMessage - tool usage attribution", () => { expect(TelemetryService.instance.captureToolUsage).not.toHaveBeenCalled() }) }) + + describe("mode delegation regression", () => { + // Regression for issue #1623. + // Before the fix, validateToolUse received the shared provider mode instead + // of the task-local mode, so a child delegated to "architect" mode would have + // its tools validated against "orchestrator". + it("passes the task-local mode to validateToolUse, not the provider mode", async () => { + // Provider says "orchestrator"; task was delegated to "architect". + mockTask.providerRef = { + deref: () => ({ + getState: vi.fn().mockResolvedValue({ + mode: "orchestrator", + customModes: [], + }), + }), + } + mockTask.getTaskMode = vi.fn().mockResolvedValue("architect") + + mockTask.assistantMessageContent = [ + { + type: "tool_use", + id: "call_delegation", + name: "read_file", + params: { path: "test.ts" }, + nativeArgs: { path: "test.ts" }, + partial: false, + }, + ] + + await presentAssistantMessage(mockTask as unknown as Task) + + // The key assertion: task-local mode "architect" was passed, not "orchestrator". + const calls = vi.mocked(validateToolUse).mock.calls + expect(calls.length).toBeGreaterThan(0) + expect(calls[0][0]).toBe("read_file") + expect(calls[0][1]).toBe("architect") + }) + }) }) diff --git a/src/core/assistant-message/__tests__/presentAssistantMessage-unknown-tool.spec.ts b/src/core/assistant-message/__tests__/presentAssistantMessage-unknown-tool.spec.ts index 78a4a19e91..78af9653c7 100644 --- a/src/core/assistant-message/__tests__/presentAssistantMessage-unknown-tool.spec.ts +++ b/src/core/assistant-message/__tests__/presentAssistantMessage-unknown-tool.spec.ts @@ -60,6 +60,7 @@ describe("presentAssistantMessage - Unknown Tool Handling", () => { }), }), }, + getTaskMode: vi.fn().mockResolvedValue("code"), say: vi.fn().mockResolvedValue(undefined), ask: vi.fn().mockResolvedValue({ response: "yesButtonClicked" }), } diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index 7b25db4e66..b5a83882be 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -344,7 +344,10 @@ export async function presentAssistantMessage(cline: Task) { // Fetch state early so it's available for toolDescription and validation const state = await cline.providerRef.deref()?.getState() - const { mode, customModes, experiments: stateExperiments, disabledTools } = state ?? {} + const { customModes, experiments: stateExperiments, disabledTools } = state ?? {} + // Read the task-local mode, not the shared provider mode. + // A delegated child task may run in a different mode than its parent. + const taskMode = await cline.getTaskMode() const toolDescription = (): string => { switch (block.name) { @@ -617,7 +620,7 @@ export async function presentAssistantMessage(cline: Task) { validateToolUse( block.name as ToolName, - mode ?? defaultModeSlug, + taskMode, customModes ?? [], toolRequirements, block.params, @@ -924,7 +927,7 @@ export async function presentAssistantMessage(cline: Task) { } const result = await customTool.execute(customToolArgs, { - mode: mode ?? defaultModeSlug, + mode: taskMode, task: cline, }) diff --git a/src/core/environment/__tests__/getEnvironmentDetails.spec.ts b/src/core/environment/__tests__/getEnvironmentDetails.spec.ts index df47e83c21..0b4d63fbac 100644 --- a/src/core/environment/__tests__/getEnvironmentDetails.spec.ts +++ b/src/core/environment/__tests__/getEnvironmentDetails.spec.ts @@ -117,6 +117,7 @@ describe("getEnvironmentDetails", () => { deref: vi.fn().mockReturnValue(mockProvider), [Symbol.toStringTag]: "WeakRef", } as unknown as WeakRef, + getTaskMode: vi.fn().mockResolvedValue("code"), } // Mock other dependencies. @@ -464,4 +465,29 @@ describe("getEnvironmentDetails", () => { const result = await getEnvironmentDetails(mockCline as Task, true) expect(result).toContain("File listing unavailable: unexpected string rejection") }) + + // Regression for issue #1623. + // Before the fix, the Current Mode block read the shared provider mode. + // A child delegated to "architect" mode would report "orchestrator" instead. + it("uses the task-local mode in the Current Mode block, not the provider mode", async () => { + // Provider mode stays "code"; task was delegated to "architect". + mockState.mode = "code" + ;(mockCline.getTaskMode as Mock).mockResolvedValue("architect") + ;(getFullModeDetails as Mock).mockResolvedValue({ + name: "🏗️ Architect", + roleDefinition: "You design software.", + customInstructions: "", + }) + + const result = await getEnvironmentDetails(mockCline as Task) + + expect(result).toContain("architect") + expect(result).not.toContain("code") + expect(getFullModeDetails).toHaveBeenCalledWith( + "architect", + [], + undefined, + expect.objectContaining({ cwd: mockCwd }), + ) + }) }) diff --git a/src/core/environment/getEnvironmentDetails.ts b/src/core/environment/getEnvironmentDetails.ts index 0e7d18a57a..773870c304 100644 --- a/src/core/environment/getEnvironmentDetails.ts +++ b/src/core/environment/getEnvironmentDetails.ts @@ -205,7 +205,6 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo // Add current mode and any mode-specific warnings. const { - mode, customModes, customModePrompts, experiments = {} as Record, @@ -213,7 +212,9 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo language, } = state ?? {} - const currentMode = mode ?? defaultModeSlug + // Read the task-local mode, not the shared provider mode. + // A delegated child task may run in a different mode than its parent. + const currentMode = await cline.getTaskMode() const modeDetails = await getFullModeDetails(currentMode, customModes, customModePrompts, { cwd: cline.cwd, From 4909065fb20c3b6818791ce27567376853d7e0be Mon Sep 17 00:00:00 2001 From: Elliott de Launay Date: Sun, 13 Sep 2026 01:52:10 +0000 Subject: [PATCH 06/17] test(delegation): cover custom-tool execute context and state fallback --- ...resentAssistantMessage-custom-tool.spec.ts | 45 +++++++++++++++++++ ...tantMessage-tool-usage-attribution.spec.ts | 39 ++++++++++++++-- 2 files changed, 80 insertions(+), 4 deletions(-) diff --git a/src/core/assistant-message/__tests__/presentAssistantMessage-custom-tool.spec.ts b/src/core/assistant-message/__tests__/presentAssistantMessage-custom-tool.spec.ts index 39553a2ddc..e7f4465441 100644 --- a/src/core/assistant-message/__tests__/presentAssistantMessage-custom-tool.spec.ts +++ b/src/core/assistant-message/__tests__/presentAssistantMessage-custom-tool.spec.ts @@ -123,6 +123,51 @@ describe("presentAssistantMessage - Custom Tool Recording", () => { }) }) + describe("Custom tool mode delegation regression", () => { + // Regression for issue #1623. + // Before the fix, customTool.execute received the shared provider mode + // instead of the task-local mode. A child delegated to "architect" would + // have its custom tool called with "orchestrator". + it("passes the task-local mode to customTool.execute, not the provider mode", async () => { + // Provider says "orchestrator"; task was delegated to "architect". + mockTask.providerRef = { + deref: () => ({ + getState: vi.fn().mockResolvedValue({ + mode: "orchestrator", + customModes: [], + experiments: { customTools: true }, + }), + }), + } + mockTask.getTaskMode = vi.fn().mockResolvedValue("architect") + + const executeMock = vi.fn().mockResolvedValue("result") + vi.mocked(customToolRegistry.has).mockReturnValue(true) + vi.mocked(customToolRegistry.get).mockReturnValue({ + name: "my_custom_tool", + description: "A custom tool", + execute: executeMock, + }) + + mockTask.assistantMessageContent = [ + { + type: "tool_use", + id: "call_delegation", + name: "my_custom_tool", + params: { value: "test" }, + partial: false, + }, + ] + + await presentAssistantMessage(mockTask) + + expect(executeMock).toHaveBeenCalledOnce() + const context = executeMock.mock.calls[0][1] + expect(context.mode).toBe("architect") + expect(context.task).toBe(mockTask) + }) + }) + describe("Custom tool error recording", () => { it("should record custom tool error as 'custom_tool'", async () => { const toolCallId = "tool_call_custom_error_123" diff --git a/src/core/assistant-message/__tests__/presentAssistantMessage-tool-usage-attribution.spec.ts b/src/core/assistant-message/__tests__/presentAssistantMessage-tool-usage-attribution.spec.ts index afcb7e8960..9becd11bbe 100644 --- a/src/core/assistant-message/__tests__/presentAssistantMessage-tool-usage-attribution.spec.ts +++ b/src/core/assistant-message/__tests__/presentAssistantMessage-tool-usage-attribution.spec.ts @@ -65,10 +65,12 @@ interface MockTask { recordToolError: ReturnType toolRepetitionDetector: { check: ReturnType } providerRef: { - deref: () => { - getState: ReturnType - getMcpHub?: () => { findServerNameBySanitizedName: (name: string) => string | undefined } - } + deref: () => + | { + getState: ReturnType + getMcpHub?: () => { findServerNameBySanitizedName: (name: string) => string | undefined } + } + | undefined } say: ReturnType ask: ReturnType @@ -319,6 +321,35 @@ describe("presentAssistantMessage - tool usage attribution", () => { }) }) + describe("undefined provider state", () => { + // Covers the `state ?? {}` fallback branch (line 347 of presentAssistantMessage.ts). + // When providerRef.deref() returns undefined, state is undefined and the + // destructure falls back to {}, so customModes / experiments / disabledTools + // are all undefined. Tool validation must still use the task-local mode. + it("falls back to empty state when provider is unavailable", async () => { + mockTask.providerRef = { deref: () => undefined } + mockTask.assistantMessageContent = [ + { + type: "tool_use", + id: "call_no_state", + name: "read_file", + params: { path: "test.ts" }, + nativeArgs: { path: "test.ts" }, + partial: false, + }, + ] + + await presentAssistantMessage(mockTask as unknown as Task) + + // validateToolUse must still be called with the task-local mode. + const calls = vi.mocked(validateToolUse).mock.calls + expect(calls.length).toBeGreaterThan(0) + expect(calls[0][1]).toBe("code") + // customModes falls back to [] (from the ?? {} path). + expect(calls[0][2]).toEqual([]) + }) + }) + describe("mode delegation regression", () => { // Regression for issue #1623. // Before the fix, validateToolUse received the shared provider mode instead From e2531fe07342b19882bd1e8bc9adaf1138e32e00 Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 13 Sep 2026 03:29:29 +0000 Subject: [PATCH 07/17] Revert "test(delegation): cover custom-tool execute context and state fallback" This reverts commit 4909065fb20c3b6818791ce27567376853d7e0be. --- ...resentAssistantMessage-custom-tool.spec.ts | 45 ------------------- ...tantMessage-tool-usage-attribution.spec.ts | 39 ++-------------- 2 files changed, 4 insertions(+), 80 deletions(-) diff --git a/src/core/assistant-message/__tests__/presentAssistantMessage-custom-tool.spec.ts b/src/core/assistant-message/__tests__/presentAssistantMessage-custom-tool.spec.ts index e7f4465441..39553a2ddc 100644 --- a/src/core/assistant-message/__tests__/presentAssistantMessage-custom-tool.spec.ts +++ b/src/core/assistant-message/__tests__/presentAssistantMessage-custom-tool.spec.ts @@ -123,51 +123,6 @@ describe("presentAssistantMessage - Custom Tool Recording", () => { }) }) - describe("Custom tool mode delegation regression", () => { - // Regression for issue #1623. - // Before the fix, customTool.execute received the shared provider mode - // instead of the task-local mode. A child delegated to "architect" would - // have its custom tool called with "orchestrator". - it("passes the task-local mode to customTool.execute, not the provider mode", async () => { - // Provider says "orchestrator"; task was delegated to "architect". - mockTask.providerRef = { - deref: () => ({ - getState: vi.fn().mockResolvedValue({ - mode: "orchestrator", - customModes: [], - experiments: { customTools: true }, - }), - }), - } - mockTask.getTaskMode = vi.fn().mockResolvedValue("architect") - - const executeMock = vi.fn().mockResolvedValue("result") - vi.mocked(customToolRegistry.has).mockReturnValue(true) - vi.mocked(customToolRegistry.get).mockReturnValue({ - name: "my_custom_tool", - description: "A custom tool", - execute: executeMock, - }) - - mockTask.assistantMessageContent = [ - { - type: "tool_use", - id: "call_delegation", - name: "my_custom_tool", - params: { value: "test" }, - partial: false, - }, - ] - - await presentAssistantMessage(mockTask) - - expect(executeMock).toHaveBeenCalledOnce() - const context = executeMock.mock.calls[0][1] - expect(context.mode).toBe("architect") - expect(context.task).toBe(mockTask) - }) - }) - describe("Custom tool error recording", () => { it("should record custom tool error as 'custom_tool'", async () => { const toolCallId = "tool_call_custom_error_123" diff --git a/src/core/assistant-message/__tests__/presentAssistantMessage-tool-usage-attribution.spec.ts b/src/core/assistant-message/__tests__/presentAssistantMessage-tool-usage-attribution.spec.ts index 9becd11bbe..afcb7e8960 100644 --- a/src/core/assistant-message/__tests__/presentAssistantMessage-tool-usage-attribution.spec.ts +++ b/src/core/assistant-message/__tests__/presentAssistantMessage-tool-usage-attribution.spec.ts @@ -65,12 +65,10 @@ interface MockTask { recordToolError: ReturnType toolRepetitionDetector: { check: ReturnType } providerRef: { - deref: () => - | { - getState: ReturnType - getMcpHub?: () => { findServerNameBySanitizedName: (name: string) => string | undefined } - } - | undefined + deref: () => { + getState: ReturnType + getMcpHub?: () => { findServerNameBySanitizedName: (name: string) => string | undefined } + } } say: ReturnType ask: ReturnType @@ -321,35 +319,6 @@ describe("presentAssistantMessage - tool usage attribution", () => { }) }) - describe("undefined provider state", () => { - // Covers the `state ?? {}` fallback branch (line 347 of presentAssistantMessage.ts). - // When providerRef.deref() returns undefined, state is undefined and the - // destructure falls back to {}, so customModes / experiments / disabledTools - // are all undefined. Tool validation must still use the task-local mode. - it("falls back to empty state when provider is unavailable", async () => { - mockTask.providerRef = { deref: () => undefined } - mockTask.assistantMessageContent = [ - { - type: "tool_use", - id: "call_no_state", - name: "read_file", - params: { path: "test.ts" }, - nativeArgs: { path: "test.ts" }, - partial: false, - }, - ] - - await presentAssistantMessage(mockTask as unknown as Task) - - // validateToolUse must still be called with the task-local mode. - const calls = vi.mocked(validateToolUse).mock.calls - expect(calls.length).toBeGreaterThan(0) - expect(calls[0][1]).toBe("code") - // customModes falls back to [] (from the ?? {} path). - expect(calls[0][2]).toEqual([]) - }) - }) - describe("mode delegation regression", () => { // Regression for issue #1623. // Before the fix, validateToolUse received the shared provider mode instead From b195e8ca8578c209c4454380d2c202b7e421ae98 Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 13 Sep 2026 03:29:29 +0000 Subject: [PATCH 08/17] Revert "fix(delegation): read task-local mode in getEnvironmentDetails and validateToolUse" This reverts commit c83d504d7e4b12a02a783fad017530f943090ec3. --- package.json | 2 +- scripts/check-delegated-mode-readers.ts | 170 ------------------ ...resentAssistantMessage-custom-tool.spec.ts | 1 - .../presentAssistantMessage-images.spec.ts | 1 - ...tantMessage-tool-usage-attribution.spec.ts | 40 ----- ...esentAssistantMessage-unknown-tool.spec.ts | 1 - .../presentAssistantMessage.ts | 9 +- .../__tests__/getEnvironmentDetails.spec.ts | 26 --- src/core/environment/getEnvironmentDetails.ts | 5 +- 9 files changed, 6 insertions(+), 249 deletions(-) delete mode 100644 scripts/check-delegated-mode-readers.ts diff --git a/package.json b/package.json index 4c72ff252c..e534572620 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "check-types": "turbo check-types --log-order grouped --output-logs new-only", "test": "turbo test --log-order grouped --output-logs new-only", "test:mutation-ci": "node --test scripts/stryker-diff.test.mjs", - "lifecycle:model-check": "tsx scripts/check-task-lifecycle.ts && tsx scripts/check-task-store-concurrency.ts && tsx scripts/check-provider-handoff-scheduler.ts && tsx scripts/check-task-fanout-protocol.ts && pnpm cleanup-protocol:model-check && pnpm parser-scope:model-check && tsx scripts/check-completion-persistence.ts && tsx scripts/check-delegated-mode-readers.ts", + "lifecycle:model-check": "tsx scripts/check-task-lifecycle.ts && tsx scripts/check-task-store-concurrency.ts && tsx scripts/check-provider-handoff-scheduler.ts && tsx scripts/check-task-fanout-protocol.ts && pnpm cleanup-protocol:model-check && pnpm parser-scope:model-check && tsx scripts/check-completion-persistence.ts", "cleanup-protocol:model-check": "tsx scripts/check-task-cleanup-protocol.ts", "parser-scope:model-check": "node scripts/run-native-tool-call-parser-scoping.mjs", "mcp:integration-check": "tsx scripts/check-mcp-oauth-integration.ts", diff --git a/scripts/check-delegated-mode-readers.ts b/scripts/check-delegated-mode-readers.ts deleted file mode 100644 index bd77de29c1..0000000000 --- a/scripts/check-delegated-mode-readers.ts +++ /dev/null @@ -1,170 +0,0 @@ -// check-delegated-mode-readers.ts -// -// Refinement check for the delegated-child mode-reader invariant (issue #1623). -// -// check-provider-handoff-scheduler.ts verifies the write side: that -// selectHandoffExecutionContext stores the task-local mode correctly. -// This script verifies the read side: that the mode observable by -// tool-validation readers is the task-local mode, not the shared provider mode. -// -// The VS Code-dependent readers (getEnvironmentDetails, -// presentAssistantMessage) are covered by their vitest regression tests. -// This script covers the pure-TS parts of the invariant chain and proves -// that the two sources of mode are observably different, so any reader -// that uses the wrong source silently produces wrong behavior. -// -// Invariant: for any delegated child task C with taskMode = M, -// toolAllowedForMode(tool, M) ≠ toolAllowedForMode(tool, providerMode) -// whenever M ≠ providerMode and the two modes differ on the tool's group. - -import assert from "node:assert/strict" - -import { DEFAULT_MODES } from "../packages/types/src/mode" - -import { TOOL_GROUPS, ALWAYS_AVAILABLE_TOOLS, TOOL_ALIASES } from "../src/shared/tools" -import { selectHandoffExecutionContext, type TaskExecutionContext } from "../src/core/task/providerHandoff" - -// --------------------------------------------------------------------------- -// Minimal inline mode-allows-tool check. -// Avoids importing src/shared/modes.ts, which pulls in VS Code. -// Only covers built-in modes (no custom modes, no file-regex options). -// That is enough to prove the behavioral divergence this check needs. -// --------------------------------------------------------------------------- - -type ModeConfig = (typeof DEFAULT_MODES)[number] -type GroupEntry = ModeConfig["groups"][number] - -function groupName(entry: GroupEntry): string { - return Array.isArray(entry) ? entry[0] : (entry as string) -} - -function toolAllowedForMode(tool: string, modeSlug: string): boolean { - const resolvedTool = (TOOL_ALIASES as Record)[tool] ?? tool - if ((ALWAYS_AVAILABLE_TOOLS as readonly string[]).includes(resolvedTool)) return true - const mode = DEFAULT_MODES.find((m) => m.slug === modeSlug) - if (!mode) return false - for (const entry of mode.groups) { - const groupTools = (TOOL_GROUPS as Record)[groupName(entry)]?.tools ?? [] - if (groupTools.includes(resolvedTool)) return true - } - return false -} - -// --------------------------------------------------------------------------- -// Scenario: parent in "orchestrator" mode delegates child to "code". -// Regression behavior: both readers used providerMode ("orchestrator"). -// Correct behavior: readers use taskMode ("code"). -// -// orchestrator groups: [] → apply_diff blocked -// code groups: [...edit] → apply_diff allowed -// --------------------------------------------------------------------------- - -const parentCtx: TaskExecutionContext = { - mode: "orchestrator", - apiConfigName: undefined, - apiConfiguration: { apiProvider: "anthropic", consecutiveMistakeLimit: 3 }, -} - -// 1. Handoff stores the task-local mode, not the parent mode. -const childCtx = selectHandoffExecutionContext(parentCtx, "code", parentCtx.mode, false, undefined) -assert.equal(childCtx.mode, "code", "handoff must store the requested task-local mode") -assert.notEqual(childCtx.mode, parentCtx.mode, "test scenario requires divergent provider and task modes") - -// 2. The two modes produce observably different tool-validation outcomes. -assert.equal(toolAllowedForMode("apply_diff", "orchestrator"), false, "orchestrator has no edit group") -assert.equal(toolAllowedForMode("apply_diff", "code"), true, "code has the edit group") - -// 3. Regression claim: a reader that consumes providerMode rejects apply_diff; -// a reader that consumes taskMode correctly allows it. -const viaProviderMode = toolAllowedForMode("apply_diff", parentCtx.mode) // "orchestrator" — wrong source -const viaTaskMode = toolAllowedForMode("apply_diff", childCtx.mode) // "code" — correct source -assert.equal(viaProviderMode, false, "stale provider mode rejects apply_diff (regression behavior)") -assert.equal(viaTaskMode, true, "task-local mode allows apply_diff (correct behavior)") - -// 4. Additional mode pairs that show the same divergence. -const DIVERGENT_PAIRS: Array<{ - label: string - providerMode: string - taskMode: string - probe: string - blockedInProvider: boolean - allowedInTask: boolean -}> = [ - // orchestrator → code: edit tools blocked at provider level, allowed at task level - { - label: "orchestrator→code apply_diff", - providerMode: "orchestrator", - taskMode: "code", - probe: "apply_diff", - blockedInProvider: true, - allowedInTask: true, - }, - // orchestrator → code: command tools blocked at provider level, allowed at task level - { - label: "orchestrator→code execute_command", - providerMode: "orchestrator", - taskMode: "code", - probe: "execute_command", - blockedInProvider: true, - allowedInTask: true, - }, - // code → ask: edit tools allowed at provider level, blocked at task level - { - label: "code→ask apply_diff", - providerMode: "code", - taskMode: "ask", - probe: "apply_diff", - blockedInProvider: false, - allowedInTask: false, - }, - // ask → code: edit tools blocked at provider level, allowed at task level - { - label: "ask→code write_to_file", - providerMode: "ask", - taskMode: "code", - probe: "write_to_file", - blockedInProvider: true, - allowedInTask: true, - }, -] - -for (const pair of DIVERGENT_PAIRS) { - const ctx = selectHandoffExecutionContext( - { ...parentCtx, mode: pair.providerMode }, - pair.taskMode, - pair.providerMode, - false, - undefined, - ) - assert.equal(ctx.mode, pair.taskMode, `${pair.label}: handoff must store task-local mode`) - assert.equal( - toolAllowedForMode(pair.probe, pair.providerMode), - !pair.blockedInProvider, - `${pair.label}: wrong provider-mode result`, - ) - assert.equal( - toolAllowedForMode(pair.probe, pair.taskMode), - pair.allowedInTask, - `${pair.label}: wrong task-mode result`, - ) - // The two sources disagree, so using the wrong one is always observable. - assert.notEqual( - toolAllowedForMode(pair.probe, pair.providerMode), - toolAllowedForMode(pair.probe, pair.taskMode), - `${pair.label}: provider and task mode must differ on this probe tool`, - ) -} - -// 5. For every built-in mode as a delegation target: selectHandoffExecutionContext -// always stores the requested mode, regardless of parent mode. -for (const mode of DEFAULT_MODES) { - const ctx = selectHandoffExecutionContext(parentCtx, mode.slug, parentCtx.mode, false, undefined) - assert.equal(ctx.mode, mode.slug, `handoff must store ${mode.slug}, not parent mode ${parentCtx.mode}`) -} - -console.log( - `Delegated mode reader check passed: ` + - `regression scenario verified, ` + - `${DIVERGENT_PAIRS.length} divergent-mode pairs checked, ` + - `${DEFAULT_MODES.length}/${DEFAULT_MODES.length} built-in modes verified`, -) diff --git a/src/core/assistant-message/__tests__/presentAssistantMessage-custom-tool.spec.ts b/src/core/assistant-message/__tests__/presentAssistantMessage-custom-tool.spec.ts index 39553a2ddc..1ef25e852b 100644 --- a/src/core/assistant-message/__tests__/presentAssistantMessage-custom-tool.spec.ts +++ b/src/core/assistant-message/__tests__/presentAssistantMessage-custom-tool.spec.ts @@ -77,7 +77,6 @@ describe("presentAssistantMessage - Custom Tool Recording", () => { }), }), }, - getTaskMode: vi.fn().mockResolvedValue("code"), say: vi.fn().mockResolvedValue(undefined), ask: vi.fn().mockResolvedValue({ response: "yesButtonClicked" }), } diff --git a/src/core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts b/src/core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts index 7cb4c427d8..fcf778b8f8 100644 --- a/src/core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts +++ b/src/core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts @@ -57,7 +57,6 @@ describe("presentAssistantMessage - Image Handling in Native Tool Calling", () = }), }), }, - getTaskMode: vi.fn().mockResolvedValue("code"), say: vi.fn().mockResolvedValue(undefined), ask: vi.fn().mockResolvedValue({ response: "yesButtonClicked" }), } diff --git a/src/core/assistant-message/__tests__/presentAssistantMessage-tool-usage-attribution.spec.ts b/src/core/assistant-message/__tests__/presentAssistantMessage-tool-usage-attribution.spec.ts index afcb7e8960..c75eb6ee18 100644 --- a/src/core/assistant-message/__tests__/presentAssistantMessage-tool-usage-attribution.spec.ts +++ b/src/core/assistant-message/__tests__/presentAssistantMessage-tool-usage-attribution.spec.ts @@ -73,7 +73,6 @@ interface MockTask { say: ReturnType ask: ReturnType pushToolResultToUserContent: ReturnType - getTaskMode: ReturnType } describe("presentAssistantMessage - tool usage attribution", () => { @@ -116,7 +115,6 @@ describe("presentAssistantMessage - tool usage attribution", () => { say: vi.fn().mockResolvedValue(undefined), ask: vi.fn().mockResolvedValue({ response: "yesButtonClicked" }), pushToolResultToUserContent: vi.fn(), - getTaskMode: vi.fn().mockResolvedValue("code"), } mockTask.pushToolResultToUserContent = vi @@ -318,42 +316,4 @@ describe("presentAssistantMessage - tool usage attribution", () => { expect(TelemetryService.instance.captureToolUsage).not.toHaveBeenCalled() }) }) - - describe("mode delegation regression", () => { - // Regression for issue #1623. - // Before the fix, validateToolUse received the shared provider mode instead - // of the task-local mode, so a child delegated to "architect" mode would have - // its tools validated against "orchestrator". - it("passes the task-local mode to validateToolUse, not the provider mode", async () => { - // Provider says "orchestrator"; task was delegated to "architect". - mockTask.providerRef = { - deref: () => ({ - getState: vi.fn().mockResolvedValue({ - mode: "orchestrator", - customModes: [], - }), - }), - } - mockTask.getTaskMode = vi.fn().mockResolvedValue("architect") - - mockTask.assistantMessageContent = [ - { - type: "tool_use", - id: "call_delegation", - name: "read_file", - params: { path: "test.ts" }, - nativeArgs: { path: "test.ts" }, - partial: false, - }, - ] - - await presentAssistantMessage(mockTask as unknown as Task) - - // The key assertion: task-local mode "architect" was passed, not "orchestrator". - const calls = vi.mocked(validateToolUse).mock.calls - expect(calls.length).toBeGreaterThan(0) - expect(calls[0][0]).toBe("read_file") - expect(calls[0][1]).toBe("architect") - }) - }) }) diff --git a/src/core/assistant-message/__tests__/presentAssistantMessage-unknown-tool.spec.ts b/src/core/assistant-message/__tests__/presentAssistantMessage-unknown-tool.spec.ts index 78af9653c7..78a4a19e91 100644 --- a/src/core/assistant-message/__tests__/presentAssistantMessage-unknown-tool.spec.ts +++ b/src/core/assistant-message/__tests__/presentAssistantMessage-unknown-tool.spec.ts @@ -60,7 +60,6 @@ describe("presentAssistantMessage - Unknown Tool Handling", () => { }), }), }, - getTaskMode: vi.fn().mockResolvedValue("code"), say: vi.fn().mockResolvedValue(undefined), ask: vi.fn().mockResolvedValue({ response: "yesButtonClicked" }), } diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index b5a83882be..7b25db4e66 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -344,10 +344,7 @@ export async function presentAssistantMessage(cline: Task) { // Fetch state early so it's available for toolDescription and validation const state = await cline.providerRef.deref()?.getState() - const { customModes, experiments: stateExperiments, disabledTools } = state ?? {} - // Read the task-local mode, not the shared provider mode. - // A delegated child task may run in a different mode than its parent. - const taskMode = await cline.getTaskMode() + const { mode, customModes, experiments: stateExperiments, disabledTools } = state ?? {} const toolDescription = (): string => { switch (block.name) { @@ -620,7 +617,7 @@ export async function presentAssistantMessage(cline: Task) { validateToolUse( block.name as ToolName, - taskMode, + mode ?? defaultModeSlug, customModes ?? [], toolRequirements, block.params, @@ -927,7 +924,7 @@ export async function presentAssistantMessage(cline: Task) { } const result = await customTool.execute(customToolArgs, { - mode: taskMode, + mode: mode ?? defaultModeSlug, task: cline, }) diff --git a/src/core/environment/__tests__/getEnvironmentDetails.spec.ts b/src/core/environment/__tests__/getEnvironmentDetails.spec.ts index 0b4d63fbac..df47e83c21 100644 --- a/src/core/environment/__tests__/getEnvironmentDetails.spec.ts +++ b/src/core/environment/__tests__/getEnvironmentDetails.spec.ts @@ -117,7 +117,6 @@ describe("getEnvironmentDetails", () => { deref: vi.fn().mockReturnValue(mockProvider), [Symbol.toStringTag]: "WeakRef", } as unknown as WeakRef, - getTaskMode: vi.fn().mockResolvedValue("code"), } // Mock other dependencies. @@ -465,29 +464,4 @@ describe("getEnvironmentDetails", () => { const result = await getEnvironmentDetails(mockCline as Task, true) expect(result).toContain("File listing unavailable: unexpected string rejection") }) - - // Regression for issue #1623. - // Before the fix, the Current Mode block read the shared provider mode. - // A child delegated to "architect" mode would report "orchestrator" instead. - it("uses the task-local mode in the Current Mode block, not the provider mode", async () => { - // Provider mode stays "code"; task was delegated to "architect". - mockState.mode = "code" - ;(mockCline.getTaskMode as Mock).mockResolvedValue("architect") - ;(getFullModeDetails as Mock).mockResolvedValue({ - name: "🏗️ Architect", - roleDefinition: "You design software.", - customInstructions: "", - }) - - const result = await getEnvironmentDetails(mockCline as Task) - - expect(result).toContain("architect") - expect(result).not.toContain("code") - expect(getFullModeDetails).toHaveBeenCalledWith( - "architect", - [], - undefined, - expect.objectContaining({ cwd: mockCwd }), - ) - }) }) diff --git a/src/core/environment/getEnvironmentDetails.ts b/src/core/environment/getEnvironmentDetails.ts index 773870c304..0e7d18a57a 100644 --- a/src/core/environment/getEnvironmentDetails.ts +++ b/src/core/environment/getEnvironmentDetails.ts @@ -205,6 +205,7 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo // Add current mode and any mode-specific warnings. const { + mode, customModes, customModePrompts, experiments = {} as Record, @@ -212,9 +213,7 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo language, } = state ?? {} - // Read the task-local mode, not the shared provider mode. - // A delegated child task may run in a different mode than its parent. - const currentMode = await cline.getTaskMode() + const currentMode = mode ?? defaultModeSlug const modeDetails = await getFullModeDetails(currentMode, customModes, customModePrompts, { cwd: cline.cwd, From 20aebc49fd2e434c7db6a36fed71e2e86a378bb1 Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 13 Sep 2026 03:31:40 +0000 Subject: [PATCH 09/17] docs(lifecycle): make gap audit self-contained --- README.md | 3 -- docs/architecture/task-lifecycle-model.md | 52 ++++++++++------------- 2 files changed, 22 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 8b898d11df..cce7fa9cf3 100644 --- a/README.md +++ b/README.md @@ -105,9 +105,6 @@ Learn more: [Using Modes](https://docs.zoocode.dev/basic-usage/using-modes) • - **[Documentation](https://docs.zoocode.dev):** The official guide to installing, configuring, and mastering Zoo Code. -- **[Task lifecycle architecture and issue traceability](docs/architecture/task-lifecycle-model.md#open-issue-traceability):** - Bounded model checks, production mappings, known-unsafe witnesses, and open - lifecycle obligations. - **[Discord Server](https://discord.gg/VxfP4Vx3gX):** Join the community for real-time help and discussion. - **[Reddit Community](https://www.reddit.com/r/ZooCode/):** Share your diff --git a/docs/architecture/task-lifecycle-model.md b/docs/architecture/task-lifecycle-model.md index dcf76c4157..26f2927d36 100644 --- a/docs/architecture/task-lifecycle-model.md +++ b/docs/architecture/task-lifecycle-model.md @@ -155,17 +155,9 @@ The production mapping above names primary lifecycle transitions, not every muta CI runs `pnpm lifecycle:model-check` in `.github/workflows/code-qa.yml` after lint and type checking. Extension-host subtask and restart-persistence E2E run separately; a green umbrella command therefore says nothing about an omitted E2E boundary or an unmodeled downstream consumer. -### Open audit points +## Gap audit -- No executable refinement currently composes persisted lifecycle ownership with provider publication, parser scope, completion durability, or cleanup. Their independent checks must not be combined into a stronger end-to-end claim. -- Generic history upserts, reconciliation, repair replay, and migrations need an explicit mutation inventory before the four lifecycle reducers can be called the only status/lineage writers. -- Public `TaskCompleted` re-emission and its downstream consumers are not part of the completion-persistence state space. -- Task-local mode readers need a repository-wide consumer inventory or enforceable API boundary before universal reader isolation can be claimed; open #1623 and PR #1625 cover the confirmed regression paths only, while closed #921 records the write-side snapshot history. -- Production fan-out needs its own live-parent integration and E2E evidence before the planned abstract model can be promoted to production-backed coverage. - -## Open-issue traceability - -The following map contains only issues confirmed open on GitHub as of 2026-09-13 that still have a production, model-refinement, or enforcement obligation. Closed reports and their still-relevant verification limits are retained separately under [Verified history and limitations](#verified-history-and-limitations). Coverage status uses these evidence classes: +This is the authoritative active tracker. It is behavior-led and self-contained; issue links are historical provenance rather than specifications. Coverage status uses these evidence classes: - **Production-backed bounded:** exhaustive only for the declared state space while executing production functions. - **Abstract bounded:** exhaustive only for model-authored transitions; refinement depends on separate adapter tests. @@ -174,26 +166,26 @@ The following map contains only issues confirmed open on GitHub as of 2026-09-13 - **Planned-only:** specifies behavior not enabled in production. - **Type/static convention:** centralized typing or guidance without repository-wide enforcement. -| Open issue | Coverage status | Remaining production/model obligation | -| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [#1469](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1469): stale completion can clear a newer handoff across hosts. | **Known-unsafe witness** plus a production-backed reducer guard. | `completeDelegatedChild` rejects stale authoritative input, but store revalidation lacks exact-child ownership. CI ratchets the shortest violation; a passing checker confirms the witness, not safety. | -| [#1021](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1021): an in-flight save can restore lineage after abandonment. | **Known-unsafe witness** plus a production-backed detach reducer. | `abandonDelegatedChild` clears both sides, but stale tasks or hosts can reattach lineage. Monotonic detachment remains unproved and unsafe. | -| [#1623](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1623): delegated consumers can read shared provider mode despite a correct child snapshot. | Write-side premise: **production-backed bounded matrix**. Confirmed readers: **proxy/partial and known unsafe** pending #1625. | [PR #1625](https://github.com/Zoo-Code-Org/Zoo-Code/pull/1625) remains open and owns fixes/tests for environment, validation, and custom-tool readers. A repository-wide mode-consumer inventory or enforceable boundary is still absent. | -| [#369](https://github.com/Zoo-Code-Org/Zoo-Code/issues/369) and [#372](https://github.com/Zoo-Code-Org/Zoo-Code/issues/372): fan-out needs live-parent isolation, result ownership/routing, permits, and orphan cleanup. | **Planned-only abstract bounded** protocol with partial shipped primitives. | The two-sibling model imports no production fan-out transition. Production fan-out, rollback/isolation, live-parent injection, concurrent sibling E2E, and orphan handling remain open. | -| [#612](https://github.com/Zoo-Code-Org/Zoo-Code/issues/612): the CLI copied lifecycle status vocabulary. | Repaired symptom plus unresolved **type/static convention**. | CLI unions now include `interrupted`, but `HistoryTrigger.tsx` still duplicates the union. No shared imported owner or repository-wide rule prevents future drift. | - -## Verified history and limitations - -These closed issues remain useful provenance for regression checks and documented limits, but they are not open work items. Moving them out of open-issue traceability does not upgrade their evidence class or erase adjacent risks. - -| Closed issue | Resolved/historical evidence | Coverage and limitations retained | -| --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [#1453](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1453) under [#1279](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1279) | Completion readiness is guarded in production, with focused `Task`/`AttemptCompletionTool` tests and a fresh-host restart E2E. Both issues are closed. | The ordering model remains **abstract bounded**: at most two write starts, abstract durability/reopen, no power-loss claim, and no model of every downstream `TaskCompleted` consumer. | -| [#921](https://github.com/Zoo-Code-Org/Zoo-Code/issues/921) | Explicit handoff snapshots and the selector matrix cover the historical parallel-view write-side problem; the issue is closed. | This is **production-backed bounded** write-side evidence, not universal consumer isolation. The distinct open reader regression is tracked by #1623 and PR #1625. | -| [#920](https://github.com/Zoo-Code-Org/Zoo-Code/issues/920) | Production delta/merge behavior, bounded cross-instance scenarios, focused tests, and a real-filesystem smoke test cover distinct-task preservation; the issue is closed. | Same-field conflicts remain last-writer-wins; crash/filesystem/lock semantics and arbitrary processes are outside scope. Concrete unsafe same-record races remain tracked by open #1469 and #1021. | -| [#1468](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1468) | The production parser is request-scoped and the checker replays 924 bounded two-scope schedules; the issue is closed. | Coverage remains **production-backed bounded** and assumes no calls after finalization; transport behavior, arbitrary indices/request counts, and malformed histories are excluded. | - -The issue-derived cases intentionally map to bug classes rather than issue-specific flags. In particular, stale event ownership, monotonic terminal/detached state, explicit scope, and single-writer boundaries generalize to future concurrent task work. +| Gap | Invariant | Current evidence | Production/model boundary and runtime impact | Missing work and objective closure criteria | +| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Cross-host completion ownership | A completion may mutate a parent only while that parent authoritatively awaits the same child. | **Known-unsafe witness** plus a production-backed reducer guard. | The reducer rejects stale authoritative input, but disk revalidation checks status legality rather than exact-child ownership. A stale host can orphan the newer child. | Revalidate exact ownership under the disk lock; add deterministic two-store regression coverage; replace the expected witness with a universal bounded invariant. | +| Monotonic detachment | Message persistence must never restore lineage after abandonment clears it. | **Known-unsafe witness** plus a production-backed detach reducer. | A live task rebuilds lineage from stale fields; a later delta can restore parent/root IDs while preserving the interrupted status. | Give lifecycle fields a disk-authoritative write owner or tombstone/generation; prove stale metadata saves cannot alter them; promote the witness to an invariant. | +| Task-local mode consumers | Every mode-sensitive child operation must use the child task's immutable mode, not shared provider/view state. | Write side: **production-backed bounded matrix**. Read side: **proxy/partial and known unsafe**. | The selector snapshot can be correct while environment rendering and tool validation consume shared mode, causing wrong prompts or permissions. | Fix confirmed consumers, inventory other mode readers, add divergent-mode production tests, and enforce a task-local reader boundary before claiming universal isolation. | +| Serial versus fan-out delegation | The shipped serial path permits one awaited/running child and resumes its parent only after child release; fan-out must not be implied by an abstract model. | Serial ownership is **production-backed bounded**; the added fan-out checker is **planned-only abstract bounded**. | Production suspends the parent and uses a one-permit scheduler by default. The fan-out model imports no live-parent production transition, so it cannot prove fan-out behavior. | Either fix scheduler capacity at one and make concurrent fan-out explicitly unreachable, or implement live-parent isolation, rollback, routing, orphan cleanup, and E2E before promoting fan-out coverage. | +| Shared lifecycle status vocabulary | All consumers should derive task status from one exported owner. | **Type/static convention**; the visible interrupted-status symptom is repaired. | Persistence derives its alias from `HistoryItem`, but CLI history adapters still copy the union, allowing future drift. | Export one shared status type, consume it at CLI/persistence boundaries, and rely on typechecking or a static rule to reject copied incompatible vocabulary. | +| Cross-model refinement | No independent checker result may be combined into a stronger end-to-end claim without an executable boundary mapping. | **Explicit limitation** only. | Persistence, provider publication, parser scope, cleanup, and completion durability run as separate state spaces; omitted consumers can violate a premise after another checker passes. | Add production-grounded boundary events and a bounded joint strategy for each genuinely cross-model invariant, or keep claims explicitly local. | +| Completion event consumers | Public completion must not be treated as universally durable beyond the modeled readiness contract. | **Abstract bounded** model plus focused tests and one fresh-host E2E path. | The model abstracts durability and parent reopen; provider status metadata and downstream `TaskCompleted` consumers are outside its state. | Inventory downstream consumers and add refinement checks only for guarantees they require; retain power-loss, filesystem, retry-count, and liveness exclusions. | + +## Historical provenance + +- Cross-host completion ownership: [#1469](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1469). +- Monotonic detachment: [#1021](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1021). +- Task-local mode isolation: [#921](https://github.com/Zoo-Code-Org/Zoo-Code/issues/921), [#1623](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1623), and runtime-fix [PR #1625](https://github.com/Zoo-Code-Org/Zoo-Code/pull/1625). +- Fan-out product backlog: [#369](https://github.com/Zoo-Code-Org/Zoo-Code/issues/369) and [#372](https://github.com/Zoo-Code-Org/Zoo-Code/issues/372). +- Shared status vocabulary: [#612](https://github.com/Zoo-Code-Org/Zoo-Code/issues/612). +- Completion visibility history: [#1453](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1453) and [#1279](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1279). +- Cross-instance history preservation: [#920](https://github.com/Zoo-Code-Org/Zoo-Code/issues/920). +- Parser request scoping: [#1468](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1468). ## Extending the model From 8d00b944f5bb0a44edc186f8f92637f77da6dedb Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 13 Sep 2026 13:15:23 +0000 Subject: [PATCH 10/17] docs(lifecycle): add exhaustive verification gap report --- .../architecture/task-lifecycle-gap-report.md | 204 ++++++++++++++++++ docs/architecture/task-lifecycle-model.md | 2 + 2 files changed, 206 insertions(+) create mode 100644 docs/architecture/task-lifecycle-gap-report.md diff --git a/docs/architecture/task-lifecycle-gap-report.md b/docs/architecture/task-lifecycle-gap-report.md new file mode 100644 index 0000000000..72288760d2 --- /dev/null +++ b/docs/architecture/task-lifecycle-gap-report.md @@ -0,0 +1,204 @@ +# Task lifecycle verification GAP report + +## Purpose and scope + +This report inventories Zoo Code task lifecycle state, mutation, persistence, scheduling, streaming, event, and verification boundaries. It is a documentation and formal-model audit, not a claim that the listed production gaps are fixed. + +The audit covers tracked TypeScript, JSON, YAML, and Markdown under `packages/`, `src/`, `apps/cli`, `apps/vscode-e2e`, `scripts/`, `.github/workflows`, and `docs/architecture`. It traces production symbols to bounded models, focused tests, extension-host E2E, and CI entry points. + +“Exhaustive” means exhaustive over the repository paths, symbol families, and search terms listed here at the audited commit. It does not include ignored/generated output, deployment branch-protection settings, runtime telemetry, dynamically constructed names that evade text search, or behavior in dependencies. GitHub issue links are historical provenance only; stable `LIFE-GAP-*` IDs own the active burn-down. + +## Methodology and audit criteria + +The inventory used structural searches for status and lineage fields, lifecycle reducers, store mutations, registry/stack operations, scheduler/semaphore queues, task start/resume/abort/dispose paths, persistence retries, stream scopes, lifecycle events, webview/API/IPC ingress, copied unions, tests, scripts, and workflow commands. Each claim was then classified by whether the checker executes production code or a model-authored proxy. + +Primary references define the audit criteria: + +- Lamport’s [High-Level View of TLA+](https://lamport.azurewebsites.net/tla/high-level-view.html) defines behavior as state sequences, distinguishes invariants from liveness, and explains that fairness is needed for steps that must eventually occur. Repository criterion: a safety explorer must not claim eventual cleanup, progress, retry, or completion without explicit temporal/fairness semantics. +- Quint’s [model-checker documentation](https://quint-lang.org/docs/model-checkers) states that model checking verifies properties of the model and that bounded checking is tied to a maximum execution length. Repository criterion: every pass claim names its state/depth/task/retry bounds and does not imply unbounded production correctness. +- Quint’s [model-based testing guidance](https://quint-lang.org/docs/model-based-testing) explicitly separates “the design is right” from “the implementation matches the design” and recommends replaying model traces or validating production traces. Repository criterion: model-authored transitions are proxy evidence until a production adapter, generated trace driver, or trace validator connects them to code. +- The [Alloy file-system tutorial](https://alloytools.org/tutorials/online/maintext-FS-1.html) states that “no solution found” guarantees only the selected finite scope and warns that facts can overconstrain away examples. Repository criterion: semantic landmarks and action reachability are required alongside invariants, and finite scope is always disclosed. +- Jepsen’s [consistency reference](https://jepsen.io/consistency) defines a consistency model as the set of legal histories. Repository criterion: cross-host claims must state which histories, conflicts, and dependencies are allowed, not merely that a mutex exists. +- SQLite’s [transactional guarantee](https://www.sqlite.org/transactional.html) ties crash atomicity to explicit crash and power-failure simulation. Repository criterion: Zoo Code’s per-file smoke tests cannot support crash-atomic or power-loss claims without an equivalent failure-injection harness. + +Evidence classes used below: + +| Class | Meaning | +| ------------------------- | ------------------------------------------------------------------------------------------ | +| Production-backed bounded | The checker executes production functions for every state/schedule within declared bounds. | +| Abstract bounded | The checker exhausts model-authored transitions; production refinement is separate. | +| Focused production test | A deterministic production path is exercised, but not all model interleavings. | +| E2E witness | A real extension-host boundary is exercised for one controlled history. | +| Proxy-only | The check demonstrates a premise or analogous mechanism, not the production claim. | +| Known-unsafe witness | CI preserves a reproducible violating history; a pass confirms the witness still exists. | +| Unmodeled | No executable property currently covers the boundary. | + +## Exhaustive lifecycle inventory + +### State owners + +| Owner | State | Primary symbols | Authority boundary | +| ------------------------ | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | +| Persisted history schema | Status, lineage, completion summary, pending action, accounting | `packages/types/src/history.ts`: `historyItemSchema`, `pendingTaskActionSchema` | Restart-visible record shape; optional status normalizes to active in lifecycle code. | +| Lifecycle reducers | Legal persisted transitions and parent-child ownership | `src/core/task-persistence/taskLifecycle.ts`: `delegateTaskToChild`, `interruptDelegatedChild`, `completeDelegatedChild`, `abandonDelegatedChild` | Pure transition authority when inputs are authoritative. | +| History store | Per-task files, cache, deltas, reconciliation, migration, repair | `src/core/task-persistence/TaskHistoryStore.ts`; `taskStoreConcurrency.ts` | Per-task files are authoritative; each extension host has an independent cache. | +| Live task | Abort/dispose, ask state, run ownership, mode/profile, streaming, message and completion readiness | `src/core/task/Task.ts` | Process-local execution state; not equivalent to persisted status. | +| Task registry | Live instances, compatibility stack, current focus | `src/core/task/TaskRegistry.ts` | Focus/publication owner; not scheduler admission or persisted lineage. | +| Provider | Transition queues, current task, registry integration, persistence orchestration, event forwarding | `src/core/webview/ClineProvider.ts` | Coordinates layers but does not make them one transaction. | +| Scheduler/semaphore | Waiting, admission, held permits, cancellation, release | `src/core/task/TaskScheduler.ts`; `src/utils/TaskSemaphore.ts` | Provider-local execution gate; default capacity is one. | +| Message queue | Queued user feedback and claims | `src/core/message-queue/MessageQueueService.ts` | Memory-only; disposal clears membership and claims. | +| Parser scope | Raw-index and tool-ID accumulators | `src/core/assistant-message/NativeToolCallParser.ts` | Request-scope parser state; transport and Task caller protocol are separate. | +| Event surfaces | Task, provider, public API, IPC, and webview lifecycle notifications | `packages/types/src/task.ts`, `events.ts`, `ipc.ts`; `src/extension/api.ts` | Related but non-identical payload and settlement contracts. | + +### Persisted lifecycle vocabulary and mutations + +Persisted statuses are `active`, `completed`, `delegated`, and `interrupted`. `VALID_TASK_STATUS_TRANSITIONS` permits active to delegated/completed/interrupted, delegated to active, interrupted to completed, and no transition from completed. Lineage fields are `rootTaskId`, `parentTaskId`, `delegatedToId`, `childIds`, `awaitingChildId`, `completedByChildId`, `completionResultSummary`, and `pendingAction`. + +| Mutation boundary | Production symbols | Modeled/tested evidence | Not covered by that evidence | +| --------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------- | -------------------------------------------------------------------------------- | +| Ordinary upsert | `TaskHistoryStore.upsert`, `upsertCore`, `writeTaskFile` | Store unit/cross-instance tests; shared-store delta model | Arbitrary process count, crashes, lock staleness, malicious/malformed records. | +| Single-record atomic update | `atomicReadAndUpdate` | Provider delegation tests; host-local lock abstraction | Cross-host compare-and-swap ownership. | +| Pair update | `atomicUpdatePair` | Pair-order/failure model and tests | Cross-host or crash atomicity; second-write failure can expose committed prefix. | +| Reconciliation | `reconcile`, `reconcileDelegationState` | Reconciliation tests | Immediate convergence, watcher delivery, concurrent repair histories. | +| Journaled repair | `repairActiveDelegation`, `replayDelegationRepairIntent` | Repair/restart tests | Other pair operations have no WAL/intent record. | +| Legacy migration/import | `migrateFromGlobalState`, `importRooTaskHistory` | Migration/import tests | Unified validation policy across generic store and importer. | +| Deletion | `delete`, `deleteMany`, `ClineProvider.deleteTaskWithId` | Focused deletion tests | Atomic history/checkpoint/directory deletion; unlink failures are best effort. | +| Live message save | `Task.saveClineMessages`, `taskMetadata`, `ClineProvider.updateTaskHistory` | Persistence tests; known stale-save witness | Disk-authoritative lifecycle-field ownership. | + +Copied persisted-status membership occurs in `packages/types/src/task.ts`, `src/core/task-persistence/taskMetadata.ts`, `src/core/task/Task.ts`, `apps/cli/src/ui/types.ts`, `HistoryTrigger.tsx`, and the core task-history reader. The runtime `TaskStatus` vocabulary (`running`, `interactive`, `resumable`, `idle`, `none`) is intentionally separate but similarly named. + +### Scheduler and queue transitions + +| Queue/lock | Transition | Scope | Evidence boundary | +| --------------------------- | ----------------------------------------------------------------------- | ------------------------------- | --------------------------------------------------------------- | +| Task semaphore | submitted → waiting → admitted → running → released; queued → cancelled | One provider | Unit tested; modeled permits are abstract. | +| Per-parent transition queue | delegation/interruption/completion/abandonment serialization | Static queue keyed by parent ID | Provider tests and handoff model; no cross-process transaction. | +| History restoration queue | request → serialized rehydration → install | One provider | Provider tests; no model state. | +| Provider-profile queue | profile mutation serialization | One provider | Settings/provider tests; outside lifecycle models. | +| Message queue | add → claim → persist → remove, or release on failure | One live task | Claim path tested; legacy dequeue-before-submit path remains. | + +Registry publication can precede scheduler admission. Therefore “current”, “running”, “active”, and “persisted active” are not interchangeable states. + +### Delegation, interruption, cancellation, completion, and abandonment + +| Flow | Production path | Key ordering | Verification | +| --------------------- | ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | +| Delegate | `NewTaskTool` → `delegateParentAndOpenChild` | Snapshot context; flush/remove parent; create paused child; commit parent ownership; then schedule child | Reducer model, handoff model, provider tests, subtask E2E. | +| Interrupt on eviction | `evictCurrentTask` → `markDelegatedChildInterrupted` | Remove live child; revalidate parent ownership; persist interrupted child | Reducer model and provider/E2E tests. | +| User cancel | `cancelTaskInternal` → request abort → bounded drain/save → interrupted persistence/rehydration | Live flags and persisted status converge through multiple fallbacks | Cleanup model is abstract; provider and E2E tests cover selected paths. | +| Complete standalone | `AttemptCompletionTool` → persistence readiness → `TaskCompleted` | Public event follows accepted completion and assistant-history visibility | Abstract completion model, focused tests, fresh-host E2E. | +| Complete child | `AttemptCompletionTool` → `reopenParentFromDelegation` | Validate IDs; write parent messages; remove child; pair update; publish; schedule parent | Reducer/handoff models and provider/E2E tests; message/lifecycle transaction is unmodeled. | +| Abandon | `abandonSubtask` | Require interrupted child; remove live child; pair-detach; process-local stale guard | Reducer/shared-store models and focused/E2E tests. | +| Resume | webview/API/IPC → `resumeTask`/`showTaskWithId` → rehydrate | Surfaces differ in awaiting, error propagation, and publication | Focused/E2E tests; no unified model. | + +### Streams and event consumers + +Each API request creates a parser scope. Provider `tool_call_partial` chunks pass through `NativeToolCallParser`, then `Task` turns parser events into partial/final assistant blocks. End-of-stream finalization is modeled for two scopes; abort/failure cleanup and provider transform semantics are separate. + +`Task` may detach an iterator to drain final usage. That continuation can update accounting/messages after foreground processing stops. Lifecycle generation ownership is not attached to those writes. + +Task events are forwarded by `ClineProvider`, enriched and re-emitted by `src/extension/api.ts`, and serialized to IPC. Node `EventEmitter.emit()` does not await async listeners, so event notification and listener settlement are separate contracts. Public completion status persistence and downstream consumers are not in the completion model. + +## Production-to-model-to-test-to-CI matrix + +| Production boundary | Model/property | Production tests | E2E | CI path | Classification | +| ------------------------- | --------------------------------------------------------------- | --------------------------------------------- | ---------------------------- | ---------------------------------- | ----------------------------------------------------- | +| Lifecycle reducers | Exact parent-child ownership, acyclicity, terminal immutability | `taskLifecycle.spec.ts` | `subtasks.test.ts` | `lifecycle:model-check`, unit, E2E | Production-backed bounded | +| Delta/merge/store | Field preservation, status legality, pair order/failure | store unit, cross-instance, real-lock smoke | None direct | model umbrella, unit | Production-backed bounded plus known-unsafe witnesses | +| Handoff selector/reducers | Commit-before-start, publication, permit/redelegation ordering | provider handoff, scheduler, delegation tests | subtask profile/resume paths | model umbrella, unit, E2E | Mixed production/abstract | +| Fan-out | Two siblings, result writer/delivery, orphan cleanup | Scheduler primitives only | None | model umbrella | Planned-only abstract | +| Cleanup | At-most-once abort/dispose, settlement order, provider drain | Task/provider cleanup tests | Indirect cancellation paths | model umbrella, unit, E2E | Abstract bounded plus focused tests | +| Parser scopes | Scope-owned IDs/arguments, exactly-once finalization | parser/provider stream tests | Indirect | model umbrella, unit | Production-backed bounded replay | +| Completion readiness | Durability before event, retry/cancel/reopen ordering | Task/completion tool tests | fresh-host restart | model umbrella, unit, E2E | Abstract bounded plus refinement witnesses | +| Mode handoff/readers | Selector snapshot and observable provider/task divergence | selector/provider tests | profile handoff | model umbrella, unit, E2E | Write-side production-backed; readers proxy/partial | +| Status vocabulary | Shared schema plus copied unions | CLI/history tests | None | typecheck, unit | Type/static convention | + +CI runs lint, typecheck, and `pnpm lifecycle:model-check` in `.github/workflows/code-qa.yml`. Unit/integration tests run separately on Ubuntu and Windows. Mocked extension-host E2E and the explicit restart-persistence phase run in `.github/workflows/e2e.yml`. Workflow files prove invocation, not branch-protection required-check configuration. E2E may reuse an identical-source pass marker on pull requests. + +## Assumptions and exclusions + +- Every checker is finite and protocol-local. Bounds are documented in the parent architecture page and checker constants. +- Safety invariants do not establish liveness. No checker includes fairness sufficient to prove eventual queue admission, cleanup, persistence, retry, or completion. +- A model-authored provider, scheduler, cleanup, fan-out, or durability action is not production refinement by itself. +- Per-file locks are treated as effective mutual exclusion in the abstract store model. Lock implementation, stale-lock recovery, rename semantics, process crashes, and power loss are excluded. +- Pair writes, lifecycle plus message writes, deletion plus filesystem cleanup, and registry plus persistence publication are not transactions. +- Parser replay fixes two scopes, one raw index, and local action order. Transport transforms and arbitrary malformed histories are excluded. +- Focused tests and E2E are representative histories, not exhaustive interleavings. +- No public-runtime telemetry or production traces were available for trace validation. + +## Ranked GAP register + +Severity reflects plausible data loss, ownership corruption, permission/context errors, or stuck work. Confidence reflects direct source evidence, deterministic witness, or inference. + +| ID | Severity | Confidence | Gap and production impact | Witness/reproducer | Dependencies | Objective closure criteria | +| ------------ | ---------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| LIFE-GAP-001 | Critical | High | Stale cross-host completion can clear a newer handoff and orphan its child. | Exact shortest witness in shared-store checker. | Disk-authoritative ownership guard or generation. | Deterministic two-store test passes; authoritative child ID is revalidated under disk lock; witness becomes universal invariant. | +| LIFE-GAP-002 | High | High | Stale message save can restore abandoned lineage. | Exact shortest stale-save/abandon witness. | Lifecycle-field ownership, tombstone, or generation. | Stale save cannot alter detached lineage in production test or bounded model; witness promoted. | +| LIFE-GAP-003 | High | High | Legacy dequeue-before-submit can lose queued user feedback on submission failure. | `Task.processQueuedMessages` removes before async submit. | Standardize claim/persist/ack path. | Every queue consumer claims first, removes after durable acceptance, releases on failure; failure test retains message. | +| LIFE-GAP-004 | High | High | Pair writes are not cross-host/crash atomic; partial lifecycle state is observable. | Modeled second-write failure landmark. | WAL/repair intent or explicitly idempotent recovery per pair operation. | Crash injection at each write boundary converges to a documented legal state. | +| LIFE-GAP-005 | High | High | Delegation creates child state and commits parent ownership in separate failure domains. | Provider rollback tests cover selected failures. | Idempotent operation ID or repair intent. | Failure injection before/after every persistence/publication step restores parent or completes delegation without orphan state. | +| LIFE-GAP-006 | High | Medium-high | Parent completion messages can persist before lifecycle completion commit fails. | Source ordering in `reopenParentFromDelegation`. | Transaction/intent or reorder with recovery. | Inject pair-write failure and prove no stale result context, or prove replay safely completes the lifecycle. | +| LIFE-GAP-007 | High | Medium-high | Task-local mode reader refinement is incomplete; wrong shared mode can affect prompts or permissions. | Divergent mode witness; known consumer regression. | Reader inventory and task-local API boundary. | Every mode-sensitive reader classified; required readers use task-local mode; divergent production tests cover both permission directions. | +| LIFE-GAP-008 | High | High | Responses API argument-only deltas may be dropped; absent indices can alias calls. | Transform requires ID/name and defaults index to zero. | Provider transform correlation design. | Multi-delta and concurrent index-less production tests reconstruct isolated calls; parser model includes mapped transform events. | +| LIFE-GAP-009 | Medium | High | Async lifecycle event listeners have no awaited settlement contract. | Async listeners registered on Node EventEmitter. | Classify notification versus barrier events. | Barrier side effects move to awaited methods; notification listeners have contained rejection tests and documented ordering. | +| LIFE-GAP-010 | Medium | Medium-high | Detached usage drain can write after abort, replacement, delegation, or a newer request generation. | Background iterator mutates/persists without generation guard. | Request-generation ownership. | Late drain may account valid usage but cannot mutate stale UI/message/lifecycle state; controlled delayed-stream test passes. | +| LIFE-GAP-011 | Medium | High | Completion model excludes terminal status persistence and downstream public consumers. | Model ends at emission readiness. | Consumer inventory and contract. | Consequential consumers are enumerated; required status/metadata ordering has production refinement tests. | +| LIFE-GAP-012 | Medium | High | No persisted attempt/generation distinguishes delayed pre-interruption completion from valid post-resume completion of the same child. | Documented model exclusion. | Persisted generation token. | Reducer/store/API/model reject stale generation while accepting resumed generation; restart E2E covers it. | +| LIFE-GAP-013 | Medium | High | Task lifecycle status vocabulary is copied across schema, task metadata, Task, CLI, and history reader. | Literal union inventory. | Shared exported schema-derived type. | Consumers import one owner; CI/static check rejects incompatible local copies. | +| LIFE-GAP-014 | Medium | High | Fan-out checker is planned-only but runs in the green umbrella, inviting overclaim. | No production imports or fan-out E2E. | Product decision. | Either enforce serial capacity as invariant, or implement fan-out adapters and E2E before reclassifying. | +| LIFE-GAP-015 | Medium | High | Independent checks do not establish end-to-end refinement. | Seven disjoint state spaces. | Boundary mappings and tractable joint bounds. | Add joint checker/trace validation for each cross-model claim, or keep every claim explicitly local. | +| LIFE-GAP-016 | Medium | High | Traceability is documentary and can drift from scripts, symbols, tests, and CI. | Shared-store scenario count has drifted in documentation. | Machine-readable manifest/checker summaries. | CI validates stable IDs, model membership, bounds, symbol/test paths, and workflow invocation. | +| LIFE-GAP-017 | Medium | High | Store cache records are exposed without cloning; external mutation may bypass locking. | `get`/`getAll` return cached objects. | Immutability/read API decision. | Freeze/clone records or prove callers cannot mutate; mutation regression test. | +| LIFE-GAP-018 | Medium | High | Ordinary history-file reads cast JSON instead of applying the shared schema. | Store reconciliation/read path. | Validation/quarantine policy. | Malformed records are rejected or quarantined deterministically with tests and recovery documentation. | +| LIFE-GAP-019 | Medium | High | Generic task IDs lack the importer’s explicit path-safety validation. | Importer validates IDs; generic paths interpolate IDs. | Shared safe-ID boundary. | Every filesystem task ID passes one validator; traversal and separator tests cover all entry points. | +| LIFE-GAP-020 | Medium | Medium-high | Watch/reconcile convergence is eventual and failure-tolerant, not coherent. | Debounced watcher plus periodic scan. | Version/notification or documented eventual contract. | Define stale-read window and convergence property; multi-host test covers missed watcher event and concurrent update. | +| LIFE-GAP-021 | Medium | High | Store disposal does not await queued writes. | Synchronous `dispose` stops watcher/timer only. | Async drain/close contract. | Disposal awaits or explicitly cancels writes; no post-dispose writes in deterministic test. | +| LIFE-GAP-022 | Medium | High | Public clear and webview clear use different delegated-child semantics. | API uses eviction; webview removes directly. | One clear contract. | All ingress paths converge on the same lifecycle transition and tests assert identical persisted results. | +| LIFE-GAP-023 | Medium | High | History deletion can be resurrected after swallowed unlink failure. | Cache removal precedes best-effort unlink. | Tombstone or surfaced failure/retry. | Inject unlink failure and prove item stays deleted or operation reports failure without false success. | +| LIFE-GAP-024 | Medium | High | Parser cleanup depends on normal finalization or garbage collection. | Weak scope maps lack universal request `finally`. | Request-level cleanup owner. | Abort/error/success all clear active parser state in production integration tests. | +| LIFE-GAP-025 | Medium | High | Abort listeners may accumulate during successful stream chunks. | Per-chunk listener removed only by abort. | Settle-time listener cleanup. | Long stream keeps bounded listener count and removes listeners on both race outcomes. | +| LIFE-GAP-026 | Medium | Medium-high | Usage-drain timeout cannot interrupt a permanently pending `iterator.next()`. | Elapsed time checked before await. | Deadline race/abort. | Hung iterator settles drain within wall-clock bound in fake-timer test. | +| LIFE-GAP-027 | Medium | High | Task-level delegation listeners are untyped/dead while provider listeners own the same public events. | `src/extension/api.ts` registers both paths. | Single typed event owner. | Remove duplicate/dead listeners or define one source; public API test proves exactly-once emission. | +| LIFE-GAP-028 | Low-medium | High | `TaskSpawned` payload semantics differ across task/provider/public surfaces. | Child-only, ambiguous task ID, and parent+child forms. | Event contract normalization. | Payloads use explicit names and adapters are type-checked with compatibility tests. | +| LIFE-GAP-029 | Low-medium | High | `Task.taskStatus` and `TaskRegistry.getRunning` are projections, not scheduler/persistence truth. | Ask markers and abort flags only. | Naming/contract clarification. | Rename or document exact predicates; callers stop using them as stronger lifecycle evidence. | +| LIFE-GAP-030 | Low-medium | Medium | `Task.run()` may resolve immediately if another path already started the task. | `_started` short-circuit versus scheduler callback. | Single start owner. | Scheduler-facing start returns the actual run promise; duplicate-start test proves settlement identity. | +| LIFE-GAP-031 | Low-medium | High | Queue state is memory-only and cleared on disposal. | `MessageQueueService.dispose`. | Product durability decision. | Document intentional loss or persist claims/messages with restart tests. | +| LIFE-GAP-032 | Low-medium | Medium | Webview abandonment handler exists without a confirmed production UI sender. | Protocol/handler search only. | Reachability decision. | Add supported sender/E2E or remove/deprecate unreachable command. | +| LIFE-GAP-033 | Low-medium | High | Resume ingress differs in awaiting and error propagation. | Webview/API/IPC adapters diverge. | Shared resume operation contract. | Contract tests compare result/error/publication semantics for each surface. | +| LIFE-GAP-034 | Low | High | Bounds and model metadata are handwritten and not mechanically synchronized. | Constants, prose, and console summaries duplicate values. | Machine-readable checker metadata. | CI compares emitted metadata with docs/manifest and rejects undocumented bound/action/landmark changes. | + +## Burn-down dependencies + +| Dependency | Enables | +| ---------------------------------------------- | ----------------------------------- | +| Disk-authoritative ownership/generation design | LIFE-GAP-001, 002, 012 | +| Durable operation intent/recovery design | LIFE-GAP-004, 005, 006, 023 | +| Task-local execution-context owner | LIFE-GAP-007, fan-out side of 014 | +| Request generation and terminal cleanup owner | LIFE-GAP-010, 024, 025, 026 | +| Event notification/barrier contract | LIFE-GAP-009, 011, 027, 028 | +| Machine-readable lifecycle manifest | LIFE-GAP-013, 016, 034 | +| Product decision on serial versus fan-out | LIFE-GAP-014 and future fan-out E2E | + +## Mechanically useful follow-up checklist + +- [ ] Assign an owner and target PR to each active `LIFE-GAP-*` ID without renumbering existing IDs. +- [ ] Add the ID to production tests, model actions/properties, and PR descriptions that address it. +- [ ] Preserve a deterministic failing test or shortest witness before changing production behavior. +- [ ] State whether the resulting evidence is production-backed, abstract, proxy, or E2E. +- [ ] Add negative/failure-path coverage, not only the successful transition. +- [ ] Record bounds and prove action/landmark reachability so an overconstrained model cannot pass vacuously. +- [ ] For cross-host work, enumerate legal histories and inject stale cache, partial write, lock, restart, and reconciliation orderings. +- [ ] For crash-consistency claims, add interruption/failure injection at every durable step. +- [ ] For liveness claims, define fairness and progress assumptions explicitly; do not infer them from safety exploration. +- [ ] For cross-model claims, supply an executable boundary mapping or keep the claim local. +- [ ] Update this report’s inventory, matrix, severity, dependencies, and closure evidence in the same PR. +- [ ] Run `pnpm lifecycle:model-check`, focused production tests, `pnpm test`, typecheck, lint, and required E2E before marking a gap closed. +- [ ] Move issue links only within historical provenance; stable burn-down IDs remain the active identity. + +## Completeness statement + +At the audited commit, this report covers every tracked definition and directly discoverable caller matching the lifecycle domains in scope, all seven umbrella checkers, their documented bounds/properties, primary focused suites, lifecycle E2E files, root package scripts, and CI workflow invocations. It does not claim semantic completeness for dynamic calls, generated code, dependencies, ignored files, deployment settings, or production histories. A future code change can invalidate completeness; `LIFE-GAP-016` and `LIFE-GAP-034` exist specifically to make this inventory mechanically maintainable. + +## Historical provenance + +Relevant historical reports include [#1469](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1469), [#1021](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1021), [#1623](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1623), [#369](https://github.com/Zoo-Code-Org/Zoo-Code/issues/369), [#372](https://github.com/Zoo-Code-Org/Zoo-Code/issues/372), [#612](https://github.com/Zoo-Code-Org/Zoo-Code/issues/612), [#1453](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1453), [#1279](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1279), [#921](https://github.com/Zoo-Code-Org/Zoo-Code/issues/921), [#920](https://github.com/Zoo-Code-Org/Zoo-Code/issues/920), and [#1468](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1468). These links provide provenance only; closure is governed by the objective criteria above. diff --git a/docs/architecture/task-lifecycle-model.md b/docs/architecture/task-lifecycle-model.md index 26f2927d36..a793896710 100644 --- a/docs/architecture/task-lifecycle-model.md +++ b/docs/architecture/task-lifecycle-model.md @@ -159,6 +159,8 @@ CI runs `pnpm lifecycle:model-check` in `.github/workflows/code-qa.yml` after li This is the authoritative active tracker. It is behavior-led and self-contained; issue links are historical provenance rather than specifications. Coverage status uses these evidence classes: +The exhaustive repository inventory, ranked stable burn-down register, source-based methodology, and follow-up checklist live in the [Task lifecycle verification GAP report](./task-lifecycle-gap-report.md). This page remains the executable model-suite specification. + - **Production-backed bounded:** exhaustive only for the declared state space while executing production functions. - **Abstract bounded:** exhaustive only for model-authored transitions; refinement depends on separate adapter tests. - **Known-unsafe witness:** CI preserves a reproducible violation and does not claim the property holds. From 378abeeb532414688c69bd2b7bc97e0e02e118c7 Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 13 Sep 2026 13:42:22 +0000 Subject: [PATCH 11/17] docs(lifecycle): audit subtask todo isolation --- .../architecture/task-lifecycle-gap-report.md | 109 ++++++++++-------- docs/architecture/task-lifecycle-model.md | 19 +-- 2 files changed, 73 insertions(+), 55 deletions(-) diff --git a/docs/architecture/task-lifecycle-gap-report.md b/docs/architecture/task-lifecycle-gap-report.md index 72288760d2..a021e071d9 100644 --- a/docs/architecture/task-lifecycle-gap-report.md +++ b/docs/architecture/task-lifecycle-gap-report.md @@ -49,6 +49,7 @@ Evidence classes used below: | Message queue | Queued user feedback and claims | `src/core/message-queue/MessageQueueService.ts` | Memory-only; disposal clears membership and claims. | | Parser scope | Raw-index and tool-ID accumulators | `src/core/assistant-message/NativeToolCallParser.ts` | Request-scope parser state; transport and Task caller protocol are separate. | | Event surfaces | Task, provider, public API, IPC, and webview lifecycle notifications | `packages/types/src/task.ts`, `events.ts`, `ipc.ts`; `src/extension/api.ts` | Related but non-identical payload and settlement contracts. | +| Task todo state | Initial and updated task-local checklist | `Task.todoList`, `NewTaskTool`, `UpdateTodoListTool`, `currentTaskTodos` | Initial todos are live-task state; later updates are recoverable from task messages. | ### Persisted lifecycle vocabulary and mutations @@ -79,6 +80,19 @@ Copied persisted-status membership occurs in `packages/types/src/task.ts`, `src/ Registry publication can precede scheduler admission. Therefore “current”, “running”, “active”, and “persisted active” are not interchangeable states. +### Task and subtask todo ownership + +The normal creation path does not inherit the parent list. The model supplies optional `new_task.todos`; `NewTaskTool.execute` parses only that argument into a fresh array, stores it in a pending action while approval is unresolved, and forwards it as `initialTodos` through `delegateParentAndOpenChild` and `createTask`. The parent task supplies lineage and workspace context, not todos. + +`Task` assigns `initialTodos` to its process-local `todoList`. `UpdateTodoListTool` later writes task-scoped `updateTodoList` messages, and `restoreTodoListForTask` reconstructs the latest list from the reopened task's own messages. `ClineProvider.getStateToPostToWebview` publishes the focused task's `currentTaskTodos`; `ChatView` and `TodoListDisplay` render that state or the current task's message-derived fallback. No frontend path intentionally copies a parent list. + +The reported appearance of inheritance therefore needs two controls: + +1. If the model emits child todos matching the parent, that is explicit tool-call content and not evidence of IDE aliasing. +2. If child todos disappear or change after navigation/restart, that is an IDE-side task-state persistence/scoping question. + +Initial child todos are not placed in a task message or `HistoryItem`. Rehydration constructs a new `Task` without `initialTodos`; before the child's first `update_todo_list`, message-derived restoration yields an empty list. Constructor and todo setter APIs also assign arrays directly, creating latent aliasing for programmatic callers even though the normal `new_task` parser creates fresh objects. No current model contains todo state, task-ID/generation-scoped todo publication, or rehydration equivalence. + ### Delegation, interruption, cancellation, completion, and abandonment | Flow | Production path | Key ordering | Verification | @@ -130,54 +144,56 @@ CI runs lint, typecheck, and `pnpm lifecycle:model-check` in `.github/workflows/ Severity reflects plausible data loss, ownership corruption, permission/context errors, or stuck work. Confidence reflects direct source evidence, deterministic witness, or inference. -| ID | Severity | Confidence | Gap and production impact | Witness/reproducer | Dependencies | Objective closure criteria | -| ------------ | ---------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -| LIFE-GAP-001 | Critical | High | Stale cross-host completion can clear a newer handoff and orphan its child. | Exact shortest witness in shared-store checker. | Disk-authoritative ownership guard or generation. | Deterministic two-store test passes; authoritative child ID is revalidated under disk lock; witness becomes universal invariant. | -| LIFE-GAP-002 | High | High | Stale message save can restore abandoned lineage. | Exact shortest stale-save/abandon witness. | Lifecycle-field ownership, tombstone, or generation. | Stale save cannot alter detached lineage in production test or bounded model; witness promoted. | -| LIFE-GAP-003 | High | High | Legacy dequeue-before-submit can lose queued user feedback on submission failure. | `Task.processQueuedMessages` removes before async submit. | Standardize claim/persist/ack path. | Every queue consumer claims first, removes after durable acceptance, releases on failure; failure test retains message. | -| LIFE-GAP-004 | High | High | Pair writes are not cross-host/crash atomic; partial lifecycle state is observable. | Modeled second-write failure landmark. | WAL/repair intent or explicitly idempotent recovery per pair operation. | Crash injection at each write boundary converges to a documented legal state. | -| LIFE-GAP-005 | High | High | Delegation creates child state and commits parent ownership in separate failure domains. | Provider rollback tests cover selected failures. | Idempotent operation ID or repair intent. | Failure injection before/after every persistence/publication step restores parent or completes delegation without orphan state. | -| LIFE-GAP-006 | High | Medium-high | Parent completion messages can persist before lifecycle completion commit fails. | Source ordering in `reopenParentFromDelegation`. | Transaction/intent or reorder with recovery. | Inject pair-write failure and prove no stale result context, or prove replay safely completes the lifecycle. | -| LIFE-GAP-007 | High | Medium-high | Task-local mode reader refinement is incomplete; wrong shared mode can affect prompts or permissions. | Divergent mode witness; known consumer regression. | Reader inventory and task-local API boundary. | Every mode-sensitive reader classified; required readers use task-local mode; divergent production tests cover both permission directions. | -| LIFE-GAP-008 | High | High | Responses API argument-only deltas may be dropped; absent indices can alias calls. | Transform requires ID/name and defaults index to zero. | Provider transform correlation design. | Multi-delta and concurrent index-less production tests reconstruct isolated calls; parser model includes mapped transform events. | -| LIFE-GAP-009 | Medium | High | Async lifecycle event listeners have no awaited settlement contract. | Async listeners registered on Node EventEmitter. | Classify notification versus barrier events. | Barrier side effects move to awaited methods; notification listeners have contained rejection tests and documented ordering. | -| LIFE-GAP-010 | Medium | Medium-high | Detached usage drain can write after abort, replacement, delegation, or a newer request generation. | Background iterator mutates/persists without generation guard. | Request-generation ownership. | Late drain may account valid usage but cannot mutate stale UI/message/lifecycle state; controlled delayed-stream test passes. | -| LIFE-GAP-011 | Medium | High | Completion model excludes terminal status persistence and downstream public consumers. | Model ends at emission readiness. | Consumer inventory and contract. | Consequential consumers are enumerated; required status/metadata ordering has production refinement tests. | -| LIFE-GAP-012 | Medium | High | No persisted attempt/generation distinguishes delayed pre-interruption completion from valid post-resume completion of the same child. | Documented model exclusion. | Persisted generation token. | Reducer/store/API/model reject stale generation while accepting resumed generation; restart E2E covers it. | -| LIFE-GAP-013 | Medium | High | Task lifecycle status vocabulary is copied across schema, task metadata, Task, CLI, and history reader. | Literal union inventory. | Shared exported schema-derived type. | Consumers import one owner; CI/static check rejects incompatible local copies. | -| LIFE-GAP-014 | Medium | High | Fan-out checker is planned-only but runs in the green umbrella, inviting overclaim. | No production imports or fan-out E2E. | Product decision. | Either enforce serial capacity as invariant, or implement fan-out adapters and E2E before reclassifying. | -| LIFE-GAP-015 | Medium | High | Independent checks do not establish end-to-end refinement. | Seven disjoint state spaces. | Boundary mappings and tractable joint bounds. | Add joint checker/trace validation for each cross-model claim, or keep every claim explicitly local. | -| LIFE-GAP-016 | Medium | High | Traceability is documentary and can drift from scripts, symbols, tests, and CI. | Shared-store scenario count has drifted in documentation. | Machine-readable manifest/checker summaries. | CI validates stable IDs, model membership, bounds, symbol/test paths, and workflow invocation. | -| LIFE-GAP-017 | Medium | High | Store cache records are exposed without cloning; external mutation may bypass locking. | `get`/`getAll` return cached objects. | Immutability/read API decision. | Freeze/clone records or prove callers cannot mutate; mutation regression test. | -| LIFE-GAP-018 | Medium | High | Ordinary history-file reads cast JSON instead of applying the shared schema. | Store reconciliation/read path. | Validation/quarantine policy. | Malformed records are rejected or quarantined deterministically with tests and recovery documentation. | -| LIFE-GAP-019 | Medium | High | Generic task IDs lack the importer’s explicit path-safety validation. | Importer validates IDs; generic paths interpolate IDs. | Shared safe-ID boundary. | Every filesystem task ID passes one validator; traversal and separator tests cover all entry points. | -| LIFE-GAP-020 | Medium | Medium-high | Watch/reconcile convergence is eventual and failure-tolerant, not coherent. | Debounced watcher plus periodic scan. | Version/notification or documented eventual contract. | Define stale-read window and convergence property; multi-host test covers missed watcher event and concurrent update. | -| LIFE-GAP-021 | Medium | High | Store disposal does not await queued writes. | Synchronous `dispose` stops watcher/timer only. | Async drain/close contract. | Disposal awaits or explicitly cancels writes; no post-dispose writes in deterministic test. | -| LIFE-GAP-022 | Medium | High | Public clear and webview clear use different delegated-child semantics. | API uses eviction; webview removes directly. | One clear contract. | All ingress paths converge on the same lifecycle transition and tests assert identical persisted results. | -| LIFE-GAP-023 | Medium | High | History deletion can be resurrected after swallowed unlink failure. | Cache removal precedes best-effort unlink. | Tombstone or surfaced failure/retry. | Inject unlink failure and prove item stays deleted or operation reports failure without false success. | -| LIFE-GAP-024 | Medium | High | Parser cleanup depends on normal finalization or garbage collection. | Weak scope maps lack universal request `finally`. | Request-level cleanup owner. | Abort/error/success all clear active parser state in production integration tests. | -| LIFE-GAP-025 | Medium | High | Abort listeners may accumulate during successful stream chunks. | Per-chunk listener removed only by abort. | Settle-time listener cleanup. | Long stream keeps bounded listener count and removes listeners on both race outcomes. | -| LIFE-GAP-026 | Medium | Medium-high | Usage-drain timeout cannot interrupt a permanently pending `iterator.next()`. | Elapsed time checked before await. | Deadline race/abort. | Hung iterator settles drain within wall-clock bound in fake-timer test. | -| LIFE-GAP-027 | Medium | High | Task-level delegation listeners are untyped/dead while provider listeners own the same public events. | `src/extension/api.ts` registers both paths. | Single typed event owner. | Remove duplicate/dead listeners or define one source; public API test proves exactly-once emission. | -| LIFE-GAP-028 | Low-medium | High | `TaskSpawned` payload semantics differ across task/provider/public surfaces. | Child-only, ambiguous task ID, and parent+child forms. | Event contract normalization. | Payloads use explicit names and adapters are type-checked with compatibility tests. | -| LIFE-GAP-029 | Low-medium | High | `Task.taskStatus` and `TaskRegistry.getRunning` are projections, not scheduler/persistence truth. | Ask markers and abort flags only. | Naming/contract clarification. | Rename or document exact predicates; callers stop using them as stronger lifecycle evidence. | -| LIFE-GAP-030 | Low-medium | Medium | `Task.run()` may resolve immediately if another path already started the task. | `_started` short-circuit versus scheduler callback. | Single start owner. | Scheduler-facing start returns the actual run promise; duplicate-start test proves settlement identity. | -| LIFE-GAP-031 | Low-medium | High | Queue state is memory-only and cleared on disposal. | `MessageQueueService.dispose`. | Product durability decision. | Document intentional loss or persist claims/messages with restart tests. | -| LIFE-GAP-032 | Low-medium | Medium | Webview abandonment handler exists without a confirmed production UI sender. | Protocol/handler search only. | Reachability decision. | Add supported sender/E2E or remove/deprecate unreachable command. | -| LIFE-GAP-033 | Low-medium | High | Resume ingress differs in awaiting and error propagation. | Webview/API/IPC adapters diverge. | Shared resume operation contract. | Contract tests compare result/error/publication semantics for each surface. | -| LIFE-GAP-034 | Low | High | Bounds and model metadata are handwritten and not mechanically synchronized. | Constants, prose, and console summaries duplicate values. | Machine-readable checker metadata. | CI compares emitted metadata with docs/manifest and rejects undocumented bound/action/landmark changes. | +| ID | Severity | Confidence | Gap and production impact | Witness/reproducer | Dependencies | Objective closure criteria | +| ------------ | ---------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| LIFE-GAP-001 | Critical | High | Stale cross-host completion can clear a newer handoff and orphan its child. | Exact shortest witness in shared-store checker. | Disk-authoritative ownership guard or generation. | Deterministic two-store test passes; authoritative child ID is revalidated under disk lock; witness becomes universal invariant. | +| LIFE-GAP-002 | High | High | Stale message save can restore abandoned lineage. | Exact shortest stale-save/abandon witness. | Lifecycle-field ownership, tombstone, or generation. | Stale save cannot alter detached lineage in production test or bounded model; witness promoted. | +| LIFE-GAP-003 | High | High | Legacy dequeue-before-submit can lose queued user feedback on submission failure. | `Task.processQueuedMessages` removes before async submit. | Standardize claim/persist/ack path. | Every queue consumer claims first, removes after durable acceptance, releases on failure; failure test retains message. | +| LIFE-GAP-004 | High | High | Pair writes are not cross-host/crash atomic; partial lifecycle state is observable. | Modeled second-write failure landmark. | WAL/repair intent or explicitly idempotent recovery per pair operation. | Crash injection at each write boundary converges to a documented legal state. | +| LIFE-GAP-005 | High | High | Delegation creates child state and commits parent ownership in separate failure domains. | Provider rollback tests cover selected failures. | Idempotent operation ID or repair intent. | Failure injection before/after every persistence/publication step restores parent or completes delegation without orphan state. | +| LIFE-GAP-006 | High | Medium-high | Parent completion messages can persist before lifecycle completion commit fails. | Source ordering in `reopenParentFromDelegation`. | Transaction/intent or reorder with recovery. | Inject pair-write failure and prove no stale result context, or prove replay safely completes the lifecycle. | +| LIFE-GAP-007 | High | Medium-high | Task-local mode reader refinement is incomplete; wrong shared mode can affect prompts or permissions. | Divergent mode witness; known consumer regression. | Reader inventory and task-local API boundary. | Every mode-sensitive reader classified; required readers use task-local mode; divergent production tests cover both permission directions. | +| LIFE-GAP-008 | High | High | Responses API argument-only deltas may be dropped; absent indices can alias calls. | Transform requires ID/name and defaults index to zero. | Provider transform correlation design. | Multi-delta and concurrent index-less production tests reconstruct isolated calls; parser model includes mapped transform events. | +| LIFE-GAP-009 | Medium | High | Async lifecycle event listeners have no awaited settlement contract. | Async listeners registered on Node EventEmitter. | Classify notification versus barrier events. | Barrier side effects move to awaited methods; notification listeners have contained rejection tests and documented ordering. | +| LIFE-GAP-010 | Medium | Medium-high | Detached usage drain can write after abort, replacement, delegation, or a newer request generation. | Background iterator mutates/persists without generation guard. | Request-generation ownership. | Late drain may account valid usage but cannot mutate stale UI/message/lifecycle state; controlled delayed-stream test passes. | +| LIFE-GAP-011 | Medium | High | Completion model excludes terminal status persistence and downstream public consumers. | Model ends at emission readiness. | Consumer inventory and contract. | Consequential consumers are enumerated; required status/metadata ordering has production refinement tests. | +| LIFE-GAP-012 | Medium | High | No persisted attempt/generation distinguishes delayed pre-interruption completion from valid post-resume completion of the same child. | Documented model exclusion. | Persisted generation token. | Reducer/store/API/model reject stale generation while accepting resumed generation; restart E2E covers it. | +| LIFE-GAP-013 | Medium | High | Task lifecycle status vocabulary is copied across schema, task metadata, Task, CLI, and history reader. | Literal union inventory. | Shared exported schema-derived type. | Consumers import one owner; CI/static check rejects incompatible local copies. | +| LIFE-GAP-014 | Medium | High | Fan-out checker is planned-only but runs in the green umbrella, inviting overclaim. | No production imports or fan-out E2E. | Product decision. | Either enforce serial capacity as invariant, or implement fan-out adapters and E2E before reclassifying. | +| LIFE-GAP-015 | Medium | High | Independent checks do not establish end-to-end refinement. | Seven disjoint state spaces. | Boundary mappings and tractable joint bounds. | Add joint checker/trace validation for each cross-model claim, or keep every claim explicitly local. | +| LIFE-GAP-016 | Medium | High | Traceability is documentary and can drift from scripts, symbols, tests, and CI. | Shared-store scenario count has drifted in documentation. | Machine-readable manifest/checker summaries. | CI validates stable IDs, model membership, bounds, symbol/test paths, and workflow invocation. | +| LIFE-GAP-017 | Medium | High | Store cache records are exposed without cloning; external mutation may bypass locking. | `get`/`getAll` return cached objects. | Immutability/read API decision. | Freeze/clone records or prove callers cannot mutate; mutation regression test. | +| LIFE-GAP-018 | Medium | High | Ordinary history-file reads cast JSON instead of applying the shared schema. | Store reconciliation/read path. | Validation/quarantine policy. | Malformed records are rejected or quarantined deterministically with tests and recovery documentation. | +| LIFE-GAP-019 | Medium | High | Generic task IDs lack the importer’s explicit path-safety validation. | Importer validates IDs; generic paths interpolate IDs. | Shared safe-ID boundary. | Every filesystem task ID passes one validator; traversal and separator tests cover all entry points. | +| LIFE-GAP-020 | Medium | Medium-high | Watch/reconcile convergence is eventual and failure-tolerant, not coherent. | Debounced watcher plus periodic scan. | Version/notification or documented eventual contract. | Define stale-read window and convergence property; multi-host test covers missed watcher event and concurrent update. | +| LIFE-GAP-021 | Medium | High | Store disposal does not await queued writes. | Synchronous `dispose` stops watcher/timer only. | Async drain/close contract. | Disposal awaits or explicitly cancels writes; no post-dispose writes in deterministic test. | +| LIFE-GAP-022 | Medium | High | Public clear and webview clear use different delegated-child semantics. | API uses eviction; webview removes directly. | One clear contract. | All ingress paths converge on the same lifecycle transition and tests assert identical persisted results. | +| LIFE-GAP-023 | Medium | High | History deletion can be resurrected after swallowed unlink failure. | Cache removal precedes best-effort unlink. | Tombstone or surfaced failure/retry. | Inject unlink failure and prove item stays deleted or operation reports failure without false success. | +| LIFE-GAP-024 | Medium | High | Parser cleanup depends on normal finalization or garbage collection. | Weak scope maps lack universal request `finally`. | Request-level cleanup owner. | Abort/error/success all clear active parser state in production integration tests. | +| LIFE-GAP-025 | Medium | High | Abort listeners may accumulate during successful stream chunks. | Per-chunk listener removed only by abort. | Settle-time listener cleanup. | Long stream keeps bounded listener count and removes listeners on both race outcomes. | +| LIFE-GAP-026 | Medium | Medium-high | Usage-drain timeout cannot interrupt a permanently pending `iterator.next()`. | Elapsed time checked before await. | Deadline race/abort. | Hung iterator settles drain within wall-clock bound in fake-timer test. | +| LIFE-GAP-027 | Medium | High | Task-level delegation listeners are untyped/dead while provider listeners own the same public events. | `src/extension/api.ts` registers both paths. | Single typed event owner. | Remove duplicate/dead listeners or define one source; public API test proves exactly-once emission. | +| LIFE-GAP-028 | Low-medium | High | `TaskSpawned` payload semantics differ across task/provider/public surfaces. | Child-only, ambiguous task ID, and parent+child forms. | Event contract normalization. | Payloads use explicit names and adapters are type-checked with compatibility tests. | +| LIFE-GAP-029 | Low-medium | High | `Task.taskStatus` and `TaskRegistry.getRunning` are projections, not scheduler/persistence truth. | Ask markers and abort flags only. | Naming/contract clarification. | Rename or document exact predicates; callers stop using them as stronger lifecycle evidence. | +| LIFE-GAP-030 | Low-medium | Medium | `Task.run()` may resolve immediately if another path already started the task. | `_started` short-circuit versus scheduler callback. | Single start owner. | Scheduler-facing start returns the actual run promise; duplicate-start test proves settlement identity. | +| LIFE-GAP-031 | Low-medium | High | Queue state is memory-only and cleared on disposal. | `MessageQueueService.dispose`. | Product durability decision. | Document intentional loss or persist claims/messages with restart tests. | +| LIFE-GAP-032 | Low-medium | Medium | Webview abandonment handler exists without a confirmed production UI sender. | Protocol/handler search only. | Reachability decision. | Add supported sender/E2E or remove/deprecate unreachable command. | +| LIFE-GAP-033 | Low-medium | High | Resume ingress differs in awaiting and error propagation. | Webview/API/IPC adapters diverge. | Shared resume operation contract. | Contract tests compare result/error/publication semantics for each surface. | +| LIFE-GAP-034 | Low | High | Bounds and model metadata are handwritten and not mechanically synchronized. | Constants, prose, and console summaries duplicate values. | Machine-readable checker metadata. | CI compares emitted metadata with docs/manifest and rejects undocumented bound/action/landmark changes. | +| LIFE-GAP-035 | Medium | High | Initial task-local todo state is not durably rehydrated; a child's model-supplied plan can disappear after switching tasks or restarting before its first todo update. | Create a child with explicit initial todos, switch or restart before `update_todo_list`, then reopen it; restoration finds no todo message and yields an empty list. | Canonical durable task-ID-scoped todo owner and publication contract. | Persist initial todos before visibility/run; restore a deep-equal independent list across switching, interrupted resume, checkpoint restore, and fresh-host restart; preserve later-update and explicit-empty precedence; keep completion gating identical before/after rehydration; add constructor deep-copy, persistence, webview scoping, and E2E witnesses. | ## Burn-down dependencies -| Dependency | Enables | -| ---------------------------------------------- | ----------------------------------- | -| Disk-authoritative ownership/generation design | LIFE-GAP-001, 002, 012 | -| Durable operation intent/recovery design | LIFE-GAP-004, 005, 006, 023 | -| Task-local execution-context owner | LIFE-GAP-007, fan-out side of 014 | -| Request generation and terminal cleanup owner | LIFE-GAP-010, 024, 025, 026 | -| Event notification/barrier contract | LIFE-GAP-009, 011, 027, 028 | -| Machine-readable lifecycle manifest | LIFE-GAP-013, 016, 034 | -| Product decision on serial versus fan-out | LIFE-GAP-014 and future fan-out E2E | +| Dependency | Enables | +| ---------------------------------------------- | ------------------------------------------------------ | +| Disk-authoritative ownership/generation design | LIFE-GAP-001, 002, 012 | +| Durable operation intent/recovery design | LIFE-GAP-004, 005, 006, 023 | +| Task-local execution-context owner | LIFE-GAP-007, fan-out side of 014 | +| Request generation and terminal cleanup owner | LIFE-GAP-010, 024, 025, 026 | +| Event notification/barrier contract | LIFE-GAP-009, 011, 027, 028 | +| Machine-readable lifecycle manifest | LIFE-GAP-013, 016, 034 | +| Product decision on serial versus fan-out | LIFE-GAP-014 and future fan-out E2E | +| Durable task-scoped todo ownership | LIFE-GAP-035 and any future todo/lifecycle composition | ## Mechanically useful follow-up checklist @@ -194,10 +210,11 @@ Severity reflects plausible data loss, ownership corruption, permission/context - [ ] Update this report’s inventory, matrix, severity, dependencies, and closure evidence in the same PR. - [ ] Run `pnpm lifecycle:model-check`, focused production tests, `pnpm test`, typecheck, lint, and required E2E before marking a gap closed. - [ ] Move issue links only within historical provenance; stable burn-down IDs remain the active identity. +- [ ] For LIFE-GAP-035, test both controls: omitted child todos must not copy the parent, while explicit initial child todos must survive task switching and restart. ## Completeness statement -At the audited commit, this report covers every tracked definition and directly discoverable caller matching the lifecycle domains in scope, all seven umbrella checkers, their documented bounds/properties, primary focused suites, lifecycle E2E files, root package scripts, and CI workflow invocations. It does not claim semantic completeness for dynamic calls, generated code, dependencies, ignored files, deployment settings, or production histories. A future code change can invalidate completeness; `LIFE-GAP-016` and `LIFE-GAP-034` exist specifically to make this inventory mechanically maintainable. +At the audited commit, this report covers every tracked definition and directly discoverable caller matching the lifecycle domains in scope, including task-local todo creation and rehydration, all seven umbrella checkers, their documented bounds/properties, primary focused suites, lifecycle E2E files, root package scripts, and CI workflow invocations. It does not claim semantic completeness for dynamic calls, generated code, dependencies, ignored files, deployment settings, or production histories. A future code change can invalidate completeness; `LIFE-GAP-016` and `LIFE-GAP-034` exist specifically to make this inventory mechanically maintainable. ## Historical provenance diff --git a/docs/architecture/task-lifecycle-model.md b/docs/architecture/task-lifecycle-model.md index a793896710..49c2e47a54 100644 --- a/docs/architecture/task-lifecycle-model.md +++ b/docs/architecture/task-lifecycle-model.md @@ -168,15 +168,16 @@ The exhaustive repository inventory, ranked stable burn-down register, source-ba - **Planned-only:** specifies behavior not enabled in production. - **Type/static convention:** centralized typing or guidance without repository-wide enforcement. -| Gap | Invariant | Current evidence | Production/model boundary and runtime impact | Missing work and objective closure criteria | -| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Cross-host completion ownership | A completion may mutate a parent only while that parent authoritatively awaits the same child. | **Known-unsafe witness** plus a production-backed reducer guard. | The reducer rejects stale authoritative input, but disk revalidation checks status legality rather than exact-child ownership. A stale host can orphan the newer child. | Revalidate exact ownership under the disk lock; add deterministic two-store regression coverage; replace the expected witness with a universal bounded invariant. | -| Monotonic detachment | Message persistence must never restore lineage after abandonment clears it. | **Known-unsafe witness** plus a production-backed detach reducer. | A live task rebuilds lineage from stale fields; a later delta can restore parent/root IDs while preserving the interrupted status. | Give lifecycle fields a disk-authoritative write owner or tombstone/generation; prove stale metadata saves cannot alter them; promote the witness to an invariant. | -| Task-local mode consumers | Every mode-sensitive child operation must use the child task's immutable mode, not shared provider/view state. | Write side: **production-backed bounded matrix**. Read side: **proxy/partial and known unsafe**. | The selector snapshot can be correct while environment rendering and tool validation consume shared mode, causing wrong prompts or permissions. | Fix confirmed consumers, inventory other mode readers, add divergent-mode production tests, and enforce a task-local reader boundary before claiming universal isolation. | -| Serial versus fan-out delegation | The shipped serial path permits one awaited/running child and resumes its parent only after child release; fan-out must not be implied by an abstract model. | Serial ownership is **production-backed bounded**; the added fan-out checker is **planned-only abstract bounded**. | Production suspends the parent and uses a one-permit scheduler by default. The fan-out model imports no live-parent production transition, so it cannot prove fan-out behavior. | Either fix scheduler capacity at one and make concurrent fan-out explicitly unreachable, or implement live-parent isolation, rollback, routing, orphan cleanup, and E2E before promoting fan-out coverage. | -| Shared lifecycle status vocabulary | All consumers should derive task status from one exported owner. | **Type/static convention**; the visible interrupted-status symptom is repaired. | Persistence derives its alias from `HistoryItem`, but CLI history adapters still copy the union, allowing future drift. | Export one shared status type, consume it at CLI/persistence boundaries, and rely on typechecking or a static rule to reject copied incompatible vocabulary. | -| Cross-model refinement | No independent checker result may be combined into a stronger end-to-end claim without an executable boundary mapping. | **Explicit limitation** only. | Persistence, provider publication, parser scope, cleanup, and completion durability run as separate state spaces; omitted consumers can violate a premise after another checker passes. | Add production-grounded boundary events and a bounded joint strategy for each genuinely cross-model invariant, or keep claims explicitly local. | -| Completion event consumers | Public completion must not be treated as universally durable beyond the modeled readiness contract. | **Abstract bounded** model plus focused tests and one fresh-host E2E path. | The model abstracts durability and parent reopen; provider status metadata and downstream `TaskCompleted` consumers are outside its state. | Inventory downstream consumers and add refinement checks only for guarantees they require; retain power-loss, filesystem, retry-count, and liveness exclusions. | +| Gap | Invariant | Current evidence | Production/model boundary and runtime impact | Missing work and objective closure criteria | +| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Cross-host completion ownership | A completion may mutate a parent only while that parent authoritatively awaits the same child. | **Known-unsafe witness** plus a production-backed reducer guard. | The reducer rejects stale authoritative input, but disk revalidation checks status legality rather than exact-child ownership. A stale host can orphan the newer child. | Revalidate exact ownership under the disk lock; add deterministic two-store regression coverage; replace the expected witness with a universal bounded invariant. | +| Monotonic detachment | Message persistence must never restore lineage after abandonment clears it. | **Known-unsafe witness** plus a production-backed detach reducer. | A live task rebuilds lineage from stale fields; a later delta can restore parent/root IDs while preserving the interrupted status. | Give lifecycle fields a disk-authoritative write owner or tombstone/generation; prove stale metadata saves cannot alter them; promote the witness to an invariant. | +| Task-local mode consumers | Every mode-sensitive child operation must use the child task's immutable mode, not shared provider/view state. | Write side: **production-backed bounded matrix**. Read side: **proxy/partial and known unsafe**. | The selector snapshot can be correct while environment rendering and tool validation consume shared mode, causing wrong prompts or permissions. | Fix confirmed consumers, inventory other mode readers, add divergent-mode production tests, and enforce a task-local reader boundary before claiming universal isolation. | +| Task-local todo rehydration | Initial and updated todos must remain task-ID-scoped, independent from parent state, and equivalent before and after rehydration. | Creation forwarding has focused tests; persistence/rehydration is **unmodeled**. | Normal creation uses explicit model-supplied child todos rather than copying the parent, but initial todos are process-local and can disappear before the first persisted todo update. | Define a durable todo owner; prove omitted child todos do not copy the parent; preserve explicit child todos across switching/restart with deep-copy, precedence, UI scoping, and E2E checks (`LIFE-GAP-035`). | +| Serial versus fan-out delegation | The shipped serial path permits one awaited/running child and resumes its parent only after child release; fan-out must not be implied by an abstract model. | Serial ownership is **production-backed bounded**; the added fan-out checker is **planned-only abstract bounded**. | Production suspends the parent and uses a one-permit scheduler by default. The fan-out model imports no live-parent production transition, so it cannot prove fan-out behavior. | Either fix scheduler capacity at one and make concurrent fan-out explicitly unreachable, or implement live-parent isolation, rollback, routing, orphan cleanup, and E2E before promoting fan-out coverage. | +| Shared lifecycle status vocabulary | All consumers should derive task status from one exported owner. | **Type/static convention**; the visible interrupted-status symptom is repaired. | Persistence derives its alias from `HistoryItem`, but CLI history adapters still copy the union, allowing future drift. | Export one shared status type, consume it at CLI/persistence boundaries, and rely on typechecking or a static rule to reject copied incompatible vocabulary. | +| Cross-model refinement | No independent checker result may be combined into a stronger end-to-end claim without an executable boundary mapping. | **Explicit limitation** only. | Persistence, provider publication, parser scope, cleanup, and completion durability run as separate state spaces; omitted consumers can violate a premise after another checker passes. | Add production-grounded boundary events and a bounded joint strategy for each genuinely cross-model invariant, or keep claims explicitly local. | +| Completion event consumers | Public completion must not be treated as universally durable beyond the modeled readiness contract. | **Abstract bounded** model plus focused tests and one fresh-host E2E path. | The model abstracts durability and parent reopen; provider status metadata and downstream `TaskCompleted` consumers are outside its state. | Inventory downstream consumers and add refinement checks only for guarantees they require; retain power-loss, filesystem, retry-count, and liveness exclusions. | ## Historical provenance From 63150c68ece8bee2b822f4fdaeb71a52a7d478ea Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 13 Sep 2026 14:31:01 +0000 Subject: [PATCH 12/17] docs(lifecycle): generalize tool state ownership gaps --- .../architecture/task-lifecycle-gap-report.md | 157 +++++++++++------- docs/architecture/task-lifecycle-model.md | 23 +-- 2 files changed, 107 insertions(+), 73 deletions(-) diff --git a/docs/architecture/task-lifecycle-gap-report.md b/docs/architecture/task-lifecycle-gap-report.md index a021e071d9..1a2ff5f527 100644 --- a/docs/architecture/task-lifecycle-gap-report.md +++ b/docs/architecture/task-lifecycle-gap-report.md @@ -37,19 +37,19 @@ Evidence classes used below: ### State owners -| Owner | State | Primary symbols | Authority boundary | -| ------------------------ | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | -| Persisted history schema | Status, lineage, completion summary, pending action, accounting | `packages/types/src/history.ts`: `historyItemSchema`, `pendingTaskActionSchema` | Restart-visible record shape; optional status normalizes to active in lifecycle code. | -| Lifecycle reducers | Legal persisted transitions and parent-child ownership | `src/core/task-persistence/taskLifecycle.ts`: `delegateTaskToChild`, `interruptDelegatedChild`, `completeDelegatedChild`, `abandonDelegatedChild` | Pure transition authority when inputs are authoritative. | -| History store | Per-task files, cache, deltas, reconciliation, migration, repair | `src/core/task-persistence/TaskHistoryStore.ts`; `taskStoreConcurrency.ts` | Per-task files are authoritative; each extension host has an independent cache. | -| Live task | Abort/dispose, ask state, run ownership, mode/profile, streaming, message and completion readiness | `src/core/task/Task.ts` | Process-local execution state; not equivalent to persisted status. | -| Task registry | Live instances, compatibility stack, current focus | `src/core/task/TaskRegistry.ts` | Focus/publication owner; not scheduler admission or persisted lineage. | -| Provider | Transition queues, current task, registry integration, persistence orchestration, event forwarding | `src/core/webview/ClineProvider.ts` | Coordinates layers but does not make them one transaction. | -| Scheduler/semaphore | Waiting, admission, held permits, cancellation, release | `src/core/task/TaskScheduler.ts`; `src/utils/TaskSemaphore.ts` | Provider-local execution gate; default capacity is one. | -| Message queue | Queued user feedback and claims | `src/core/message-queue/MessageQueueService.ts` | Memory-only; disposal clears membership and claims. | -| Parser scope | Raw-index and tool-ID accumulators | `src/core/assistant-message/NativeToolCallParser.ts` | Request-scope parser state; transport and Task caller protocol are separate. | -| Event surfaces | Task, provider, public API, IPC, and webview lifecycle notifications | `packages/types/src/task.ts`, `events.ts`, `ipc.ts`; `src/extension/api.ts` | Related but non-identical payload and settlement contracts. | -| Task todo state | Initial and updated task-local checklist | `Task.todoList`, `NewTaskTool`, `UpdateTodoListTool`, `currentTaskTodos` | Initial todos are live-task state; later updates are recoverable from task messages. | +| Owner | State | Primary symbols | Authority boundary | +| -------------------------- | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | +| Persisted history schema | Status, lineage, completion summary, pending action, accounting | `packages/types/src/history.ts`: `historyItemSchema`, `pendingTaskActionSchema` | Restart-visible record shape; optional status normalizes to active in lifecycle code. | +| Lifecycle reducers | Legal persisted transitions and parent-child ownership | `src/core/task-persistence/taskLifecycle.ts`: `delegateTaskToChild`, `interruptDelegatedChild`, `completeDelegatedChild`, `abandonDelegatedChild` | Pure transition authority when inputs are authoritative. | +| History store | Per-task files, cache, deltas, reconciliation, migration, repair | `src/core/task-persistence/TaskHistoryStore.ts`; `taskStoreConcurrency.ts` | Per-task files are authoritative; each extension host has an independent cache. | +| Live task | Abort/dispose, ask state, run ownership, mode/profile, streaming, message and completion readiness | `src/core/task/Task.ts` | Process-local execution state; not equivalent to persisted status. | +| Task registry | Live instances, compatibility stack, current focus | `src/core/task/TaskRegistry.ts` | Focus/publication owner; not scheduler admission or persisted lineage. | +| Provider | Transition queues, current task, registry integration, persistence orchestration, event forwarding | `src/core/webview/ClineProvider.ts` | Coordinates layers but does not make them one transaction. | +| Scheduler/semaphore | Waiting, admission, held permits, cancellation, release | `src/core/task/TaskScheduler.ts`; `src/utils/TaskSemaphore.ts` | Provider-local execution gate; default capacity is one. | +| Message queue | Queued user feedback and claims | `src/core/message-queue/MessageQueueService.ts` | Memory-only; disposal clears membership and claims. | +| Parser scope | Raw-index and tool-ID accumulators | `src/core/assistant-message/NativeToolCallParser.ts` | Request-scope parser state; transport and Task caller protocol are separate. | +| Event surfaces | Task, provider, public API, IPC, and webview lifecycle notifications | `packages/types/src/task.ts`, `events.ts`, `ipc.ts`; `src/extension/api.ts` | Related but non-identical payload and settlement contracts. | +| Tool-originated task state | Child initialization, approvals, partial calls, results, pending actions, and replay identity | `BaseTool`, `NewTaskTool`, `UpdateTodoListTool`, `AttemptCompletionTool`, `presentAssistantMessage`, `Task.todoList` | Ownership spans request, call, task, provider, message, and persisted-history scopes. | ### Persisted lifecycle vocabulary and mutations @@ -80,7 +80,21 @@ Copied persisted-status membership occurs in `packages/types/src/task.ts`, `src/ Registry publication can precede scheduler admission. Therefore “current”, “running”, “active”, and “persisted active” are not interchangeable states. -### Task and subtask todo ownership +### Tool-state ownership boundary + +Tool inputs originate in provider stream transforms, are assembled by `NativeToolCallParser`, converted to authoritative `nativeArgs`, validated centrally and inside handlers, optionally edited through webview approval, and can mutate live `Task`, provider, message, and persisted history state. Tool outputs return through `pushToolResult`, parent result injection, pending-action replay, or public lifecycle events. Those stages do not share one canonical identity or transaction. + +| Boundary | Production path | Current evidence | Ownership limitation | +| --------------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | +| Argument assembly | Provider transform → parser scope → `ToolUse.nativeArgs` → `BaseTool.handle` | Production-backed bounded parser replay plus provider/tool tests | Provider transforms, parser state, and downstream handler state are separate owners. | +| Validation | `validateToolUse` plus handler-local parsing and policy checks | Focused tests | Validation mode and configuration may come from shared provider state rather than task context. | +| Approval edits | Handler proposal → `askApproval` → webview edit → handler settlement | Focused single-approval tests | Todo edit state is process-global and carries no task/action/call identity. | +| Child initialization | `new_task` args → pending create action → provider delegation → child `Task` | Focused forwarding and pending-action tests | Some tool-originated child state is live-only and lacks a durable rehydration owner. | +| Completion/result injection | `attempt_completion` pending action → parent UI/API messages → lifecycle pair | Focused tests and separate lifecycle/completion models | Message, lifecycle, and replay commits are separate failure domains. | +| Partial presentation | Singleton tool handler partial methods | Tool-local tests | `BaseTool.lastSeenPartialPath` is shared across calls/tasks. | +| Identity/replay | raw call ID → sanitized ID → history/result/pending action | Duplicate-ID helper tests | Sanitization is non-injective; history deduplication and execution do not share a proven bijection. | + +#### `new_task` and todo evidence The normal creation path does not inherit the parent list. The model supplies optional `new_task.todos`; `NewTaskTool.execute` parses only that argument into a fresh array, stores it in a pending action while approval is unresolved, and forwards it as `initialTodos` through `delegateParentAndOpenChild` and `createTask`. The parent task supplies lineage and workspace context, not todos. @@ -93,6 +107,14 @@ The reported appearance of inheritance therefore needs two controls: Initial child todos are not placed in a task message or `HistoryItem`. Rehydration constructs a new `Task` without `initialTodos`; before the child's first `update_todo_list`, message-derived restoration yields an empty list. Constructor and todo setter APIs also assign arrays directly, creating latent aliasing for programmatic callers even though the normal `new_task` parser creates fresh objects. No current model contains todo state, task-ID/generation-scoped todo publication, or rehydration equivalence. +#### Provider-mode causality + +The provider-mode regression addressed by historical PR #1625 can change mode-sensitive prompt context, validation, and tool availability. It could therefore influence what arguments the model chooses to emit, but no production path uses provider mode to select or transfer the parent's `todoList`. It is not a direct mechanism for parent todos appearing in a child. Matching lists at creation are evidence of explicit model-supplied `new_task.todos` unless a separate ownership witness shows otherwise. A distinct plausible contamination path is the process-global todo approval edit slot described by `LIFE-GAP-036`. + +#### Formal-model decision + +No broad tool-state checker is added in this PR. A green model would have to invent a unified owner across parser, handler singleton, webview approval, task state, message files, lifecycle records, and replay. Initial child-state persistence has no production transition to import; approval correlation lacks task/action identity; canonical call identity spans multiple embedded I/O paths. Until those owners are extracted, stable gaps and deterministic witness criteria are stronger evidence than an abstract passing model. The existing parser, lifecycle, handoff, cleanup, completion, and fan-out checkers remain explicitly local. + ### Delegation, interruption, cancellation, completion, and abandonment | Flow | Production path | Key ordering | Verification | @@ -144,56 +166,62 @@ CI runs lint, typecheck, and `pnpm lifecycle:model-check` in `.github/workflows/ Severity reflects plausible data loss, ownership corruption, permission/context errors, or stuck work. Confidence reflects direct source evidence, deterministic witness, or inference. -| ID | Severity | Confidence | Gap and production impact | Witness/reproducer | Dependencies | Objective closure criteria | -| ------------ | ---------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| LIFE-GAP-001 | Critical | High | Stale cross-host completion can clear a newer handoff and orphan its child. | Exact shortest witness in shared-store checker. | Disk-authoritative ownership guard or generation. | Deterministic two-store test passes; authoritative child ID is revalidated under disk lock; witness becomes universal invariant. | -| LIFE-GAP-002 | High | High | Stale message save can restore abandoned lineage. | Exact shortest stale-save/abandon witness. | Lifecycle-field ownership, tombstone, or generation. | Stale save cannot alter detached lineage in production test or bounded model; witness promoted. | -| LIFE-GAP-003 | High | High | Legacy dequeue-before-submit can lose queued user feedback on submission failure. | `Task.processQueuedMessages` removes before async submit. | Standardize claim/persist/ack path. | Every queue consumer claims first, removes after durable acceptance, releases on failure; failure test retains message. | -| LIFE-GAP-004 | High | High | Pair writes are not cross-host/crash atomic; partial lifecycle state is observable. | Modeled second-write failure landmark. | WAL/repair intent or explicitly idempotent recovery per pair operation. | Crash injection at each write boundary converges to a documented legal state. | -| LIFE-GAP-005 | High | High | Delegation creates child state and commits parent ownership in separate failure domains. | Provider rollback tests cover selected failures. | Idempotent operation ID or repair intent. | Failure injection before/after every persistence/publication step restores parent or completes delegation without orphan state. | -| LIFE-GAP-006 | High | Medium-high | Parent completion messages can persist before lifecycle completion commit fails. | Source ordering in `reopenParentFromDelegation`. | Transaction/intent or reorder with recovery. | Inject pair-write failure and prove no stale result context, or prove replay safely completes the lifecycle. | -| LIFE-GAP-007 | High | Medium-high | Task-local mode reader refinement is incomplete; wrong shared mode can affect prompts or permissions. | Divergent mode witness; known consumer regression. | Reader inventory and task-local API boundary. | Every mode-sensitive reader classified; required readers use task-local mode; divergent production tests cover both permission directions. | -| LIFE-GAP-008 | High | High | Responses API argument-only deltas may be dropped; absent indices can alias calls. | Transform requires ID/name and defaults index to zero. | Provider transform correlation design. | Multi-delta and concurrent index-less production tests reconstruct isolated calls; parser model includes mapped transform events. | -| LIFE-GAP-009 | Medium | High | Async lifecycle event listeners have no awaited settlement contract. | Async listeners registered on Node EventEmitter. | Classify notification versus barrier events. | Barrier side effects move to awaited methods; notification listeners have contained rejection tests and documented ordering. | -| LIFE-GAP-010 | Medium | Medium-high | Detached usage drain can write after abort, replacement, delegation, or a newer request generation. | Background iterator mutates/persists without generation guard. | Request-generation ownership. | Late drain may account valid usage but cannot mutate stale UI/message/lifecycle state; controlled delayed-stream test passes. | -| LIFE-GAP-011 | Medium | High | Completion model excludes terminal status persistence and downstream public consumers. | Model ends at emission readiness. | Consumer inventory and contract. | Consequential consumers are enumerated; required status/metadata ordering has production refinement tests. | -| LIFE-GAP-012 | Medium | High | No persisted attempt/generation distinguishes delayed pre-interruption completion from valid post-resume completion of the same child. | Documented model exclusion. | Persisted generation token. | Reducer/store/API/model reject stale generation while accepting resumed generation; restart E2E covers it. | -| LIFE-GAP-013 | Medium | High | Task lifecycle status vocabulary is copied across schema, task metadata, Task, CLI, and history reader. | Literal union inventory. | Shared exported schema-derived type. | Consumers import one owner; CI/static check rejects incompatible local copies. | -| LIFE-GAP-014 | Medium | High | Fan-out checker is planned-only but runs in the green umbrella, inviting overclaim. | No production imports or fan-out E2E. | Product decision. | Either enforce serial capacity as invariant, or implement fan-out adapters and E2E before reclassifying. | -| LIFE-GAP-015 | Medium | High | Independent checks do not establish end-to-end refinement. | Seven disjoint state spaces. | Boundary mappings and tractable joint bounds. | Add joint checker/trace validation for each cross-model claim, or keep every claim explicitly local. | -| LIFE-GAP-016 | Medium | High | Traceability is documentary and can drift from scripts, symbols, tests, and CI. | Shared-store scenario count has drifted in documentation. | Machine-readable manifest/checker summaries. | CI validates stable IDs, model membership, bounds, symbol/test paths, and workflow invocation. | -| LIFE-GAP-017 | Medium | High | Store cache records are exposed without cloning; external mutation may bypass locking. | `get`/`getAll` return cached objects. | Immutability/read API decision. | Freeze/clone records or prove callers cannot mutate; mutation regression test. | -| LIFE-GAP-018 | Medium | High | Ordinary history-file reads cast JSON instead of applying the shared schema. | Store reconciliation/read path. | Validation/quarantine policy. | Malformed records are rejected or quarantined deterministically with tests and recovery documentation. | -| LIFE-GAP-019 | Medium | High | Generic task IDs lack the importer’s explicit path-safety validation. | Importer validates IDs; generic paths interpolate IDs. | Shared safe-ID boundary. | Every filesystem task ID passes one validator; traversal and separator tests cover all entry points. | -| LIFE-GAP-020 | Medium | Medium-high | Watch/reconcile convergence is eventual and failure-tolerant, not coherent. | Debounced watcher plus periodic scan. | Version/notification or documented eventual contract. | Define stale-read window and convergence property; multi-host test covers missed watcher event and concurrent update. | -| LIFE-GAP-021 | Medium | High | Store disposal does not await queued writes. | Synchronous `dispose` stops watcher/timer only. | Async drain/close contract. | Disposal awaits or explicitly cancels writes; no post-dispose writes in deterministic test. | -| LIFE-GAP-022 | Medium | High | Public clear and webview clear use different delegated-child semantics. | API uses eviction; webview removes directly. | One clear contract. | All ingress paths converge on the same lifecycle transition and tests assert identical persisted results. | -| LIFE-GAP-023 | Medium | High | History deletion can be resurrected after swallowed unlink failure. | Cache removal precedes best-effort unlink. | Tombstone or surfaced failure/retry. | Inject unlink failure and prove item stays deleted or operation reports failure without false success. | -| LIFE-GAP-024 | Medium | High | Parser cleanup depends on normal finalization or garbage collection. | Weak scope maps lack universal request `finally`. | Request-level cleanup owner. | Abort/error/success all clear active parser state in production integration tests. | -| LIFE-GAP-025 | Medium | High | Abort listeners may accumulate during successful stream chunks. | Per-chunk listener removed only by abort. | Settle-time listener cleanup. | Long stream keeps bounded listener count and removes listeners on both race outcomes. | -| LIFE-GAP-026 | Medium | Medium-high | Usage-drain timeout cannot interrupt a permanently pending `iterator.next()`. | Elapsed time checked before await. | Deadline race/abort. | Hung iterator settles drain within wall-clock bound in fake-timer test. | -| LIFE-GAP-027 | Medium | High | Task-level delegation listeners are untyped/dead while provider listeners own the same public events. | `src/extension/api.ts` registers both paths. | Single typed event owner. | Remove duplicate/dead listeners or define one source; public API test proves exactly-once emission. | -| LIFE-GAP-028 | Low-medium | High | `TaskSpawned` payload semantics differ across task/provider/public surfaces. | Child-only, ambiguous task ID, and parent+child forms. | Event contract normalization. | Payloads use explicit names and adapters are type-checked with compatibility tests. | -| LIFE-GAP-029 | Low-medium | High | `Task.taskStatus` and `TaskRegistry.getRunning` are projections, not scheduler/persistence truth. | Ask markers and abort flags only. | Naming/contract clarification. | Rename or document exact predicates; callers stop using them as stronger lifecycle evidence. | -| LIFE-GAP-030 | Low-medium | Medium | `Task.run()` may resolve immediately if another path already started the task. | `_started` short-circuit versus scheduler callback. | Single start owner. | Scheduler-facing start returns the actual run promise; duplicate-start test proves settlement identity. | -| LIFE-GAP-031 | Low-medium | High | Queue state is memory-only and cleared on disposal. | `MessageQueueService.dispose`. | Product durability decision. | Document intentional loss or persist claims/messages with restart tests. | -| LIFE-GAP-032 | Low-medium | Medium | Webview abandonment handler exists without a confirmed production UI sender. | Protocol/handler search only. | Reachability decision. | Add supported sender/E2E or remove/deprecate unreachable command. | -| LIFE-GAP-033 | Low-medium | High | Resume ingress differs in awaiting and error propagation. | Webview/API/IPC adapters diverge. | Shared resume operation contract. | Contract tests compare result/error/publication semantics for each surface. | -| LIFE-GAP-034 | Low | High | Bounds and model metadata are handwritten and not mechanically synchronized. | Constants, prose, and console summaries duplicate values. | Machine-readable checker metadata. | CI compares emitted metadata with docs/manifest and rejects undocumented bound/action/landmark changes. | -| LIFE-GAP-035 | Medium | High | Initial task-local todo state is not durably rehydrated; a child's model-supplied plan can disappear after switching tasks or restarting before its first todo update. | Create a child with explicit initial todos, switch or restart before `update_todo_list`, then reopen it; restoration finds no todo message and yields an empty list. | Canonical durable task-ID-scoped todo owner and publication contract. | Persist initial todos before visibility/run; restore a deep-equal independent list across switching, interrupted resume, checkpoint restore, and fresh-host restart; preserve later-update and explicit-empty precedence; keep completion gating identical before/after rehydration; add constructor deep-copy, persistence, webview scoping, and E2E witnesses. | +| ID | Severity | Confidence | Gap and production impact | Witness/reproducer | Dependencies | Objective closure criteria | +| ------------ | ----------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| LIFE-GAP-001 | Critical | High | Stale cross-host completion can clear a newer handoff and orphan its child. | Exact shortest witness in shared-store checker. | Disk-authoritative ownership guard or generation. | Deterministic two-store test passes; authoritative child ID is revalidated under disk lock; witness becomes universal invariant. | +| LIFE-GAP-002 | High | High | Stale message save can restore abandoned lineage. | Exact shortest stale-save/abandon witness. | Lifecycle-field ownership, tombstone, or generation. | Stale save cannot alter detached lineage in production test or bounded model; witness promoted. | +| LIFE-GAP-003 | High | High | Legacy dequeue-before-submit can lose queued user feedback on submission failure. | `Task.processQueuedMessages` removes before async submit. | Standardize claim/persist/ack path. | Every queue consumer claims first, removes after durable acceptance, releases on failure; failure test retains message. | +| LIFE-GAP-004 | High | High | Pair writes are not cross-host/crash atomic; partial lifecycle state is observable. | Modeled second-write failure landmark. | WAL/repair intent or explicitly idempotent recovery per pair operation. | Crash injection at each write boundary converges to a documented legal state. | +| LIFE-GAP-005 | High | High | Delegation creates child state and commits parent ownership in separate failure domains. | Provider rollback tests cover selected failures. | Idempotent operation ID or repair intent. | Failure injection before/after every persistence/publication step restores parent or completes delegation without orphan state. | +| LIFE-GAP-006 | High | Medium-high | Parent completion messages can persist before lifecycle completion commit fails. | Source ordering in `reopenParentFromDelegation`. | Transaction/intent or reorder with recovery. | Inject pair-write failure and prove no stale result context, or prove replay safely completes the lifecycle. | +| LIFE-GAP-007 | High | Medium-high | Task-local mode reader refinement is incomplete; wrong shared mode can affect prompts or permissions. | Divergent mode witness; known consumer regression. | Reader inventory and task-local API boundary. | Every mode-sensitive reader classified; required readers use task-local mode; divergent production tests cover both permission directions. | +| LIFE-GAP-008 | High | High | Responses API argument-only deltas may be dropped; absent indices can alias calls. | Transform requires ID/name and defaults index to zero. | Provider transform correlation design. | Multi-delta and concurrent index-less production tests reconstruct isolated calls; parser model includes mapped transform events. | +| LIFE-GAP-009 | Medium | High | Async lifecycle event listeners have no awaited settlement contract. | Async listeners registered on Node EventEmitter. | Classify notification versus barrier events. | Barrier side effects move to awaited methods; notification listeners have contained rejection tests and documented ordering. | +| LIFE-GAP-010 | Medium | Medium-high | Detached usage drain can write after abort, replacement, delegation, or a newer request generation. | Background iterator mutates/persists without generation guard. | Request-generation ownership. | Late drain may account valid usage but cannot mutate stale UI/message/lifecycle state; controlled delayed-stream test passes. | +| LIFE-GAP-011 | Medium | High | Completion model excludes terminal status persistence and downstream public consumers. | Model ends at emission readiness. | Consumer inventory and contract. | Consequential consumers are enumerated; required status/metadata ordering has production refinement tests. | +| LIFE-GAP-012 | Medium | High | No persisted attempt/generation distinguishes delayed pre-interruption completion from valid post-resume completion of the same child. | Documented model exclusion. | Persisted generation token. | Reducer/store/API/model reject stale generation while accepting resumed generation; restart E2E covers it. | +| LIFE-GAP-013 | Medium | High | Task lifecycle status vocabulary is copied across schema, task metadata, Task, CLI, and history reader. | Literal union inventory. | Shared exported schema-derived type. | Consumers import one owner; CI/static check rejects incompatible local copies. | +| LIFE-GAP-014 | Medium | High | Fan-out checker is planned-only but runs in the green umbrella, inviting overclaim. | No production imports or fan-out E2E. | Product decision. | Either enforce serial capacity as invariant, or implement fan-out adapters and E2E before reclassifying. | +| LIFE-GAP-015 | Medium | High | Independent checks do not establish end-to-end refinement. | Seven disjoint state spaces. | Boundary mappings and tractable joint bounds. | Add joint checker/trace validation for each cross-model claim, or keep every claim explicitly local. | +| LIFE-GAP-016 | Medium | High | Traceability is documentary and can drift from scripts, symbols, tests, and CI. | Shared-store scenario count has drifted in documentation. | Machine-readable manifest/checker summaries. | CI validates stable IDs, model membership, bounds, symbol/test paths, and workflow invocation. | +| LIFE-GAP-017 | Medium | High | Store cache records are exposed without cloning; external mutation may bypass locking. | `get`/`getAll` return cached objects. | Immutability/read API decision. | Freeze/clone records or prove callers cannot mutate; mutation regression test. | +| LIFE-GAP-018 | Medium | High | Ordinary history-file reads cast JSON instead of applying the shared schema. | Store reconciliation/read path. | Validation/quarantine policy. | Malformed records are rejected or quarantined deterministically with tests and recovery documentation. | +| LIFE-GAP-019 | Medium | High | Generic task IDs lack the importer’s explicit path-safety validation. | Importer validates IDs; generic paths interpolate IDs. | Shared safe-ID boundary. | Every filesystem task ID passes one validator; traversal and separator tests cover all entry points. | +| LIFE-GAP-020 | Medium | Medium-high | Watch/reconcile convergence is eventual and failure-tolerant, not coherent. | Debounced watcher plus periodic scan. | Version/notification or documented eventual contract. | Define stale-read window and convergence property; multi-host test covers missed watcher event and concurrent update. | +| LIFE-GAP-021 | Medium | High | Store disposal does not await queued writes. | Synchronous `dispose` stops watcher/timer only. | Async drain/close contract. | Disposal awaits or explicitly cancels writes; no post-dispose writes in deterministic test. | +| LIFE-GAP-022 | Medium | High | Public clear and webview clear use different delegated-child semantics. | API uses eviction; webview removes directly. | One clear contract. | All ingress paths converge on the same lifecycle transition and tests assert identical persisted results. | +| LIFE-GAP-023 | Medium | High | History deletion can be resurrected after swallowed unlink failure. | Cache removal precedes best-effort unlink. | Tombstone or surfaced failure/retry. | Inject unlink failure and prove item stays deleted or operation reports failure without false success. | +| LIFE-GAP-024 | Medium | High | Parser cleanup depends on normal finalization or garbage collection. | Weak scope maps lack universal request `finally`. | Request-level cleanup owner. | Abort/error/success all clear active parser state in production integration tests. | +| LIFE-GAP-025 | Medium | High | Abort listeners may accumulate during successful stream chunks. | Per-chunk listener removed only by abort. | Settle-time listener cleanup. | Long stream keeps bounded listener count and removes listeners on both race outcomes. | +| LIFE-GAP-026 | Medium | Medium-high | Usage-drain timeout cannot interrupt a permanently pending `iterator.next()`. | Elapsed time checked before await. | Deadline race/abort. | Hung iterator settles drain within wall-clock bound in fake-timer test. | +| LIFE-GAP-027 | Medium | High | Task-level delegation listeners are untyped/dead while provider listeners own the same public events. | `src/extension/api.ts` registers both paths. | Single typed event owner. | Remove duplicate/dead listeners or define one source; public API test proves exactly-once emission. | +| LIFE-GAP-028 | Low-medium | High | `TaskSpawned` payload semantics differ across task/provider/public surfaces. | Child-only, ambiguous task ID, and parent+child forms. | Event contract normalization. | Payloads use explicit names and adapters are type-checked with compatibility tests. | +| LIFE-GAP-029 | Low-medium | High | `Task.taskStatus` and `TaskRegistry.getRunning` are projections, not scheduler/persistence truth. | Ask markers and abort flags only. | Naming/contract clarification. | Rename or document exact predicates; callers stop using them as stronger lifecycle evidence. | +| LIFE-GAP-030 | Low-medium | Medium | `Task.run()` may resolve immediately if another path already started the task. | `_started` short-circuit versus scheduler callback. | Single start owner. | Scheduler-facing start returns the actual run promise; duplicate-start test proves settlement identity. | +| LIFE-GAP-031 | Low-medium | High | Queue state is memory-only and cleared on disposal. | `MessageQueueService.dispose`. | Product durability decision. | Document intentional loss or persist claims/messages with restart tests. | +| LIFE-GAP-032 | Low-medium | Medium | Webview abandonment handler exists without a confirmed production UI sender. | Protocol/handler search only. | Reachability decision. | Add supported sender/E2E or remove/deprecate unreachable command. | +| LIFE-GAP-033 | Low-medium | High | Resume ingress differs in awaiting and error propagation. | Webview/API/IPC adapters diverge. | Shared resume operation contract. | Contract tests compare result/error/publication semantics for each surface. | +| LIFE-GAP-034 | Low | High | Bounds and model metadata are handwritten and not mechanically synchronized. | Constants, prose, and console summaries duplicate values. | Machine-readable checker metadata. | CI compares emitted metadata with docs/manifest and rejects undocumented bound/action/landmark changes. | +| LIFE-GAP-035 | Medium | High | Tool-originated child initialization lacks a complete durable ownership contract; initial todo state is the confirmed witness and can disappear after rehydration. | Create a child with explicit initial todos, switch or restart before `update_todo_list`, then reopen it; restoration finds no todo message and yields an empty list. | Canonical durable task-ID-scoped child-initialization owner and publication contract. | Inventory every `new_task`-originated child field; persist required initial state before visibility/run; restore deep-equal independent state across switching, interrupted resume, checkpoint restore, and fresh-host restart; preserve later-update and explicit-empty precedence; add constructor deep-copy, persistence, webview scoping, and E2E witnesses. | +| LIFE-GAP-036 | High | High | Interactive todo approval edit state is process-global and uncorrelated; one task's delayed edit can be consumed by another task's pending approval. | Start approvals for tasks A and B, send A's edited list through `setPendingTodoList`, then resolve B; B reads the shared `approvedTodoList`. | Task/action/tool-call-correlated approval state and webview protocol. | Carry task ID and action/tool-call ID through proposal, webview edit, approval, cancellation, and settlement; reject stale/mismatched edits; deep-clone inputs; test two interleaved approvals, denial, cancellation, task switch, and delayed edits. | +| LIFE-GAP-037 | Medium-high | High | Singleton tool handlers share partial presentation state across calls/tasks, so interleaved paths can cause false or missed stabilization. | Interleave A:`x`, B:`y`, A:`x` or A:`x`, B:`x` through one handler's `lastSeenPartialPath`. | Per-call handler state keyed by task and tool-call identity. | Isolate partial state by `(taskId, toolCallId)` or handler instance; prove independent stabilization and cleanup after success, malformed finalization, rejection, cancellation, abandonment, and incomplete streams. | +| LIFE-GAP-038 | High | High | Lossy tool-ID canonicalization can deduplicate persisted history without deduplicating execution, results, approvals, or pending-action replay. | Distinct raw IDs such as `call:a` and `call/a` both sanitize to `call_a`; history may retain one call while execution retains both. | One collision-resistant canonical call identity before indexing and persistence. | Reject or disambiguate collisions; prove a bijection among parsed call, durable tool use, approval, execution, result, pending action, and replay; test adversarial native/MCP IDs and restart between approval and settlement. | ## Burn-down dependencies -| Dependency | Enables | -| ---------------------------------------------- | ------------------------------------------------------ | -| Disk-authoritative ownership/generation design | LIFE-GAP-001, 002, 012 | -| Durable operation intent/recovery design | LIFE-GAP-004, 005, 006, 023 | -| Task-local execution-context owner | LIFE-GAP-007, fan-out side of 014 | -| Request generation and terminal cleanup owner | LIFE-GAP-010, 024, 025, 026 | -| Event notification/barrier contract | LIFE-GAP-009, 011, 027, 028 | -| Machine-readable lifecycle manifest | LIFE-GAP-013, 016, 034 | -| Product decision on serial versus fan-out | LIFE-GAP-014 and future fan-out E2E | -| Durable task-scoped todo ownership | LIFE-GAP-035 and any future todo/lifecycle composition | +| Dependency | Enables | +| ---------------------------------------------- | ------------------------------------------------------------- | +| Disk-authoritative ownership/generation design | LIFE-GAP-001, 002, 012 | +| Durable operation intent/recovery design | LIFE-GAP-004, 005, 006, 023 | +| Task-local execution-context owner | LIFE-GAP-007, fan-out side of 014 | +| Request generation and terminal cleanup owner | LIFE-GAP-010, 024, 025, 026 | +| Event notification/barrier contract | LIFE-GAP-009, 011, 027, 028 | +| Machine-readable lifecycle manifest | LIFE-GAP-013, 016, 034 | +| Product decision on serial versus fan-out | LIFE-GAP-014 and future fan-out E2E | +| Durable task-scoped child initialization | LIFE-GAP-035 and future tool/lifecycle composition | +| Correlated approval ownership | LIFE-GAP-036 | +| Task/tool-call-scoped partial state | LIFE-GAP-037 with request-generation cleanup gaps 010 and 024 | +| Canonical tool-call identity | LIFE-GAP-038 with generation/replay gap 012 | ## Mechanically useful follow-up checklist @@ -210,11 +238,14 @@ Severity reflects plausible data loss, ownership corruption, permission/context - [ ] Update this report’s inventory, matrix, severity, dependencies, and closure evidence in the same PR. - [ ] Run `pnpm lifecycle:model-check`, focused production tests, `pnpm test`, typecheck, lint, and required E2E before marking a gap closed. - [ ] Move issue links only within historical provenance; stable burn-down IDs remain the active identity. -- [ ] For LIFE-GAP-035, test both controls: omitted child todos must not copy the parent, while explicit initial child todos must survive task switching and restart. +- [ ] For LIFE-GAP-035, inventory all tool-originated child state and test both todo controls: omitted child todos must not copy the parent, while explicit initial child todos must survive task switching and restart. +- [ ] For LIFE-GAP-036, correlate every approval edit and settlement with task ID plus action/tool-call ID; reject stale cross-task edits. +- [ ] For LIFE-GAP-037, interleave equal and unequal partial paths across two calls and two tasks, then verify terminal cleanup. +- [ ] For LIFE-GAP-038, use adversarial raw IDs to verify one-to-one durable call, approval, execution, result, pending-action, and replay identity. ## Completeness statement -At the audited commit, this report covers every tracked definition and directly discoverable caller matching the lifecycle domains in scope, including task-local todo creation and rehydration, all seven umbrella checkers, their documented bounds/properties, primary focused suites, lifecycle E2E files, root package scripts, and CI workflow invocations. It does not claim semantic completeness for dynamic calls, generated code, dependencies, ignored files, deployment settings, or production histories. A future code change can invalidate completeness; `LIFE-GAP-016` and `LIFE-GAP-034` exist specifically to make this inventory mechanically maintainable. +At the audited commit, this report covers every tracked definition and directly discoverable caller matching the lifecycle domains in scope, including tool argument assembly, validation, approval, child initialization, partial presentation, result/pending-action identity, todo rehydration, all seven umbrella checkers, their documented bounds/properties, primary focused suites, lifecycle E2E files, root package scripts, and CI workflow invocations. It does not claim semantic completeness for dynamic calls, generated code, dependencies, ignored files, deployment settings, or production histories. A future code change can invalidate completeness; `LIFE-GAP-016` and `LIFE-GAP-034` exist specifically to make this inventory mechanically maintainable. ## Historical provenance diff --git a/docs/architecture/task-lifecycle-model.md b/docs/architecture/task-lifecycle-model.md index 49c2e47a54..2aaff60cf5 100644 --- a/docs/architecture/task-lifecycle-model.md +++ b/docs/architecture/task-lifecycle-model.md @@ -168,16 +168,19 @@ The exhaustive repository inventory, ranked stable burn-down register, source-ba - **Planned-only:** specifies behavior not enabled in production. - **Type/static convention:** centralized typing or guidance without repository-wide enforcement. -| Gap | Invariant | Current evidence | Production/model boundary and runtime impact | Missing work and objective closure criteria | -| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Cross-host completion ownership | A completion may mutate a parent only while that parent authoritatively awaits the same child. | **Known-unsafe witness** plus a production-backed reducer guard. | The reducer rejects stale authoritative input, but disk revalidation checks status legality rather than exact-child ownership. A stale host can orphan the newer child. | Revalidate exact ownership under the disk lock; add deterministic two-store regression coverage; replace the expected witness with a universal bounded invariant. | -| Monotonic detachment | Message persistence must never restore lineage after abandonment clears it. | **Known-unsafe witness** plus a production-backed detach reducer. | A live task rebuilds lineage from stale fields; a later delta can restore parent/root IDs while preserving the interrupted status. | Give lifecycle fields a disk-authoritative write owner or tombstone/generation; prove stale metadata saves cannot alter them; promote the witness to an invariant. | -| Task-local mode consumers | Every mode-sensitive child operation must use the child task's immutable mode, not shared provider/view state. | Write side: **production-backed bounded matrix**. Read side: **proxy/partial and known unsafe**. | The selector snapshot can be correct while environment rendering and tool validation consume shared mode, causing wrong prompts or permissions. | Fix confirmed consumers, inventory other mode readers, add divergent-mode production tests, and enforce a task-local reader boundary before claiming universal isolation. | -| Task-local todo rehydration | Initial and updated todos must remain task-ID-scoped, independent from parent state, and equivalent before and after rehydration. | Creation forwarding has focused tests; persistence/rehydration is **unmodeled**. | Normal creation uses explicit model-supplied child todos rather than copying the parent, but initial todos are process-local and can disappear before the first persisted todo update. | Define a durable todo owner; prove omitted child todos do not copy the parent; preserve explicit child todos across switching/restart with deep-copy, precedence, UI scoping, and E2E checks (`LIFE-GAP-035`). | -| Serial versus fan-out delegation | The shipped serial path permits one awaited/running child and resumes its parent only after child release; fan-out must not be implied by an abstract model. | Serial ownership is **production-backed bounded**; the added fan-out checker is **planned-only abstract bounded**. | Production suspends the parent and uses a one-permit scheduler by default. The fan-out model imports no live-parent production transition, so it cannot prove fan-out behavior. | Either fix scheduler capacity at one and make concurrent fan-out explicitly unreachable, or implement live-parent isolation, rollback, routing, orphan cleanup, and E2E before promoting fan-out coverage. | -| Shared lifecycle status vocabulary | All consumers should derive task status from one exported owner. | **Type/static convention**; the visible interrupted-status symptom is repaired. | Persistence derives its alias from `HistoryItem`, but CLI history adapters still copy the union, allowing future drift. | Export one shared status type, consume it at CLI/persistence boundaries, and rely on typechecking or a static rule to reject copied incompatible vocabulary. | -| Cross-model refinement | No independent checker result may be combined into a stronger end-to-end claim without an executable boundary mapping. | **Explicit limitation** only. | Persistence, provider publication, parser scope, cleanup, and completion durability run as separate state spaces; omitted consumers can violate a premise after another checker passes. | Add production-grounded boundary events and a bounded joint strategy for each genuinely cross-model invariant, or keep claims explicitly local. | -| Completion event consumers | Public completion must not be treated as universally durable beyond the modeled readiness contract. | **Abstract bounded** model plus focused tests and one fresh-host E2E path. | The model abstracts durability and parent reopen; provider status metadata and downstream `TaskCompleted` consumers are outside its state. | Inventory downstream consumers and add refinement checks only for guarantees they require; retain power-loss, filesystem, retry-count, and liveness exclusions. | +| Gap | Invariant | Current evidence | Production/model boundary and runtime impact | Missing work and objective closure criteria | +| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Cross-host completion ownership | A completion may mutate a parent only while that parent authoritatively awaits the same child. | **Known-unsafe witness** plus a production-backed reducer guard. | The reducer rejects stale authoritative input, but disk revalidation checks status legality rather than exact-child ownership. A stale host can orphan the newer child. | Revalidate exact ownership under the disk lock; add deterministic two-store regression coverage; replace the expected witness with a universal bounded invariant. | +| Monotonic detachment | Message persistence must never restore lineage after abandonment clears it. | **Known-unsafe witness** plus a production-backed detach reducer. | A live task rebuilds lineage from stale fields; a later delta can restore parent/root IDs while preserving the interrupted status. | Give lifecycle fields a disk-authoritative write owner or tombstone/generation; prove stale metadata saves cannot alter them; promote the witness to an invariant. | +| Task-local mode consumers | Every mode-sensitive child operation must use the child task's immutable mode, not shared provider/view state. | Write side: **production-backed bounded matrix**. Read side: **proxy/partial and known unsafe**. | The selector snapshot can be correct while environment rendering and tool validation consume shared mode, causing wrong prompts or permissions. | Fix confirmed consumers, inventory other mode readers, add divergent-mode production tests, and enforce a task-local reader boundary before claiming universal isolation. | +| Tool-originated child initialization | Every required child field originating in `new_task` must be task-scoped, durably owned, and equivalent after rehydration. | Argument forwarding has focused tests; durable initial-state refinement is **unmodeled**. | Normal creation uses explicit model-supplied child todos rather than copying the parent, but initial todos demonstrate that some child state is process-local and can disappear. | Inventory all child initialization fields; define durable ownership, deep-copy, precedence, publication, and rehydration contracts with focused and E2E checks (`LIFE-GAP-035`). | +| Tool approval ownership | An interactive edit or settlement may affect only the matching task, action, and tool call. | Single-approval behavior has focused tests; cross-task correlation is **unmodeled and known unsafe by inspection**. | `update_todo_list` uses process-global edit state without task/action identity, allowing delayed or concurrent approval contamination. | Correlate proposal/edit/approval/cancellation by task and action/tool-call ID; reject stale edits and test interleavings (`LIFE-GAP-036`). | +| Tool partial-state isolation | Partial presentation state must be owned by one task and tool call and cleared on every terminal path. | Tool-local tests only; cross-call/task interleavings are **unmodeled**. | Singleton handlers share `lastSeenPartialPath`, so another call can create false or missed path stabilization. | Key state by `(taskId, toolCallId)` or instantiate handlers per call; test interleavings and cleanup (`LIFE-GAP-037`). | +| Tool identity correspondence | Parsed call, durable history, approval, execution, result, pending action, and replay must have one collision-resistant identity. | Duplicate-ID helper tests are **proxy/partial**; end-to-end correspondence is **unmodeled**. | Non-injective sanitization can collapse distinct raw IDs in history while execution still treats them as separate calls. | Reject/disambiguate collisions and prove a one-to-one identity mapping across native/MCP calls and restart (`LIFE-GAP-038`). | +| Serial versus fan-out delegation | The shipped serial path permits one awaited/running child and resumes its parent only after child release; fan-out must not be implied by an abstract model. | Serial ownership is **production-backed bounded**; the added fan-out checker is **planned-only abstract bounded**. | Production suspends the parent and uses a one-permit scheduler by default. The fan-out model imports no live-parent production transition, so it cannot prove fan-out behavior. | Either fix scheduler capacity at one and make concurrent fan-out explicitly unreachable, or implement live-parent isolation, rollback, routing, orphan cleanup, and E2E before promoting fan-out coverage. | +| Shared lifecycle status vocabulary | All consumers should derive task status from one exported owner. | **Type/static convention**; the visible interrupted-status symptom is repaired. | Persistence derives its alias from `HistoryItem`, but CLI history adapters still copy the union, allowing future drift. | Export one shared status type, consume it at CLI/persistence boundaries, and rely on typechecking or a static rule to reject copied incompatible vocabulary. | +| Cross-model refinement | No independent checker result may be combined into a stronger end-to-end claim without an executable boundary mapping. | **Explicit limitation** only. | Persistence, provider publication, parser scope, cleanup, and completion durability run as separate state spaces; omitted consumers can violate a premise after another checker passes. | Add production-grounded boundary events and a bounded joint strategy for each genuinely cross-model invariant, or keep claims explicitly local. | +| Completion event consumers | Public completion must not be treated as universally durable beyond the modeled readiness contract. | **Abstract bounded** model plus focused tests and one fresh-host E2E path. | The model abstracts durability and parent reopen; provider status metadata and downstream `TaskCompleted` consumers are outside its state. | Inventory downstream consumers and add refinement checks only for guarantees they require; retain power-loss, filesystem, retry-count, and liveness exclusions. | ## Historical provenance From 7ea3edd9e5a83fc2c263821c55a34e7866679265 Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 13 Sep 2026 14:58:51 +0000 Subject: [PATCH 13/17] docs(lifecycle): add remediation portfolio sizing --- .../architecture/task-lifecycle-gap-report.md | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/docs/architecture/task-lifecycle-gap-report.md b/docs/architecture/task-lifecycle-gap-report.md index 1a2ff5f527..04953d90fc 100644 --- a/docs/architecture/task-lifecycle-gap-report.md +++ b/docs/architecture/task-lifecycle-gap-report.md @@ -207,6 +207,83 @@ Severity reflects plausible data loss, ownership corruption, permission/context | LIFE-GAP-037 | Medium-high | High | Singleton tool handlers share partial presentation state across calls/tasks, so interleaved paths can cause false or missed stabilization. | Interleave A:`x`, B:`y`, A:`x` or A:`x`, B:`x` through one handler's `lastSeenPartialPath`. | Per-call handler state keyed by task and tool-call identity. | Isolate partial state by `(taskId, toolCallId)` or handler instance; prove independent stabilization and cleanup after success, malformed finalization, rejection, cancellation, abandonment, and incomplete streams. | | LIFE-GAP-038 | High | High | Lossy tool-ID canonicalization can deduplicate persisted history without deduplicating execution, results, approvals, or pending-action replay. | Distinct raw IDs such as `call:a` and `call/a` both sanitize to `call_a`; history may retain one call while execution retains both. | One collision-resistant canonical call identity before indexing and persistence. | Reject or disambiguate collisions; prove a bijection among parsed call, durable tool use, approval, execution, result, pending action, and replay; test adversarial native/MCP IDs and restart between approval and settlement. | +## Portfolio remediation plan + +The 38 IDs are not 38 independent projects. They group into eight programs with shared root causes and implementation surfaces. Estimates are engineering effort, not calendar commitments; they include implementation, deterministic tests, proportional model/refinement work, documentation, and stabilization. + +| Cluster | Gap IDs | Root fix and likely ownership | Size / effort | Engineering risk | Objective portfolio evidence | +| ------------------------------------------ | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| P1. Persisted ownership and generation | 001, 002, 012, 017, 020 | Disk-authoritative lifecycle ownership/generation and immutable store reads across history types, lifecycle reducers, `TaskHistoryStore`, provider delegation, and reconciliation. | XL, 15–25 engineer-days | High: persisted compatibility and cross-host races | Two-host stale-write tests, promoted invariants, restart/reconciliation evidence, backward-compatible optional data. | +| P2. Durable operation and crash recovery | 004, 005, 006, 021, 023 | Operation intent/replay or explicit idempotent recovery for pair writes, delegation, completion messages, shutdown, and deletion. | XL, 20–35 days | Very high: failure ordering can create new corruption | Fault injection at every durable boundary, crash/restart convergence, no false success, recovery reachability. | +| P3. Schema, path, and lifecycle vocabulary | 013, 018, 019 | Schema-derived status ownership, validated ordinary history reads, and one safe task-ID boundary across types, persistence, metadata, CLI, and import paths. | M, 5–9 days | Medium: malformed legacy data and downgrade behavior | Migration/quarantine fixtures, traversal tests, type/static ratchets. | +| P4. Request, stream, and tool identity | 008, 010, 024, 025, 026, 030, 037, 038 | Request generation plus canonical call identity, then task/generation/call-scoped parser and partial-handler state. Owners include provider transforms, parser, `Task`, `BaseTool`, editing handlers, and tool-ID utilities. | XL, 18–30 days | High: provider compatibility and duplicate execution | Adversarial IDs/index-less streams, delayed/cancelled generation tests, cleanup/deadline checks, production-backed call-state model. | +| P5. Tool-owned task state and queueing | 003, 007, 031, 035, 036 | Task-local context, durable child initialization, correlated approval identity, and claim/persist/ack queueing across tools, `Task`, provider/webview, message queue, and history schema. | XL, 16–27 days | High: cross-task contamination and persistence precedence | Omitted/explicit child controls, switch/restart E2E, two-approval schedules, queue failure retention, mode-permission tests. | +| P6. Event and ingress contracts | 009, 011, 022, 027, 028, 029, 032, 033 | Classify barriers versus notifications; normalize lifecycle payloads and clear/resume semantics across Task, provider, public API, IPC, and webview. | L, 12–20 days | Medium-high: public compatibility and ordering | Exactly-once event tests, consumer inventory, cross-surface contract matrix, compatibility adapters where required. | +| P7. Scheduler and fan-out decision | 014 | Enforce serial capacity, or implement live-parent fan-out across scheduler, registry, routing, rollback, orphan cleanup, webview scoping, and E2E. | Serial: M, 3–6 days. Fan-out: XL, 20–35 days | Serial low-medium; fan-out very high | Serial API/capacity ratchet, or production imports plus concurrent/failure E2E before fan-out reclassification. | +| P8. Verification and traceability platform | 015, 016, 034 | Machine-readable model metadata/traceability and selected cross-model trace validation across checker scripts, package commands, CI, and architecture docs. | L, 7–12 days | Medium: vacuity and CI cost | CI validates IDs, symbols, tests, bounds, actions, landmarks, workflows, and executable mappings for cross-model claims. | + +### Root fixes that close multiple gaps + +- One persisted generation and disk-authoritative ownership design should close 001, 002, and 012; immutable reads and explicit reconciliation semantics address 017/020 around that owner. +- One durable operation-intent/replay framework can support 004, 005, 006, 021, and 023, but each operation still needs its own legal recovery states and fault-injection matrix. +- One request-generation/canonical-call identity established before parser indexing can support 008, 010, 024–026, 030, 037, and 038. +- One correlated `(taskId, actionId, toolCallId)` approval protocol can close 036 and support 007/035; it does not itself make child state durable. +- One typed lifecycle operation layer can normalize P6, but public compatibility requires separate adapters rather than a flag-day payload rewrite. + +### Independent work that should not be collapsed + +- Schema/path hardening (P3) is reviewable independently from transaction recovery (P2), despite shared persistence files. +- Serial-versus-fan-out is a product decision and must not be hidden inside scheduler cleanup. +- Completion consumer contracts (011) are not solved by safe EventEmitter listeners (009). +- Durable child initialization (035) and approval correlation (036) need separate persistence and cancellation owners. +- Verification platform work can proceed in parallel, but cannot promote another cluster before its production transition exists. + +### Sequencing and critical path + +1. **Foundation:** choose serial versus fan-out (serial recommended for current behavior), define lifecycle ownership/generation (P1), and define canonical request/tool identity (P4). +2. **Integrity:** build durable operation recovery (P2) on P1. Run P3 in parallel once legacy-data policy is settled. +3. **Task isolation:** implement P5 using P4 identity and P1/P2 persistence rules. +4. **Surface convergence:** implement P6 after barrier/notification and generation semantics are known. +5. **Mechanical assurance:** start P8 metadata early; add cross-model refinement as production owners land. + +Critical path: **P7 decision → P1 ownership/generation → P2 recovery → P5 durable task state → P6 public contracts**. P3 and P8 metadata can run in parallel from the first tranche. P4 can run beside P1 after agreeing how task and request generations relate. + +### Quick wins versus architectural programs + +| Category | Scope | Effort | Notes | +| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | --------------------------------------------------------------------------------- | +| Quick wins | 013 shared status type; 025 listener cleanup; 029 projection naming/contracts; 034 checker metadata. | 1–4 days each | Separate PRs; reduce drift but do not close cross-host integrity. | +| Medium projects | 019 path safety, 021 disposal drain, 022/033 ingress convergence, 024 parser cleanup, 026 hard deadline, 027/028 event ownership, serial 014. | 3–8 days each | Focused subsystem work with deterministic tests. | +| Architectural programs | P1, P2, P4 identity/generation, P5 durable task state, P6 public contracts, or full fan-out. | 12–35 days per program | Require staged PRs, failure injection, compatibility plans, and model refinement. | + +### Overall scale and parallel workstreams + +With the recommended serial contract, shared infrastructure reduces the portfolio to approximately **85–145 engineer-days**. Full fan-out raises it to roughly **105–175 engineer-days** and increases critical-path risk. These are effort confidence bands, not delivery dates. + +Four workstreams can proceed concurrently after foundation decisions: + +1. persistence ownership/recovery (P1/P2); +2. request/tool identity and streaming (P4); +3. schema/path hardening and verification metadata (P3 plus P8 metadata); +4. event/ingress compatibility design (P6 discovery, implementation after generation semantics). + +### Recommended first tranche + +1. Decide and enforce serial behavior for 014 unless fan-out is explicitly funded. +2. Add deterministic failing tests for 001/002/012, then implement their shared ownership/generation primitive. +3. Define canonical call identity and adversarial tests for 038/008; reuse it for 037 and 036. +4. Land independent hardening for 013, 025, 029, and 034. +5. Add P8 machine-readable mapping incrementally so closure PRs name symbols, witnesses, tests, bounds, and evidence class. + +### Sizing assumptions and reconciliations + +- Effort includes focused/full tests and relevant E2E, not only code edits. +- Crash-consistency closure requires deterministic interruption and rollback fault injection; happy paths do not close P2. +- Prefer lazy optional-field migrations. Existing lost data is unrecoverable; downgrade readers must ignore new fields safely. +- High severity is reserved for demonstrated corruption, cross-task permission/state contamination, or execution/history divergence. Gap 035 remains Medium because its confirmed witness loses planning state; 036 and 038 remain High because they cross task/call ownership. +- Gap 014 remains Medium while fan-out is disabled; risk rises if production concurrency is enabled prematurely. +- Re-estimate after P1, P4, and P7 decisions because they define shared interfaces. + ## Burn-down dependencies | Dependency | Enables | From 64a0ec8e7ad3806eada4f99ed3e43c0c0fcdd25e Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 13 Sep 2026 15:11:19 +0000 Subject: [PATCH 14/17] docs(lifecycle): separate fan-out from baseline --- .../architecture/task-lifecycle-gap-report.md | 123 +++++++++--------- docs/architecture/task-lifecycle-model.md | 47 +++---- package.json | 3 +- 3 files changed, 89 insertions(+), 84 deletions(-) diff --git a/docs/architecture/task-lifecycle-gap-report.md b/docs/architecture/task-lifecycle-gap-report.md index 04953d90fc..17048eb6c9 100644 --- a/docs/architecture/task-lifecycle-gap-report.md +++ b/docs/architecture/task-lifecycle-gap-report.md @@ -142,7 +142,7 @@ Task events are forwarded by `ClineProvider`, enriched and re-emitted by `src/ex | Lifecycle reducers | Exact parent-child ownership, acyclicity, terminal immutability | `taskLifecycle.spec.ts` | `subtasks.test.ts` | `lifecycle:model-check`, unit, E2E | Production-backed bounded | | Delta/merge/store | Field preservation, status legality, pair order/failure | store unit, cross-instance, real-lock smoke | None direct | model umbrella, unit | Production-backed bounded plus known-unsafe witnesses | | Handoff selector/reducers | Commit-before-start, publication, permit/redelegation ordering | provider handoff, scheduler, delegation tests | subtask profile/resume paths | model umbrella, unit, E2E | Mixed production/abstract | -| Fan-out | Two siblings, result writer/delivery, orphan cleanup | Scheduler primitives only | None | model umbrella | Planned-only abstract | +| Optional fan-out | Two siblings, result writer/delivery, orphan cleanup | Scheduler primitives only | None | explicit optional command | Planned-only abstract, excluded from baseline | | Cleanup | At-most-once abort/dispose, settlement order, provider drain | Task/provider cleanup tests | Indirect cancellation paths | model umbrella, unit, E2E | Abstract bounded plus focused tests | | Parser scopes | Scope-owned IDs/arguments, exactly-once finalization | parser/provider stream tests | Indirect | model umbrella, unit | Production-backed bounded replay | | Completion readiness | Durability before event, retry/cancel/reopen ordering | Task/completion tool tests | fresh-host restart | model umbrella, unit, E2E | Abstract bounded plus refinement witnesses | @@ -166,61 +166,61 @@ CI runs lint, typecheck, and `pnpm lifecycle:model-check` in `.github/workflows/ Severity reflects plausible data loss, ownership corruption, permission/context errors, or stuck work. Confidence reflects direct source evidence, deterministic witness, or inference. -| ID | Severity | Confidence | Gap and production impact | Witness/reproducer | Dependencies | Objective closure criteria | -| ------------ | ----------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| LIFE-GAP-001 | Critical | High | Stale cross-host completion can clear a newer handoff and orphan its child. | Exact shortest witness in shared-store checker. | Disk-authoritative ownership guard or generation. | Deterministic two-store test passes; authoritative child ID is revalidated under disk lock; witness becomes universal invariant. | -| LIFE-GAP-002 | High | High | Stale message save can restore abandoned lineage. | Exact shortest stale-save/abandon witness. | Lifecycle-field ownership, tombstone, or generation. | Stale save cannot alter detached lineage in production test or bounded model; witness promoted. | -| LIFE-GAP-003 | High | High | Legacy dequeue-before-submit can lose queued user feedback on submission failure. | `Task.processQueuedMessages` removes before async submit. | Standardize claim/persist/ack path. | Every queue consumer claims first, removes after durable acceptance, releases on failure; failure test retains message. | -| LIFE-GAP-004 | High | High | Pair writes are not cross-host/crash atomic; partial lifecycle state is observable. | Modeled second-write failure landmark. | WAL/repair intent or explicitly idempotent recovery per pair operation. | Crash injection at each write boundary converges to a documented legal state. | -| LIFE-GAP-005 | High | High | Delegation creates child state and commits parent ownership in separate failure domains. | Provider rollback tests cover selected failures. | Idempotent operation ID or repair intent. | Failure injection before/after every persistence/publication step restores parent or completes delegation without orphan state. | -| LIFE-GAP-006 | High | Medium-high | Parent completion messages can persist before lifecycle completion commit fails. | Source ordering in `reopenParentFromDelegation`. | Transaction/intent or reorder with recovery. | Inject pair-write failure and prove no stale result context, or prove replay safely completes the lifecycle. | -| LIFE-GAP-007 | High | Medium-high | Task-local mode reader refinement is incomplete; wrong shared mode can affect prompts or permissions. | Divergent mode witness; known consumer regression. | Reader inventory and task-local API boundary. | Every mode-sensitive reader classified; required readers use task-local mode; divergent production tests cover both permission directions. | -| LIFE-GAP-008 | High | High | Responses API argument-only deltas may be dropped; absent indices can alias calls. | Transform requires ID/name and defaults index to zero. | Provider transform correlation design. | Multi-delta and concurrent index-less production tests reconstruct isolated calls; parser model includes mapped transform events. | -| LIFE-GAP-009 | Medium | High | Async lifecycle event listeners have no awaited settlement contract. | Async listeners registered on Node EventEmitter. | Classify notification versus barrier events. | Barrier side effects move to awaited methods; notification listeners have contained rejection tests and documented ordering. | -| LIFE-GAP-010 | Medium | Medium-high | Detached usage drain can write after abort, replacement, delegation, or a newer request generation. | Background iterator mutates/persists without generation guard. | Request-generation ownership. | Late drain may account valid usage but cannot mutate stale UI/message/lifecycle state; controlled delayed-stream test passes. | -| LIFE-GAP-011 | Medium | High | Completion model excludes terminal status persistence and downstream public consumers. | Model ends at emission readiness. | Consumer inventory and contract. | Consequential consumers are enumerated; required status/metadata ordering has production refinement tests. | -| LIFE-GAP-012 | Medium | High | No persisted attempt/generation distinguishes delayed pre-interruption completion from valid post-resume completion of the same child. | Documented model exclusion. | Persisted generation token. | Reducer/store/API/model reject stale generation while accepting resumed generation; restart E2E covers it. | -| LIFE-GAP-013 | Medium | High | Task lifecycle status vocabulary is copied across schema, task metadata, Task, CLI, and history reader. | Literal union inventory. | Shared exported schema-derived type. | Consumers import one owner; CI/static check rejects incompatible local copies. | -| LIFE-GAP-014 | Medium | High | Fan-out checker is planned-only but runs in the green umbrella, inviting overclaim. | No production imports or fan-out E2E. | Product decision. | Either enforce serial capacity as invariant, or implement fan-out adapters and E2E before reclassifying. | -| LIFE-GAP-015 | Medium | High | Independent checks do not establish end-to-end refinement. | Seven disjoint state spaces. | Boundary mappings and tractable joint bounds. | Add joint checker/trace validation for each cross-model claim, or keep every claim explicitly local. | -| LIFE-GAP-016 | Medium | High | Traceability is documentary and can drift from scripts, symbols, tests, and CI. | Shared-store scenario count has drifted in documentation. | Machine-readable manifest/checker summaries. | CI validates stable IDs, model membership, bounds, symbol/test paths, and workflow invocation. | -| LIFE-GAP-017 | Medium | High | Store cache records are exposed without cloning; external mutation may bypass locking. | `get`/`getAll` return cached objects. | Immutability/read API decision. | Freeze/clone records or prove callers cannot mutate; mutation regression test. | -| LIFE-GAP-018 | Medium | High | Ordinary history-file reads cast JSON instead of applying the shared schema. | Store reconciliation/read path. | Validation/quarantine policy. | Malformed records are rejected or quarantined deterministically with tests and recovery documentation. | -| LIFE-GAP-019 | Medium | High | Generic task IDs lack the importer’s explicit path-safety validation. | Importer validates IDs; generic paths interpolate IDs. | Shared safe-ID boundary. | Every filesystem task ID passes one validator; traversal and separator tests cover all entry points. | -| LIFE-GAP-020 | Medium | Medium-high | Watch/reconcile convergence is eventual and failure-tolerant, not coherent. | Debounced watcher plus periodic scan. | Version/notification or documented eventual contract. | Define stale-read window and convergence property; multi-host test covers missed watcher event and concurrent update. | -| LIFE-GAP-021 | Medium | High | Store disposal does not await queued writes. | Synchronous `dispose` stops watcher/timer only. | Async drain/close contract. | Disposal awaits or explicitly cancels writes; no post-dispose writes in deterministic test. | -| LIFE-GAP-022 | Medium | High | Public clear and webview clear use different delegated-child semantics. | API uses eviction; webview removes directly. | One clear contract. | All ingress paths converge on the same lifecycle transition and tests assert identical persisted results. | -| LIFE-GAP-023 | Medium | High | History deletion can be resurrected after swallowed unlink failure. | Cache removal precedes best-effort unlink. | Tombstone or surfaced failure/retry. | Inject unlink failure and prove item stays deleted or operation reports failure without false success. | -| LIFE-GAP-024 | Medium | High | Parser cleanup depends on normal finalization or garbage collection. | Weak scope maps lack universal request `finally`. | Request-level cleanup owner. | Abort/error/success all clear active parser state in production integration tests. | -| LIFE-GAP-025 | Medium | High | Abort listeners may accumulate during successful stream chunks. | Per-chunk listener removed only by abort. | Settle-time listener cleanup. | Long stream keeps bounded listener count and removes listeners on both race outcomes. | -| LIFE-GAP-026 | Medium | Medium-high | Usage-drain timeout cannot interrupt a permanently pending `iterator.next()`. | Elapsed time checked before await. | Deadline race/abort. | Hung iterator settles drain within wall-clock bound in fake-timer test. | -| LIFE-GAP-027 | Medium | High | Task-level delegation listeners are untyped/dead while provider listeners own the same public events. | `src/extension/api.ts` registers both paths. | Single typed event owner. | Remove duplicate/dead listeners or define one source; public API test proves exactly-once emission. | -| LIFE-GAP-028 | Low-medium | High | `TaskSpawned` payload semantics differ across task/provider/public surfaces. | Child-only, ambiguous task ID, and parent+child forms. | Event contract normalization. | Payloads use explicit names and adapters are type-checked with compatibility tests. | -| LIFE-GAP-029 | Low-medium | High | `Task.taskStatus` and `TaskRegistry.getRunning` are projections, not scheduler/persistence truth. | Ask markers and abort flags only. | Naming/contract clarification. | Rename or document exact predicates; callers stop using them as stronger lifecycle evidence. | -| LIFE-GAP-030 | Low-medium | Medium | `Task.run()` may resolve immediately if another path already started the task. | `_started` short-circuit versus scheduler callback. | Single start owner. | Scheduler-facing start returns the actual run promise; duplicate-start test proves settlement identity. | -| LIFE-GAP-031 | Low-medium | High | Queue state is memory-only and cleared on disposal. | `MessageQueueService.dispose`. | Product durability decision. | Document intentional loss or persist claims/messages with restart tests. | -| LIFE-GAP-032 | Low-medium | Medium | Webview abandonment handler exists without a confirmed production UI sender. | Protocol/handler search only. | Reachability decision. | Add supported sender/E2E or remove/deprecate unreachable command. | -| LIFE-GAP-033 | Low-medium | High | Resume ingress differs in awaiting and error propagation. | Webview/API/IPC adapters diverge. | Shared resume operation contract. | Contract tests compare result/error/publication semantics for each surface. | -| LIFE-GAP-034 | Low | High | Bounds and model metadata are handwritten and not mechanically synchronized. | Constants, prose, and console summaries duplicate values. | Machine-readable checker metadata. | CI compares emitted metadata with docs/manifest and rejects undocumented bound/action/landmark changes. | -| LIFE-GAP-035 | Medium | High | Tool-originated child initialization lacks a complete durable ownership contract; initial todo state is the confirmed witness and can disappear after rehydration. | Create a child with explicit initial todos, switch or restart before `update_todo_list`, then reopen it; restoration finds no todo message and yields an empty list. | Canonical durable task-ID-scoped child-initialization owner and publication contract. | Inventory every `new_task`-originated child field; persist required initial state before visibility/run; restore deep-equal independent state across switching, interrupted resume, checkpoint restore, and fresh-host restart; preserve later-update and explicit-empty precedence; add constructor deep-copy, persistence, webview scoping, and E2E witnesses. | -| LIFE-GAP-036 | High | High | Interactive todo approval edit state is process-global and uncorrelated; one task's delayed edit can be consumed by another task's pending approval. | Start approvals for tasks A and B, send A's edited list through `setPendingTodoList`, then resolve B; B reads the shared `approvedTodoList`. | Task/action/tool-call-correlated approval state and webview protocol. | Carry task ID and action/tool-call ID through proposal, webview edit, approval, cancellation, and settlement; reject stale/mismatched edits; deep-clone inputs; test two interleaved approvals, denial, cancellation, task switch, and delayed edits. | -| LIFE-GAP-037 | Medium-high | High | Singleton tool handlers share partial presentation state across calls/tasks, so interleaved paths can cause false or missed stabilization. | Interleave A:`x`, B:`y`, A:`x` or A:`x`, B:`x` through one handler's `lastSeenPartialPath`. | Per-call handler state keyed by task and tool-call identity. | Isolate partial state by `(taskId, toolCallId)` or handler instance; prove independent stabilization and cleanup after success, malformed finalization, rejection, cancellation, abandonment, and incomplete streams. | -| LIFE-GAP-038 | High | High | Lossy tool-ID canonicalization can deduplicate persisted history without deduplicating execution, results, approvals, or pending-action replay. | Distinct raw IDs such as `call:a` and `call/a` both sanitize to `call_a`; history may retain one call while execution retains both. | One collision-resistant canonical call identity before indexing and persistence. | Reject or disambiguate collisions; prove a bijection among parsed call, durable tool use, approval, execution, result, pending action, and replay; test adversarial native/MCP IDs and restart between approval and settlement. | +| ID | Severity | Confidence | Gap and production impact | Witness/reproducer | Dependencies | Objective closure criteria | +| ------------ | ----------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| LIFE-GAP-001 | Critical | High | Stale cross-host completion can clear a newer handoff and orphan its child. | Exact shortest witness in shared-store checker. | Disk-authoritative ownership guard or generation. | Deterministic two-store test passes; authoritative child ID is revalidated under disk lock; witness becomes universal invariant. | +| LIFE-GAP-002 | High | High | Stale message save can restore abandoned lineage. | Exact shortest stale-save/abandon witness. | Lifecycle-field ownership, tombstone, or generation. | Stale save cannot alter detached lineage in production test or bounded model; witness promoted. | +| LIFE-GAP-003 | High | High | Legacy dequeue-before-submit can lose queued user feedback on submission failure. | `Task.processQueuedMessages` removes before async submit. | Standardize claim/persist/ack path. | Every queue consumer claims first, removes after durable acceptance, releases on failure; failure test retains message. | +| LIFE-GAP-004 | High | High | Pair writes are not cross-host/crash atomic; partial lifecycle state is observable. | Modeled second-write failure landmark. | WAL/repair intent or explicitly idempotent recovery per pair operation. | Crash injection at each write boundary converges to a documented legal state. | +| LIFE-GAP-005 | High | High | Delegation creates child state and commits parent ownership in separate failure domains. | Provider rollback tests cover selected failures. | Idempotent operation ID or repair intent. | Failure injection before/after every persistence/publication step restores parent or completes delegation without orphan state. | +| LIFE-GAP-006 | High | Medium-high | Parent completion messages can persist before lifecycle completion commit fails. | Source ordering in `reopenParentFromDelegation`. | Transaction/intent or reorder with recovery. | Inject pair-write failure and prove no stale result context, or prove replay safely completes the lifecycle. | +| LIFE-GAP-007 | High | Medium-high | Task-local mode reader refinement is incomplete; wrong shared mode can affect prompts or permissions. | Divergent mode witness; known consumer regression. | Reader inventory and task-local API boundary. | Every mode-sensitive reader classified; required readers use task-local mode; divergent production tests cover both permission directions. | +| LIFE-GAP-008 | High | High | Responses API argument-only deltas may be dropped; absent indices can alias calls. | Transform requires ID/name and defaults index to zero. | Provider transform correlation design. | Multi-delta and concurrent index-less production tests reconstruct isolated calls; parser model includes mapped transform events. | +| LIFE-GAP-009 | Medium | High | Async lifecycle event listeners have no awaited settlement contract. | Async listeners registered on Node EventEmitter. | Classify notification versus barrier events. | Barrier side effects move to awaited methods; notification listeners have contained rejection tests and documented ordering. | +| LIFE-GAP-010 | Medium | Medium-high | Detached usage drain can write after abort, replacement, delegation, or a newer request generation. | Background iterator mutates/persists without generation guard. | Request-generation ownership. | Late drain may account valid usage but cannot mutate stale UI/message/lifecycle state; controlled delayed-stream test passes. | +| LIFE-GAP-011 | Medium | High | Completion model excludes terminal status persistence and downstream public consumers. | Model ends at emission readiness. | Consumer inventory and contract. | Consequential consumers are enumerated; required status/metadata ordering has production refinement tests. | +| LIFE-GAP-012 | Medium | High | No persisted attempt/generation distinguishes delayed pre-interruption completion from valid post-resume completion of the same child. | Documented model exclusion. | Persisted generation token. | Reducer/store/API/model reject stale generation while accepting resumed generation; restart E2E covers it. | +| LIFE-GAP-013 | Medium | High | Task lifecycle status vocabulary is copied across schema, task metadata, Task, CLI, and history reader. | Literal union inventory. | Shared exported schema-derived type. | Consumers import one owner; CI/static check rejects incompatible local copies. | +| LIFE-GAP-014 | Medium | High | The serial production contract relies on singular reducer ownership and a default one-permit provider scheduler; optional fan-out must not be mistaken for baseline coverage. | The production-backed lifecycle model rejects multiple active awaited children, while the optional fan-out model has no production imports or E2E. | Serial baseline ratchet; separately ticketed fan-out decision. | Baseline: close cross-host violations, assert provider scheduler capacity and serial ordering, and keep fan-out outside baseline CI. Optional fan-out: implement adapters/E2E before reclassification. | +| LIFE-GAP-015 | Medium | High | Independent checks do not establish end-to-end refinement. | Six baseline state spaces plus one optional fan-out state space remain disjoint. | Boundary mappings and tractable joint bounds. | Add joint checker/trace validation for each cross-model claim, or keep every claim explicitly local. | +| LIFE-GAP-016 | Medium | High | Traceability is documentary and can drift from scripts, symbols, tests, and CI. | Shared-store scenario count has drifted in documentation. | Machine-readable manifest/checker summaries. | CI validates stable IDs, model membership, bounds, symbol/test paths, and workflow invocation. | +| LIFE-GAP-017 | Medium | High | Store cache records are exposed without cloning; external mutation may bypass locking. | `get`/`getAll` return cached objects. | Immutability/read API decision. | Freeze/clone records or prove callers cannot mutate; mutation regression test. | +| LIFE-GAP-018 | Medium | High | Ordinary history-file reads cast JSON instead of applying the shared schema. | Store reconciliation/read path. | Validation/quarantine policy. | Malformed records are rejected or quarantined deterministically with tests and recovery documentation. | +| LIFE-GAP-019 | Medium | High | Generic task IDs lack the importer’s explicit path-safety validation. | Importer validates IDs; generic paths interpolate IDs. | Shared safe-ID boundary. | Every filesystem task ID passes one validator; traversal and separator tests cover all entry points. | +| LIFE-GAP-020 | Medium | Medium-high | Watch/reconcile convergence is eventual and failure-tolerant, not coherent. | Debounced watcher plus periodic scan. | Version/notification or documented eventual contract. | Define stale-read window and convergence property; multi-host test covers missed watcher event and concurrent update. | +| LIFE-GAP-021 | Medium | High | Store disposal does not await queued writes. | Synchronous `dispose` stops watcher/timer only. | Async drain/close contract. | Disposal awaits or explicitly cancels writes; no post-dispose writes in deterministic test. | +| LIFE-GAP-022 | Medium | High | Public clear and webview clear use different delegated-child semantics. | API uses eviction; webview removes directly. | One clear contract. | All ingress paths converge on the same lifecycle transition and tests assert identical persisted results. | +| LIFE-GAP-023 | Medium | High | History deletion can be resurrected after swallowed unlink failure. | Cache removal precedes best-effort unlink. | Tombstone or surfaced failure/retry. | Inject unlink failure and prove item stays deleted or operation reports failure without false success. | +| LIFE-GAP-024 | Medium | High | Parser cleanup depends on normal finalization or garbage collection. | Weak scope maps lack universal request `finally`. | Request-level cleanup owner. | Abort/error/success all clear active parser state in production integration tests. | +| LIFE-GAP-025 | Medium | High | Abort listeners may accumulate during successful stream chunks. | Per-chunk listener removed only by abort. | Settle-time listener cleanup. | Long stream keeps bounded listener count and removes listeners on both race outcomes. | +| LIFE-GAP-026 | Medium | Medium-high | Usage-drain timeout cannot interrupt a permanently pending `iterator.next()`. | Elapsed time checked before await. | Deadline race/abort. | Hung iterator settles drain within wall-clock bound in fake-timer test. | +| LIFE-GAP-027 | Medium | High | Task-level delegation listeners are untyped/dead while provider listeners own the same public events. | `src/extension/api.ts` registers both paths. | Single typed event owner. | Remove duplicate/dead listeners or define one source; public API test proves exactly-once emission. | +| LIFE-GAP-028 | Low-medium | High | `TaskSpawned` payload semantics differ across task/provider/public surfaces. | Child-only, ambiguous task ID, and parent+child forms. | Event contract normalization. | Payloads use explicit names and adapters are type-checked with compatibility tests. | +| LIFE-GAP-029 | Low-medium | High | `Task.taskStatus` and `TaskRegistry.getRunning` are projections, not scheduler/persistence truth. | Ask markers and abort flags only. | Naming/contract clarification. | Rename or document exact predicates; callers stop using them as stronger lifecycle evidence. | +| LIFE-GAP-030 | Low-medium | Medium | `Task.run()` may resolve immediately if another path already started the task. | `_started` short-circuit versus scheduler callback. | Single start owner. | Scheduler-facing start returns the actual run promise; duplicate-start test proves settlement identity. | +| LIFE-GAP-031 | Low-medium | High | Queue state is memory-only and cleared on disposal. | `MessageQueueService.dispose`. | Product durability decision. | Document intentional loss or persist claims/messages with restart tests. | +| LIFE-GAP-032 | Low-medium | Medium | Webview abandonment handler exists without a confirmed production UI sender. | Protocol/handler search only. | Reachability decision. | Add supported sender/E2E or remove/deprecate unreachable command. | +| LIFE-GAP-033 | Low-medium | High | Resume ingress differs in awaiting and error propagation. | Webview/API/IPC adapters diverge. | Shared resume operation contract. | Contract tests compare result/error/publication semantics for each surface. | +| LIFE-GAP-034 | Low | High | Bounds and model metadata are handwritten and not mechanically synchronized. | Constants, prose, and console summaries duplicate values. | Machine-readable checker metadata. | CI compares emitted metadata with docs/manifest and rejects undocumented bound/action/landmark changes. | +| LIFE-GAP-035 | Medium | High | Tool-originated child initialization lacks a complete durable ownership contract; initial todo state is the confirmed witness and can disappear after rehydration. | Create a child with explicit initial todos, switch or restart before `update_todo_list`, then reopen it; restoration finds no todo message and yields an empty list. | Canonical durable task-ID-scoped child-initialization owner and publication contract. | Inventory every `new_task`-originated child field; persist required initial state before visibility/run; restore deep-equal independent state across switching, interrupted resume, checkpoint restore, and fresh-host restart; preserve later-update and explicit-empty precedence; add constructor deep-copy, persistence, webview scoping, and E2E witnesses. | +| LIFE-GAP-036 | High | High | Interactive todo approval edit state is process-global and uncorrelated; one task's delayed edit can be consumed by another task's pending approval. | Start approvals for tasks A and B, send A's edited list through `setPendingTodoList`, then resolve B; B reads the shared `approvedTodoList`. | Task/action/tool-call-correlated approval state and webview protocol. | Carry task ID and action/tool-call ID through proposal, webview edit, approval, cancellation, and settlement; reject stale/mismatched edits; deep-clone inputs; test two interleaved approvals, denial, cancellation, task switch, and delayed edits. | +| LIFE-GAP-037 | Medium-high | High | Singleton tool handlers share partial presentation state across calls/tasks, so interleaved paths can cause false or missed stabilization. | Interleave A:`x`, B:`y`, A:`x` or A:`x`, B:`x` through one handler's `lastSeenPartialPath`. | Per-call handler state keyed by task and tool-call identity. | Isolate partial state by `(taskId, toolCallId)` or handler instance; prove independent stabilization and cleanup after success, malformed finalization, rejection, cancellation, abandonment, and incomplete streams. | +| LIFE-GAP-038 | High | High | Lossy tool-ID canonicalization can deduplicate persisted history without deduplicating execution, results, approvals, or pending-action replay. | Distinct raw IDs such as `call:a` and `call/a` both sanitize to `call_a`; history may retain one call while execution retains both. | One collision-resistant canonical call identity before indexing and persistence. | Reject or disambiguate collisions; prove a bijection among parsed call, durable tool use, approval, execution, result, pending action, and replay; test adversarial native/MCP IDs and restart between approval and settlement. | ## Portfolio remediation plan The 38 IDs are not 38 independent projects. They group into eight programs with shared root causes and implementation surfaces. Estimates are engineering effort, not calendar commitments; they include implementation, deterministic tests, proportional model/refinement work, documentation, and stabilization. -| Cluster | Gap IDs | Root fix and likely ownership | Size / effort | Engineering risk | Objective portfolio evidence | -| ------------------------------------------ | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| P1. Persisted ownership and generation | 001, 002, 012, 017, 020 | Disk-authoritative lifecycle ownership/generation and immutable store reads across history types, lifecycle reducers, `TaskHistoryStore`, provider delegation, and reconciliation. | XL, 15–25 engineer-days | High: persisted compatibility and cross-host races | Two-host stale-write tests, promoted invariants, restart/reconciliation evidence, backward-compatible optional data. | -| P2. Durable operation and crash recovery | 004, 005, 006, 021, 023 | Operation intent/replay or explicit idempotent recovery for pair writes, delegation, completion messages, shutdown, and deletion. | XL, 20–35 days | Very high: failure ordering can create new corruption | Fault injection at every durable boundary, crash/restart convergence, no false success, recovery reachability. | -| P3. Schema, path, and lifecycle vocabulary | 013, 018, 019 | Schema-derived status ownership, validated ordinary history reads, and one safe task-ID boundary across types, persistence, metadata, CLI, and import paths. | M, 5–9 days | Medium: malformed legacy data and downgrade behavior | Migration/quarantine fixtures, traversal tests, type/static ratchets. | -| P4. Request, stream, and tool identity | 008, 010, 024, 025, 026, 030, 037, 038 | Request generation plus canonical call identity, then task/generation/call-scoped parser and partial-handler state. Owners include provider transforms, parser, `Task`, `BaseTool`, editing handlers, and tool-ID utilities. | XL, 18–30 days | High: provider compatibility and duplicate execution | Adversarial IDs/index-less streams, delayed/cancelled generation tests, cleanup/deadline checks, production-backed call-state model. | -| P5. Tool-owned task state and queueing | 003, 007, 031, 035, 036 | Task-local context, durable child initialization, correlated approval identity, and claim/persist/ack queueing across tools, `Task`, provider/webview, message queue, and history schema. | XL, 16–27 days | High: cross-task contamination and persistence precedence | Omitted/explicit child controls, switch/restart E2E, two-approval schedules, queue failure retention, mode-permission tests. | -| P6. Event and ingress contracts | 009, 011, 022, 027, 028, 029, 032, 033 | Classify barriers versus notifications; normalize lifecycle payloads and clear/resume semantics across Task, provider, public API, IPC, and webview. | L, 12–20 days | Medium-high: public compatibility and ordering | Exactly-once event tests, consumer inventory, cross-surface contract matrix, compatibility adapters where required. | -| P7. Scheduler and fan-out decision | 014 | Enforce serial capacity, or implement live-parent fan-out across scheduler, registry, routing, rollback, orphan cleanup, webview scoping, and E2E. | Serial: M, 3–6 days. Fan-out: XL, 20–35 days | Serial low-medium; fan-out very high | Serial API/capacity ratchet, or production imports plus concurrent/failure E2E before fan-out reclassification. | -| P8. Verification and traceability platform | 015, 016, 034 | Machine-readable model metadata/traceability and selected cross-model trace validation across checker scripts, package commands, CI, and architecture docs. | L, 7–12 days | Medium: vacuity and CI cost | CI validates IDs, symbols, tests, bounds, actions, landmarks, workflows, and executable mappings for cross-model claims. | +| Cluster | Gap IDs | Root fix and likely ownership | Size / effort | Engineering risk | Objective portfolio evidence | +| ------------------------------------------ | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| P1. Persisted ownership and generation | 001, 002, 012, 017, 020 | Disk-authoritative lifecycle ownership/generation and immutable store reads across history types, lifecycle reducers, `TaskHistoryStore`, provider delegation, and reconciliation. | XL, 15–25 engineer-days | High: persisted compatibility and cross-host races | Two-host stale-write tests, promoted invariants, restart/reconciliation evidence, backward-compatible optional data. | +| P2. Durable operation and crash recovery | 004, 005, 006, 021, 023 | Operation intent/replay or explicit idempotent recovery for pair writes, delegation, completion messages, shutdown, and deletion. | XL, 20–35 days | Very high: failure ordering can create new corruption | Fault injection at every durable boundary, crash/restart convergence, no false success, recovery reachability. | +| P3. Schema, path, and lifecycle vocabulary | 013, 018, 019 | Schema-derived status ownership, validated ordinary history reads, and one safe task-ID boundary across types, persistence, metadata, CLI, and import paths. | M, 5–9 days | Medium: malformed legacy data and downgrade behavior | Migration/quarantine fixtures, traversal tests, type/static ratchets. | +| P4. Request, stream, and tool identity | 008, 010, 024, 025, 026, 030, 037, 038 | Request generation plus canonical call identity, then task/generation/call-scoped parser and partial-handler state. Owners include provider transforms, parser, `Task`, `BaseTool`, editing handlers, and tool-ID utilities. | XL, 18–30 days | High: provider compatibility and duplicate execution | Adversarial IDs/index-less streams, delayed/cancelled generation tests, cleanup/deadline checks, production-backed call-state model. | +| P5. Tool-owned task state and queueing | 003, 007, 031, 035, 036 | Task-local context, durable child initialization, correlated approval identity, and claim/persist/ack queueing across tools, `Task`, provider/webview, message queue, and history schema. | XL, 16–27 days | High: cross-task contamination and persistence precedence | Omitted/explicit child controls, switch/restart E2E, two-approval schedules, queue failure retention, mode-permission tests. | +| P6. Event and ingress contracts | 009, 011, 022, 027, 028, 029, 032, 033 | Classify barriers versus notifications; normalize lifecycle payloads and clear/resume semantics across Task, provider, public API, IPC, and webview. | L, 12–20 days | Medium-high: public compatibility and ordering | Exactly-once event tests, consumer inventory, cross-surface contract matrix, compatibility adapters where required. | +| P7. Serial scheduler baseline | 014 | Ratchet the current one-permit provider scheduler and singular active-child ownership; keep live-parent fan-out under separate future scope. | M, 3–6 days baseline | Low-medium: current behavior, but cross-host exceptions remain | Production capacity/order assertions plus lifecycle/store invariants proving the bounded serial contract. | +| P8. Verification and traceability platform | 015, 016, 034 | Machine-readable model metadata/traceability and selected cross-model trace validation across checker scripts, package commands, CI, and architecture docs. | L, 7–12 days | Medium: vacuity and CI cost | CI validates IDs, symbols, tests, bounds, actions, landmarks, workflows, and executable mappings for cross-model claims. | ### Root fixes that close multiple gaps @@ -233,20 +233,20 @@ The 38 IDs are not 38 independent projects. They group into eight programs with ### Independent work that should not be collapsed - Schema/path hardening (P3) is reviewable independently from transaction recovery (P2), despite shared persistence files. -- Serial-versus-fan-out is a product decision and must not be hidden inside scheduler cleanup. +- Optional fan-out is a separate product program and must not be hidden inside baseline scheduler closure. - Completion consumer contracts (011) are not solved by safe EventEmitter listeners (009). - Durable child initialization (035) and approval correlation (036) need separate persistence and cancellation owners. - Verification platform work can proceed in parallel, but cannot promote another cluster before its production transition exists. ### Sequencing and critical path -1. **Foundation:** choose serial versus fan-out (serial recommended for current behavior), define lifecycle ownership/generation (P1), and define canonical request/tool identity (P4). +1. **Foundation:** establish the current serial scheduler baseline (P7), define lifecycle ownership/generation (P1), and define canonical request/tool identity (P4). 2. **Integrity:** build durable operation recovery (P2) on P1. Run P3 in parallel once legacy-data policy is settled. 3. **Task isolation:** implement P5 using P4 identity and P1/P2 persistence rules. 4. **Surface convergence:** implement P6 after barrier/notification and generation semantics are known. 5. **Mechanical assurance:** start P8 metadata early; add cross-model refinement as production owners land. -Critical path: **P7 decision → P1 ownership/generation → P2 recovery → P5 durable task state → P6 public contracts**. P3 and P8 metadata can run in parallel from the first tranche. P4 can run beside P1 after agreeing how task and request generations relate. +Critical path: **P7 serial baseline → P1 ownership/generation → P2 recovery → P5 durable task state → P6 public contracts**. P3 and P8 metadata can run in parallel from the first tranche. P4 can run beside P1 after agreeing how task and request generations relate. ### Quick wins versus architectural programs @@ -258,7 +258,7 @@ Critical path: **P7 decision → P1 ownership/generation → P2 recovery → P5 ### Overall scale and parallel workstreams -With the recommended serial contract, shared infrastructure reduces the portfolio to approximately **85–145 engineer-days**. Full fan-out raises it to roughly **105–175 engineer-days** and increases critical-path risk. These are effort confidence bands, not delivery dates. +Baseline closure for exhaustive modeling and verification of current serial production behavior is approximately **85–145 engineer-days**. Concurrent fan-out is excluded from that range. If separately authorized, its live-parent execution, routing, rollback, orphan cleanup, UI scoping, E2E, and model-refinement prerequisites add approximately **20–35 engineer-days** after baseline foundations; that increment should be tracked by #369/#372 or their successor ticket. These are effort confidence bands, not delivery dates. Four workstreams can proceed concurrently after foundation decisions: @@ -269,7 +269,7 @@ Four workstreams can proceed concurrently after foundation decisions: ### Recommended first tranche -1. Decide and enforce serial behavior for 014 unless fan-out is explicitly funded. +1. Ratchet current serial behavior for 014 and leave fan-out to its separately scoped ticket. 2. Add deterministic failing tests for 001/002/012, then implement their shared ownership/generation primitive. 3. Define canonical call identity and adversarial tests for 038/008; reuse it for 037 and 036. 4. Land independent hardening for 013, 025, 029, and 034. @@ -281,8 +281,8 @@ Four workstreams can proceed concurrently after foundation decisions: - Crash-consistency closure requires deterministic interruption and rollback fault injection; happy paths do not close P2. - Prefer lazy optional-field migrations. Existing lost data is unrecoverable; downgrade readers must ignore new fields safely. - High severity is reserved for demonstrated corruption, cross-task permission/state contamination, or execution/history divergence. Gap 035 remains Medium because its confirmed witness loses planning state; 036 and 038 remain High because they cross task/call ownership. -- Gap 014 remains Medium while fan-out is disabled; risk rises if production concurrency is enabled prematurely. -- Re-estimate after P1, P4, and P7 decisions because they define shared interfaces. +- Gap 014 remains Medium because baseline seriality is partly production-backed but not statically ratcheted and cross-host ownership violations remain. Optional fan-out does not affect baseline severity. +- Re-estimate after P1 and P4 decisions because they define shared interfaces; estimate fan-out only in its separate scope. ## Burn-down dependencies @@ -290,11 +290,12 @@ Four workstreams can proceed concurrently after foundation decisions: | ---------------------------------------------- | ------------------------------------------------------------- | | Disk-authoritative ownership/generation design | LIFE-GAP-001, 002, 012 | | Durable operation intent/recovery design | LIFE-GAP-004, 005, 006, 023 | -| Task-local execution-context owner | LIFE-GAP-007, fan-out side of 014 | +| Task-local execution-context owner | LIFE-GAP-007 and optional future fan-out | | Request generation and terminal cleanup owner | LIFE-GAP-010, 024, 025, 026 | | Event notification/barrier contract | LIFE-GAP-009, 011, 027, 028 | | Machine-readable lifecycle manifest | LIFE-GAP-013, 016, 034 | -| Product decision on serial versus fan-out | LIFE-GAP-014 and future fan-out E2E | +| Serial scheduler baseline | LIFE-GAP-014 | +| Optional fan-out product program | Historical #369/#372 scope, outside baseline | | Durable task-scoped child initialization | LIFE-GAP-035 and future tool/lifecycle composition | | Correlated approval ownership | LIFE-GAP-036 | | Task/tool-call-scoped partial state | LIFE-GAP-037 with request-generation cleanup gaps 010 and 024 | @@ -322,7 +323,7 @@ Four workstreams can proceed concurrently after foundation decisions: ## Completeness statement -At the audited commit, this report covers every tracked definition and directly discoverable caller matching the lifecycle domains in scope, including tool argument assembly, validation, approval, child initialization, partial presentation, result/pending-action identity, todo rehydration, all seven umbrella checkers, their documented bounds/properties, primary focused suites, lifecycle E2E files, root package scripts, and CI workflow invocations. It does not claim semantic completeness for dynamic calls, generated code, dependencies, ignored files, deployment settings, or production histories. A future code change can invalidate completeness; `LIFE-GAP-016` and `LIFE-GAP-034` exist specifically to make this inventory mechanically maintainable. +At the audited commit, this report covers every tracked definition and directly discoverable caller matching the lifecycle domains in scope, including tool argument assembly, validation, approval, child initialization, partial presentation, result/pending-action identity, todo rehydration, all six baseline checkers and the separate optional fan-out checker, their documented bounds/properties, primary focused suites, lifecycle E2E files, root package scripts, and CI workflow invocations. It does not claim semantic completeness for dynamic calls, generated code, dependencies, ignored files, deployment settings, or production histories. A future code change can invalidate completeness; `LIFE-GAP-016` and `LIFE-GAP-034` exist specifically to make this inventory mechanically maintainable. ## Historical provenance diff --git a/docs/architecture/task-lifecycle-model.md b/docs/architecture/task-lifecycle-model.md index 2aaff60cf5..d27c13bb10 100644 --- a/docs/architecture/task-lifecycle-model.md +++ b/docs/architecture/task-lifecycle-model.md @@ -6,15 +6,16 @@ Zoo Code checks task lifecycle protocols through one umbrella command for indepe pnpm lifecycle:model-check ``` -The command runs seven independent bounded submodels in sequence: +The baseline command runs six independent bounded submodels in sequence: 1. the persisted task delegation lifecycle; 2. shared-store concurrency across task-history hosts; 3. production-backed handoff reducers with an abstract provider/scheduler protocol; -4. the task fan-out protocol; -5. the task cleanup protocol; -6. request-stream parser scoping; and -7. completion persistence. +4. the task cleanup protocol; +5. request-stream parser scoping; and +6. completion persistence. + +The planned two-sibling fan-out protocol is intentionally outside the baseline and CI umbrella. Run it explicitly with `pnpm fanout-protocol:model-check`; it describes optional future functionality, not current production coverage. This umbrella command is the single model-check entry point in the `compile` CI job after type checking. Command-level composition does not merge the submodels' state spaces: each checker retains its own bounds, transitions, invariant ownership, reachability requirements, and counterexample format. In particular, parser state is not part of the persisted lifecycle graph. The focused parser checker remains directly runnable with `pnpm parser-scope:model-check` for debugging. @@ -97,9 +98,11 @@ Issue #1623 exposed that missing refinement: `getEnvironmentDetails`, `presentAs ## Task fan-out protocol model -`scripts/check-task-fanout-protocol.ts` is a separate bounded protocol model for the #369/#372 fan-out safety contract. It explores a live parent with two sibling slots, a two-permit scheduler, independent result readiness, explicit child-to-parent delivery, parent loss, orphan cancellation, and permit release. It checks scheduler capacity and exact permit ownership, one writer and at-most-once delivery per child, readiness before delivery, and no result routing after parent loss. Named landmarks require concurrent siblings beneath a live parent, out-of-order result delivery, parent loss while work is running, and complete orphan cleanup to remain reachable. Injected unsafe states confirm those invariants reject wrong writers, early and duplicate delivery, post-parent-loss routing, and scheduler over-allocation. +`scripts/check-task-fanout-protocol.ts` is a separate, optional bounded protocol model for the #369/#372 fan-out safety contract. It explores a live parent with two sibling slots, a two-permit scheduler, independent result readiness, explicit child-to-parent delivery, parent loss, orphan cancellation, and permit release. It checks scheduler capacity and exact permit ownership, one writer and at-most-once delivery per child, readiness before delivery, and no result routing after parent loss. Named landmarks require concurrent siblings beneath a live parent, out-of-order result delivery, parent loss while work is running, and complete orphan cleanup to remain reachable. Injected unsafe states confirm those invariants reject wrong writers, early and duplicate delivery, post-parent-loss routing, and scheduler over-allocation. + +This model checks an intended abstract composition boundary without claiming that concurrent sibling fan-out is enabled in production. It imports no production fan-out transition and is excluded from `pnpm lifecycle:model-check` and baseline CI. `TaskScheduler` provides generic bounded permits, but `ClineProvider` constructs it at the default capacity of one and production delegation persists a singular `awaitingChildId`. Raising production concurrency still requires live-parent result integration and extension-host coverage before fan-out can ship. -This model checks the intended abstract composition boundary without claiming that concurrent sibling fan-out is enabled in production. `TaskScheduler` already provides bounded permits and guaranteed release, and completion APIs route by explicit parent and child IDs; focused tests cover those adapters. Raising production concurrency still requires live-parent result integration and extension-host coverage before fan-out can ship. Keeping this state space separate preserves the serial handoff checker's one-child ownership invariants. +The production-backed lifecycle checker already models the current serial ownership invariant: a parent has at most one `awaitingChildId`, `delegatedToId` matches it, and every active/delegated linked child is the child currently awaited. The reducer rejects re-delegation while that child is active. This is exhaustive only for the checker's three slots and depth 12 and does not erase the documented cross-host stale-write violations, so baseline closure still requires their production fixes and model promotion. ## Completion persistence model @@ -146,7 +149,7 @@ These are safety claims within the documented bounds. The checks do not claim li | Delegation lifecycle | Production-backed bounded universal | The explorer calls the four production reducers for three task slots through depth 12. | Excludes provider instances, persistence failures, scheduler state, most live `Task` behavior, and generation identity for delayed pre-interruption completion. Recovery-compatible active-parent completion is test-only. | | Shared-store concurrency | Production-backed bounded scenarios plus known-unsafe witnesses | The explorer imports production delta/merge functions and reducers; a real-filesystem test is a smoke check. | Does not prove crash safety, filesystem/lock semantics, arbitrary processes, or loss-free same-field merging. #1469 and #1021 remain unsafe. | | Provider handoff and scheduler | Mixed: production-backed reducers/selector plus abstract bounded protocol | Commits use production reducers; provider ownership, publication, transition locks, and permits are model abstractions through depth 15. | Selector correctness does not refine all downstream readers. Scheduler tests cover concrete permit behavior separately. | -| Fan-out | Planned-only abstract bounded protocol over partial shipped primitives | The model has two sibling slots and two abstract permits; production ships a scheduler and ID-routed serial completion primitives. | Production fan-out, live-parent result integration, concurrent sibling E2E, and orphan discovery are absent. | +| Optional fan-out scope | Planned-only abstract bounded protocol outside baseline CI | The model has two sibling slots and two abstract permits and imports no production fan-out transition. | Excluded from baseline closure and estimate; production fan-out remains separately scoped future functionality. | | Cleanup | Abstract bounded universal plus adapter tests | Abort, disposal, settlement, rejection, and provider shutdown are modeled as protocol/environment actions. | No direct execution of all production cleanup methods, filesystem/editor promises, timing liveness, fairness, or arbitrary task counts. | | Parser request scope | Production-backed bounded schedule replay | The checker executes production parser APIs across 924 order-preserving schedules for two scopes. | Assumes callers stop invoking a finalized scope; transport behavior, arbitrary request counts, indices, and malformed histories are outside the claim. | | Completion persistence | Abstract bounded universal plus production tests and one fresh-host E2E path | The model abstracts persistence as a durable phase with at most two write starts; production guards and retry paths are tested separately. | Production permits more retries; no power-loss/filesystem proof, fairness, arbitrary retry count, complete delegated fallback, provider status metadata, or downstream event-consumer model. | @@ -168,19 +171,19 @@ The exhaustive repository inventory, ranked stable burn-down register, source-ba - **Planned-only:** specifies behavior not enabled in production. - **Type/static convention:** centralized typing or guidance without repository-wide enforcement. -| Gap | Invariant | Current evidence | Production/model boundary and runtime impact | Missing work and objective closure criteria | -| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Cross-host completion ownership | A completion may mutate a parent only while that parent authoritatively awaits the same child. | **Known-unsafe witness** plus a production-backed reducer guard. | The reducer rejects stale authoritative input, but disk revalidation checks status legality rather than exact-child ownership. A stale host can orphan the newer child. | Revalidate exact ownership under the disk lock; add deterministic two-store regression coverage; replace the expected witness with a universal bounded invariant. | -| Monotonic detachment | Message persistence must never restore lineage after abandonment clears it. | **Known-unsafe witness** plus a production-backed detach reducer. | A live task rebuilds lineage from stale fields; a later delta can restore parent/root IDs while preserving the interrupted status. | Give lifecycle fields a disk-authoritative write owner or tombstone/generation; prove stale metadata saves cannot alter them; promote the witness to an invariant. | -| Task-local mode consumers | Every mode-sensitive child operation must use the child task's immutable mode, not shared provider/view state. | Write side: **production-backed bounded matrix**. Read side: **proxy/partial and known unsafe**. | The selector snapshot can be correct while environment rendering and tool validation consume shared mode, causing wrong prompts or permissions. | Fix confirmed consumers, inventory other mode readers, add divergent-mode production tests, and enforce a task-local reader boundary before claiming universal isolation. | -| Tool-originated child initialization | Every required child field originating in `new_task` must be task-scoped, durably owned, and equivalent after rehydration. | Argument forwarding has focused tests; durable initial-state refinement is **unmodeled**. | Normal creation uses explicit model-supplied child todos rather than copying the parent, but initial todos demonstrate that some child state is process-local and can disappear. | Inventory all child initialization fields; define durable ownership, deep-copy, precedence, publication, and rehydration contracts with focused and E2E checks (`LIFE-GAP-035`). | -| Tool approval ownership | An interactive edit or settlement may affect only the matching task, action, and tool call. | Single-approval behavior has focused tests; cross-task correlation is **unmodeled and known unsafe by inspection**. | `update_todo_list` uses process-global edit state without task/action identity, allowing delayed or concurrent approval contamination. | Correlate proposal/edit/approval/cancellation by task and action/tool-call ID; reject stale edits and test interleavings (`LIFE-GAP-036`). | -| Tool partial-state isolation | Partial presentation state must be owned by one task and tool call and cleared on every terminal path. | Tool-local tests only; cross-call/task interleavings are **unmodeled**. | Singleton handlers share `lastSeenPartialPath`, so another call can create false or missed path stabilization. | Key state by `(taskId, toolCallId)` or instantiate handlers per call; test interleavings and cleanup (`LIFE-GAP-037`). | -| Tool identity correspondence | Parsed call, durable history, approval, execution, result, pending action, and replay must have one collision-resistant identity. | Duplicate-ID helper tests are **proxy/partial**; end-to-end correspondence is **unmodeled**. | Non-injective sanitization can collapse distinct raw IDs in history while execution still treats them as separate calls. | Reject/disambiguate collisions and prove a one-to-one identity mapping across native/MCP calls and restart (`LIFE-GAP-038`). | -| Serial versus fan-out delegation | The shipped serial path permits one awaited/running child and resumes its parent only after child release; fan-out must not be implied by an abstract model. | Serial ownership is **production-backed bounded**; the added fan-out checker is **planned-only abstract bounded**. | Production suspends the parent and uses a one-permit scheduler by default. The fan-out model imports no live-parent production transition, so it cannot prove fan-out behavior. | Either fix scheduler capacity at one and make concurrent fan-out explicitly unreachable, or implement live-parent isolation, rollback, routing, orphan cleanup, and E2E before promoting fan-out coverage. | -| Shared lifecycle status vocabulary | All consumers should derive task status from one exported owner. | **Type/static convention**; the visible interrupted-status symptom is repaired. | Persistence derives its alias from `HistoryItem`, but CLI history adapters still copy the union, allowing future drift. | Export one shared status type, consume it at CLI/persistence boundaries, and rely on typechecking or a static rule to reject copied incompatible vocabulary. | -| Cross-model refinement | No independent checker result may be combined into a stronger end-to-end claim without an executable boundary mapping. | **Explicit limitation** only. | Persistence, provider publication, parser scope, cleanup, and completion durability run as separate state spaces; omitted consumers can violate a premise after another checker passes. | Add production-grounded boundary events and a bounded joint strategy for each genuinely cross-model invariant, or keep claims explicitly local. | -| Completion event consumers | Public completion must not be treated as universally durable beyond the modeled readiness contract. | **Abstract bounded** model plus focused tests and one fresh-host E2E path. | The model abstracts durability and parent reopen; provider status metadata and downstream `TaskCompleted` consumers are outside its state. | Inventory downstream consumers and add refinement checks only for guarantees they require; retain power-loss, filesystem, retry-count, and liveness exclusions. | +| Gap | Invariant | Current evidence | Production/model boundary and runtime impact | Missing work and objective closure criteria | +| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Cross-host completion ownership | A completion may mutate a parent only while that parent authoritatively awaits the same child. | **Known-unsafe witness** plus a production-backed reducer guard. | The reducer rejects stale authoritative input, but disk revalidation checks status legality rather than exact-child ownership. A stale host can orphan the newer child. | Revalidate exact ownership under the disk lock; add deterministic two-store regression coverage; replace the expected witness with a universal bounded invariant. | +| Monotonic detachment | Message persistence must never restore lineage after abandonment clears it. | **Known-unsafe witness** plus a production-backed detach reducer. | A live task rebuilds lineage from stale fields; a later delta can restore parent/root IDs while preserving the interrupted status. | Give lifecycle fields a disk-authoritative write owner or tombstone/generation; prove stale metadata saves cannot alter them; promote the witness to an invariant. | +| Task-local mode consumers | Every mode-sensitive child operation must use the child task's immutable mode, not shared provider/view state. | Write side: **production-backed bounded matrix**. Read side: **proxy/partial and known unsafe**. | The selector snapshot can be correct while environment rendering and tool validation consume shared mode, causing wrong prompts or permissions. | Fix confirmed consumers, inventory other mode readers, add divergent-mode production tests, and enforce a task-local reader boundary before claiming universal isolation. | +| Tool-originated child initialization | Every required child field originating in `new_task` must be task-scoped, durably owned, and equivalent after rehydration. | Argument forwarding has focused tests; durable initial-state refinement is **unmodeled**. | Normal creation uses explicit model-supplied child todos rather than copying the parent, but initial todos demonstrate that some child state is process-local and can disappear. | Inventory all child initialization fields; define durable ownership, deep-copy, precedence, publication, and rehydration contracts with focused and E2E checks (`LIFE-GAP-035`). | +| Tool approval ownership | An interactive edit or settlement may affect only the matching task, action, and tool call. | Single-approval behavior has focused tests; cross-task correlation is **unmodeled and known unsafe by inspection**. | `update_todo_list` uses process-global edit state without task/action identity, allowing delayed or concurrent approval contamination. | Correlate proposal/edit/approval/cancellation by task and action/tool-call ID; reject stale edits and test interleavings (`LIFE-GAP-036`). | +| Tool partial-state isolation | Partial presentation state must be owned by one task and tool call and cleared on every terminal path. | Tool-local tests only; cross-call/task interleavings are **unmodeled**. | Singleton handlers share `lastSeenPartialPath`, so another call can create false or missed path stabilization. | Key state by `(taskId, toolCallId)` or instantiate handlers per call; test interleavings and cleanup (`LIFE-GAP-037`). | +| Tool identity correspondence | Parsed call, durable history, approval, execution, result, pending action, and replay must have one collision-resistant identity. | Duplicate-ID helper tests are **proxy/partial**; end-to-end correspondence is **unmodeled**. | Non-injective sanitization can collapse distinct raw IDs in history while execution still treats them as separate calls. | Reject/disambiguate collisions and prove a one-to-one identity mapping across native/MCP calls and restart (`LIFE-GAP-038`). | +| Serial delegation baseline | Current production permits one awaited active child per parent and resumes the parent only after child release. | Singular ownership is **production-backed bounded**; scheduler/provider ordering is mixed production/abstract. | Reducers and the normal provider path enforce singular ownership, but stale cross-host persistence can still violate the relationship; scheduler capacity is defaulted, not statically fixed. | Close current serial persistence/ordering gaps and ratchet the provider's one-permit baseline. Treat fan-out as separate optional scope. | +| Shared lifecycle status vocabulary | All consumers should derive task status from one exported owner. | **Type/static convention**; the visible interrupted-status symptom is repaired. | Persistence derives its alias from `HistoryItem`, but CLI history adapters still copy the union, allowing future drift. | Export one shared status type, consume it at CLI/persistence boundaries, and rely on typechecking or a static rule to reject copied incompatible vocabulary. | +| Cross-model refinement | No independent checker result may be combined into a stronger end-to-end claim without an executable boundary mapping. | **Explicit limitation** only. | Persistence, provider publication, parser scope, cleanup, and completion durability run as separate state spaces; omitted consumers can violate a premise after another checker passes. | Add production-grounded boundary events and a bounded joint strategy for each genuinely cross-model invariant, or keep claims explicitly local. | +| Completion event consumers | Public completion must not be treated as universally durable beyond the modeled readiness contract. | **Abstract bounded** model plus focused tests and one fresh-host E2E path. | The model abstracts durability and parent reopen; provider status metadata and downstream `TaskCompleted` consumers are outside its state. | Inventory downstream consumers and add refinement checks only for guarantees they require; retain power-loss, filesystem, retry-count, and liveness exclusions. | ## Historical provenance @@ -203,7 +206,7 @@ When production lifecycle behavior changes: 4. Increase depth or task slots only when the new scenario requires it. Keep the state budget explicit and ensure CI completes quickly. 5. Convert any discovered counterexample into a focused production regression test as well as retaining the architectural invariant. -Completion-readiness changes belong in `scripts/check-completion-persistence.ts`; shared-store interleavings belong in `scripts/check-task-store-concurrency.ts`; fan-out permit, result-routing, and orphan-cleanup changes belong in `scripts/check-task-fanout-protocol.ts`. Do not weaken bounds or remove an invariant merely to make CI pass. If state growth becomes difficult to control, split independent protocols or move the model to TLC/Quint with an implementation trace adapter rather than silently sampling the state space. +Completion-readiness changes belong in `scripts/check-completion-persistence.ts`; shared-store interleavings belong in `scripts/check-task-store-concurrency.ts`. Optional fan-out work belongs in `scripts/check-task-fanout-protocol.ts` and its separately scoped ticket until production transitions exist. Do not weaken bounds or remove an invariant merely to make CI pass. If state growth becomes difficult to control, split independent protocols or move the model to TLC/Quint with an implementation trace adapter rather than silently sampling the state space. Parser request scoping is one such independent bounded submodel within the umbrella suite. Extend `scripts/check-native-tool-call-parser-scoping.ts` and its focused architecture document instead of adding parser state or transitions to `taskLifecycle.ts` or the persisted lifecycle state graph. diff --git a/package.json b/package.json index e534572620..96f2d9eeae 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "check-types": "turbo check-types --log-order grouped --output-logs new-only", "test": "turbo test --log-order grouped --output-logs new-only", "test:mutation-ci": "node --test scripts/stryker-diff.test.mjs", - "lifecycle:model-check": "tsx scripts/check-task-lifecycle.ts && tsx scripts/check-task-store-concurrency.ts && tsx scripts/check-provider-handoff-scheduler.ts && tsx scripts/check-task-fanout-protocol.ts && pnpm cleanup-protocol:model-check && pnpm parser-scope:model-check && tsx scripts/check-completion-persistence.ts", + "lifecycle:model-check": "tsx scripts/check-task-lifecycle.ts && tsx scripts/check-task-store-concurrency.ts && tsx scripts/check-provider-handoff-scheduler.ts && pnpm cleanup-protocol:model-check && pnpm parser-scope:model-check && tsx scripts/check-completion-persistence.ts", + "fanout-protocol:model-check": "tsx scripts/check-task-fanout-protocol.ts", "cleanup-protocol:model-check": "tsx scripts/check-task-cleanup-protocol.ts", "parser-scope:model-check": "node scripts/run-native-tool-call-parser-scoping.mjs", "mcp:integration-check": "tsx scripts/check-mcp-oauth-integration.ts", From de94fa58e0163a41464fe29ce40cc2795adebb42 Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 13 Sep 2026 17:02:52 +0000 Subject: [PATCH 15/17] docs(lifecycle): remove remediation time estimates --- .../architecture/task-lifecycle-gap-report.md | 44 +++++++++---------- docs/architecture/task-lifecycle-model.md | 2 +- scripts/check-task-fanout-protocol.ts | 4 +- 3 files changed, 26 insertions(+), 24 deletions(-) diff --git a/docs/architecture/task-lifecycle-gap-report.md b/docs/architecture/task-lifecycle-gap-report.md index 17048eb6c9..9bdefcebf9 100644 --- a/docs/architecture/task-lifecycle-gap-report.md +++ b/docs/architecture/task-lifecycle-gap-report.md @@ -209,18 +209,18 @@ Severity reflects plausible data loss, ownership corruption, permission/context ## Portfolio remediation plan -The 38 IDs are not 38 independent projects. They group into eight programs with shared root causes and implementation surfaces. Estimates are engineering effort, not calendar commitments; they include implementation, deterministic tests, proportional model/refinement work, documentation, and stabilization. - -| Cluster | Gap IDs | Root fix and likely ownership | Size / effort | Engineering risk | Objective portfolio evidence | -| ------------------------------------------ | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| P1. Persisted ownership and generation | 001, 002, 012, 017, 020 | Disk-authoritative lifecycle ownership/generation and immutable store reads across history types, lifecycle reducers, `TaskHistoryStore`, provider delegation, and reconciliation. | XL, 15–25 engineer-days | High: persisted compatibility and cross-host races | Two-host stale-write tests, promoted invariants, restart/reconciliation evidence, backward-compatible optional data. | -| P2. Durable operation and crash recovery | 004, 005, 006, 021, 023 | Operation intent/replay or explicit idempotent recovery for pair writes, delegation, completion messages, shutdown, and deletion. | XL, 20–35 days | Very high: failure ordering can create new corruption | Fault injection at every durable boundary, crash/restart convergence, no false success, recovery reachability. | -| P3. Schema, path, and lifecycle vocabulary | 013, 018, 019 | Schema-derived status ownership, validated ordinary history reads, and one safe task-ID boundary across types, persistence, metadata, CLI, and import paths. | M, 5–9 days | Medium: malformed legacy data and downgrade behavior | Migration/quarantine fixtures, traversal tests, type/static ratchets. | -| P4. Request, stream, and tool identity | 008, 010, 024, 025, 026, 030, 037, 038 | Request generation plus canonical call identity, then task/generation/call-scoped parser and partial-handler state. Owners include provider transforms, parser, `Task`, `BaseTool`, editing handlers, and tool-ID utilities. | XL, 18–30 days | High: provider compatibility and duplicate execution | Adversarial IDs/index-less streams, delayed/cancelled generation tests, cleanup/deadline checks, production-backed call-state model. | -| P5. Tool-owned task state and queueing | 003, 007, 031, 035, 036 | Task-local context, durable child initialization, correlated approval identity, and claim/persist/ack queueing across tools, `Task`, provider/webview, message queue, and history schema. | XL, 16–27 days | High: cross-task contamination and persistence precedence | Omitted/explicit child controls, switch/restart E2E, two-approval schedules, queue failure retention, mode-permission tests. | -| P6. Event and ingress contracts | 009, 011, 022, 027, 028, 029, 032, 033 | Classify barriers versus notifications; normalize lifecycle payloads and clear/resume semantics across Task, provider, public API, IPC, and webview. | L, 12–20 days | Medium-high: public compatibility and ordering | Exactly-once event tests, consumer inventory, cross-surface contract matrix, compatibility adapters where required. | -| P7. Serial scheduler baseline | 014 | Ratchet the current one-permit provider scheduler and singular active-child ownership; keep live-parent fan-out under separate future scope. | M, 3–6 days baseline | Low-medium: current behavior, but cross-host exceptions remain | Production capacity/order assertions plus lifecycle/store invariants proving the bounded serial contract. | -| P8. Verification and traceability platform | 015, 016, 034 | Machine-readable model metadata/traceability and selected cross-model trace validation across checker scripts, package commands, CI, and architecture docs. | L, 7–12 days | Medium: vacuity and CI cost | CI validates IDs, symbols, tests, bounds, actions, landmarks, workflows, and executable mappings for cross-model claims. | +The 38 IDs are not 38 independent projects. They group into eight programs with shared root causes and implementation surfaces. Complexity classes reflect implementation breadth, coupling, and verification risk rather than schedule or duration. + +| Cluster | Gap IDs | Root fix and likely ownership | Complexity | Engineering risk | Objective portfolio evidence | +| ------------------------------------------ | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| P1. Persisted ownership and generation | 001, 002, 012, 017, 020 | Disk-authoritative lifecycle ownership/generation and immutable store reads across history types, lifecycle reducers, `TaskHistoryStore`, provider delegation, and reconciliation. | XL | High: persisted compatibility and cross-host races | Two-host stale-write tests, promoted invariants, restart/reconciliation evidence, backward-compatible optional data. | +| P2. Durable operation and crash recovery | 004, 005, 006, 021, 023 | Operation intent/replay or explicit idempotent recovery for pair writes, delegation, completion messages, shutdown, and deletion. | XL | Very high: failure ordering can create new corruption | Fault injection at every durable boundary, crash/restart convergence, no false success, recovery reachability. | +| P3. Schema, path, and lifecycle vocabulary | 013, 018, 019 | Schema-derived status ownership, validated ordinary history reads, and one safe task-ID boundary across types, persistence, metadata, CLI, and import paths. | M | Medium: malformed legacy data and downgrade behavior | Migration/quarantine fixtures, traversal tests, type/static ratchets. | +| P4. Request, stream, and tool identity | 008, 010, 024, 025, 026, 030, 037, 038 | Request generation plus canonical call identity, then task/generation/call-scoped parser and partial-handler state. Owners include provider transforms, parser, `Task`, `BaseTool`, editing handlers, and tool-ID utilities. | XL | High: provider compatibility and duplicate execution | Adversarial IDs/index-less streams, delayed/cancelled generation tests, cleanup/deadline checks, production-backed call-state model. | +| P5. Tool-owned task state and queueing | 003, 007, 031, 035, 036 | Task-local context, durable child initialization, correlated approval identity, and claim/persist/ack queueing across tools, `Task`, provider/webview, message queue, and history schema. | XL | High: cross-task contamination and persistence precedence | Omitted/explicit child controls, switch/restart E2E, two-approval schedules, queue failure retention, mode-permission tests. | +| P6. Event and ingress contracts | 009, 011, 022, 027, 028, 029, 032, 033 | Classify barriers versus notifications; normalize lifecycle payloads and clear/resume semantics across Task, provider, public API, IPC, and webview. | L | Medium-high: public compatibility and ordering | Exactly-once event tests, consumer inventory, cross-surface contract matrix, compatibility adapters where required. | +| P7. Serial scheduler baseline | 014 | Ratchet the current one-permit provider scheduler and singular active-child ownership; keep live-parent fan-out under separate future scope. | M | Low-medium: current behavior, but cross-host exceptions remain | Production capacity/order assertions plus lifecycle/store invariants proving the bounded serial contract. | +| P8. Verification and traceability platform | 015, 016, 034 | Machine-readable model metadata/traceability and selected cross-model trace validation across checker scripts, package commands, CI, and architecture docs. | L | Medium: vacuity and CI cost | CI validates IDs, symbols, tests, bounds, actions, landmarks, workflows, and executable mappings for cross-model claims. | ### Root fixes that close multiple gaps @@ -250,15 +250,15 @@ Critical path: **P7 serial baseline → P1 ownership/generation → P2 recovery ### Quick wins versus architectural programs -| Category | Scope | Effort | Notes | -| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | --------------------------------------------------------------------------------- | -| Quick wins | 013 shared status type; 025 listener cleanup; 029 projection naming/contracts; 034 checker metadata. | 1–4 days each | Separate PRs; reduce drift but do not close cross-host integrity. | -| Medium projects | 019 path safety, 021 disposal drain, 022/033 ingress convergence, 024 parser cleanup, 026 hard deadline, 027/028 event ownership, serial 014. | 3–8 days each | Focused subsystem work with deterministic tests. | -| Architectural programs | P1, P2, P4 identity/generation, P5 durable task state, P6 public contracts, or full fan-out. | 12–35 days per program | Require staged PRs, failure injection, compatibility plans, and model refinement. | +| Category | Scope | Implementation shape | Notes | +| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | --------------------------------------------------------------------------------- | +| Quick wins | 013 shared status type; 025 listener cleanup; 029 projection naming/contracts; 034 checker metadata. | Narrow, independently reviewable changes | Reduce drift but do not close cross-host integrity. | +| Focused projects | 019 path safety, 021 disposal drain, 022/033 ingress convergence, 024 parser cleanup, 026 hard deadline, 027/028 event ownership, serial 014. | One or two related subsystem boundaries | Require deterministic tests and explicit compatibility checks. | +| Architectural programs | P1, P2, P4 identity/generation, P5 durable task state, P6 public contracts, or full fan-out. | Cross-cutting owner or protocol changes | Require staged PRs, failure injection, compatibility plans, and model refinement. | -### Overall scale and parallel workstreams +### Portfolio scale and parallel workstreams -Baseline closure for exhaustive modeling and verification of current serial production behavior is approximately **85–145 engineer-days**. Concurrent fan-out is excluded from that range. If separately authorized, its live-parent execution, routing, rollback, orphan cleanup, UI scoping, E2E, and model-refinement prerequisites add approximately **20–35 engineer-days** after baseline foundations; that increment should be tracked by #369/#372 or their successor ticket. These are effort confidence bands, not delivery dates. +Baseline closure is a multi-program architecture effort spanning P1 through P8, with current serial behavior as the production target. Concurrent fan-out is excluded and remains separately tracked by #369/#372 or their successor ticket. Its live-parent execution, routing, rollback, orphan cleanup, UI scoping, E2E, and model-refinement prerequisites depend on baseline ownership, identity, and recovery foundations. Four workstreams can proceed concurrently after foundation decisions: @@ -275,14 +275,14 @@ Four workstreams can proceed concurrently after foundation decisions: 4. Land independent hardening for 013, 025, 029, and 034. 5. Add P8 machine-readable mapping incrementally so closure PRs name symbols, witnesses, tests, bounds, and evidence class. -### Sizing assumptions and reconciliations +### Planning assumptions and reconciliations -- Effort includes focused/full tests and relevant E2E, not only code edits. +- Complexity classes include focused/full tests and relevant E2E, not only code edits. - Crash-consistency closure requires deterministic interruption and rollback fault injection; happy paths do not close P2. - Prefer lazy optional-field migrations. Existing lost data is unrecoverable; downgrade readers must ignore new fields safely. - High severity is reserved for demonstrated corruption, cross-task permission/state contamination, or execution/history divergence. Gap 035 remains Medium because its confirmed witness loses planning state; 036 and 038 remain High because they cross task/call ownership. - Gap 014 remains Medium because baseline seriality is partly production-backed but not statically ratcheted and cross-host ownership violations remain. Optional fan-out does not affect baseline severity. -- Re-estimate after P1 and P4 decisions because they define shared interfaces; estimate fan-out only in its separate scope. +- Reassess cluster boundaries after P1 and P4 decisions because they define shared interfaces; keep fan-out in its separate scope. ## Burn-down dependencies diff --git a/docs/architecture/task-lifecycle-model.md b/docs/architecture/task-lifecycle-model.md index d27c13bb10..994cafd133 100644 --- a/docs/architecture/task-lifecycle-model.md +++ b/docs/architecture/task-lifecycle-model.md @@ -149,7 +149,7 @@ These are safety claims within the documented bounds. The checks do not claim li | Delegation lifecycle | Production-backed bounded universal | The explorer calls the four production reducers for three task slots through depth 12. | Excludes provider instances, persistence failures, scheduler state, most live `Task` behavior, and generation identity for delayed pre-interruption completion. Recovery-compatible active-parent completion is test-only. | | Shared-store concurrency | Production-backed bounded scenarios plus known-unsafe witnesses | The explorer imports production delta/merge functions and reducers; a real-filesystem test is a smoke check. | Does not prove crash safety, filesystem/lock semantics, arbitrary processes, or loss-free same-field merging. #1469 and #1021 remain unsafe. | | Provider handoff and scheduler | Mixed: production-backed reducers/selector plus abstract bounded protocol | Commits use production reducers; provider ownership, publication, transition locks, and permits are model abstractions through depth 15. | Selector correctness does not refine all downstream readers. Scheduler tests cover concrete permit behavior separately. | -| Optional fan-out scope | Planned-only abstract bounded protocol outside baseline CI | The model has two sibling slots and two abstract permits and imports no production fan-out transition. | Excluded from baseline closure and estimate; production fan-out remains separately scoped future functionality. | +| Optional fan-out scope | Planned-only abstract bounded protocol outside baseline CI | The model has two sibling slots and two abstract permits and imports no production fan-out transition. | Excluded from baseline closure; production fan-out remains separately scoped future functionality. | | Cleanup | Abstract bounded universal plus adapter tests | Abort, disposal, settlement, rejection, and provider shutdown are modeled as protocol/environment actions. | No direct execution of all production cleanup methods, filesystem/editor promises, timing liveness, fairness, or arbitrary task counts. | | Parser request scope | Production-backed bounded schedule replay | The checker executes production parser APIs across 924 order-preserving schedules for two scopes. | Assumes callers stop invoking a finalized scope; transport behavior, arbitrary request counts, indices, and malformed histories are outside the claim. | | Completion persistence | Abstract bounded universal plus production tests and one fresh-host E2E path | The model abstracts persistence as a durable phase with at most two write starts; production guards and retry paths are tested separately. | Production permits more retries; no power-loss/filesystem proof, fairness, arbitrary retry count, complete delegated fallback, provider status metadata, or downstream event-consumer model. | diff --git a/scripts/check-task-fanout-protocol.ts b/scripts/check-task-fanout-protocol.ts index 3ae0b4ab96..f732f6536d 100644 --- a/scripts/check-task-fanout-protocol.ts +++ b/scripts/check-task-fanout-protocol.ts @@ -28,7 +28,9 @@ const LANDMARKS = { "parent-loss-with-running-child": (state: ModelState) => !state.parentLive && CHILDREN.some((child) => state.children[child] === "running"), "orphan-cleanup": (state: ModelState) => - !state.parentLive && CHILDREN.every((child) => !["running", "ready"].includes(state.children[child])), + !state.parentLive && + state.permitOwners.length === 0 && + CHILDREN.every((child) => !["running", "ready"].includes(state.children[child])), } satisfies Record boolean> const start = initialState() From 6b0b83932cd59aa2e7ed4176d17d33b8e6cde2e2 Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 13 Sep 2026 19:10:13 +0000 Subject: [PATCH 16/17] docs(lifecycle): split remediation into one-point blocks --- .../architecture/task-lifecycle-gap-report.md | 2 + .../task-lifecycle-remediation-blocks.md | 130 ++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 docs/architecture/task-lifecycle-remediation-blocks.md diff --git a/docs/architecture/task-lifecycle-gap-report.md b/docs/architecture/task-lifecycle-gap-report.md index 9bdefcebf9..b3523b525f 100644 --- a/docs/architecture/task-lifecycle-gap-report.md +++ b/docs/architecture/task-lifecycle-gap-report.md @@ -209,6 +209,8 @@ Severity reflects plausible data loss, ownership corruption, permission/context ## Portfolio remediation plan +The [1-SP remediation block register](./task-lifecycle-remediation-blocks.md) decomposes this portfolio into small modeling/documentation increments. It assigns every GAP exactly one primary block, preserves dependencies across workstreams, and keeps optional fan-out separate from baseline ownership. + The 38 IDs are not 38 independent projects. They group into eight programs with shared root causes and implementation surfaces. Complexity classes reflect implementation breadth, coupling, and verification risk rather than schedule or duration. | Cluster | Gap IDs | Root fix and likely ownership | Complexity | Engineering risk | Objective portfolio evidence | diff --git a/docs/architecture/task-lifecycle-remediation-blocks.md b/docs/architecture/task-lifecycle-remediation-blocks.md new file mode 100644 index 0000000000..3f5c2924b5 --- /dev/null +++ b/docs/architecture/task-lifecycle-remediation-blocks.md @@ -0,0 +1,130 @@ +# Task lifecycle remediation blocks + +## One story point in this report + +One story point (1 SP) is a small, independently reviewable **modeling or documentation increment**, not a time estimate. A 1-SP block owns one bounded behavior or property and must include: + +- an explicit production symbol or boundary mapping; +- one model/checker change when a faithful model boundary exists, otherwise an explicit reason no checker is appropriate; +- focused test or CI evidence references; +- objective acceptance criteria and declared exclusions. + +Completing one block does not close its `LIFE-GAP` unless the parent GAP closure criteria are also satisfied. Blocks may depend on shared primitives or earlier evidence, so story-point size does not imply scheduling independence. + +## Ownership rules + +- Every `LIFE-GAP-001` through `LIFE-GAP-038` has exactly one primary block below. +- A block owns exactly one GAP ID. Dependencies may reference other blocks but do not duplicate ownership. +- Block IDs are stable: `LIFE-BLK-P-`. +- Baseline blocks describe current serial production behavior. Optional fan-out is isolated under `FANOUT-BLK-*` and does not own a baseline `LIFE-GAP`. +- Each block is documentation/formal-model scope. Runtime work named in acceptance criteria belongs in a later implementation PR. + +## P1: Persisted ownership and generation + +| Block | GAP | 1-SP increment | Production/model/test mapping | Depends on | Acceptance | +| --------------- | --- | --------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------------- | -------------------------------------------------------------------------------------------------------- | +| LIFE-BLK-P1-001 | 001 | Encode authoritative awaited-child revalidation as a model boundary and retain the shortest stale-completion witness. | `TaskHistoryStore.atomicUpdatePair`, `ClineProvider.reopenParentFromDelegation`; shared-store checker; cross-instance tests. | None | Model names lock-time ownership check, witness, bounds, and production test required for promotion. | +| LIFE-BLK-P1-002 | 002 | Specify lifecycle-owned lineage fields versus metadata writes and the stale-save witness. | `Task.saveClineMessages`, `taskMetadata`, `mergeHistoryDelta`; shared-store checker. | P1-001 ownership vocabulary | Field ownership table and monotonic-detachment invariant are explicit; no claim of current safety. | +| LIFE-BLK-P1-012 | 012 | Add attempt-generation state and stale-versus-resumed completion scenarios to the specification. | `PendingTaskAction.actionId`, interruption/resume/completion reducers; lifecycle checker exclusion. | P1-001 | Two generations and acceptance/rejection landmarks are specified with a bounded future checker shape. | +| LIFE-BLK-P1-017 | 017 | Inventory mutable cache read consumers and define immutable read semantics. | `TaskHistoryStore.get/getAll`; store tests. | None | Every direct caller is classified; clone/freeze test criteria and compatibility exclusions are recorded. | +| LIFE-BLK-P1-020 | 020 | Define observable stale-cache and convergence histories. | watcher, `invalidate`, `reconcile`; shared-store landmarks and cross-instance tests. | P1-001 | Missed-watch and explicit-refresh histories have bounded properties and objective convergence evidence. | + +## P2: Durable operation and crash recovery + +| Block | GAP | 1-SP increment | Production/model/test mapping | Depends on | Acceptance | +| --------------- | --- | -------------------------------------------------------------------------- | ----------------------------------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------- | +| LIFE-BLK-P2-004 | 004 | Enumerate pair-write interruption points and legal recovered states. | `atomicUpdatePair`; pair-failure landmark/tests. | P1-001 | Every pre/post-write cut has one legal outcome and required fault-injection assertion. | +| LIFE-BLK-P2-005 | 005 | Map delegation create/persist/publish/start cuts and rollback obligations. | `delegateParentAndOpenChild`; provider handoff model/tests. | P1-001, P2-004 | Transition table covers every cut without claiming child/parent atomicity. | +| LIFE-BLK-P2-006 | 006 | Specify completion message/lifecycle commit phases and replay outcomes. | `reopenParentFromDelegation`; completion and shared-store models. | P1-012, P2-004 | Result visibility and lifecycle state are mapped for each injected failure point. | +| LIFE-BLK-P2-021 | 021 | Define store close/drain semantics and post-dispose write exclusion. | `TaskHistoryStore.dispose`, write lock; store tests. | P2-004 recovery vocabulary | A bounded close-state machine and deterministic pending-write test criteria are documented. | +| LIFE-BLK-P2-023 | 023 | Specify deletion unlink failure and reconciliation histories. | `delete/deleteMany`, task directory/checkpoint cleanup; deletion tests. | P2-004 | False-success and resurrection outcomes are explicit with tombstone/retry closure choices. | + +## P3: Schema, path, and vocabulary + +| Block | GAP | 1-SP increment | Production/model/test mapping | Depends on | Acceptance | +| --------------- | --- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | ---------- | ---------------------------------------------------------------------------------------- | +| LIFE-BLK-P3-013 | 013 | Publish the canonical persisted-status owner and copied-union inventory. | `historyItemSchema`, task metadata, Task, CLI/history reader; typecheck/tests. | None | Every copy is listed with replacement/static-ratchet criteria. | +| LIFE-BLK-P3-018 | 018 | Define normal-read validation and quarantine outcomes for malformed history. | `readTaskFile`, reconciliation, shared Zod schema; fixtures. | None | Missing/invalid/legacy records have distinct expected outcomes and test fixtures. | +| LIFE-BLK-P3-019 | 019 | Inventory every task-ID-to-path entry and one shared safe-ID contract. | store paths, imports, deletion, checkpoints; traversal tests. | P3-018 | All path constructors are mapped and separator/traversal acceptance tests are specified. | + +## P4: Request, stream, and tool identity + +| Block | GAP | 1-SP increment | Production/model/test mapping | Depends on | Acceptance | +| --------------- | --- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | -------------------------- | ------------------------------------------------------------------------------------------- | +| LIFE-BLK-P4-008 | 008 | Add transform-to-parser cases for argument-only deltas and absent indices. | Responses transform, parser APIs/tests. | None | Two-call bounded schedules and expected isolated reconstruction are specified. | +| LIFE-BLK-P4-010 | 010 | Define request-generation ownership for detached usage writes. | Task request/drain paths; delayed-stream tests. | P4-038 identity vocabulary | Old/new generation mutations and allowed accounting-only updates are explicit. | +| LIFE-BLK-P4-024 | 024 | Map parser cleanup on success, abort, provider error, and replacement. | parser scope plus Task request terminal paths. | P4-010 | Every terminal path owns cleanup; late-event exclusions are stated. | +| LIFE-BLK-P4-025 | 025 | Specify listener lifetime for one chunk race and long streams. | `nextChunkWithAbort`; listener-count tests. | None | Both race outcomes remove listeners and a bounded stream cannot accumulate them. | +| LIFE-BLK-P4-026 | 026 | Model a true wall-clock deadline around pending iterator reads. | detached usage drain; fake-timer tests. | P4-010 | Permanently pending `next()` has a terminal deadline transition and no stale mutations. | +| LIFE-BLK-P4-030 | 030 | Define duplicate-start/run-promise identity. | `Task.start/run`, scheduler callback; Task tests. | P4-010 | Repeated starts share the actual settlement and cannot bypass scheduler ownership. | +| LIFE-BLK-P4-037 | 037 | Specify call-scoped partial path state and two-call interleavings. | `BaseTool.lastSeenPartialPath`, editing tool singletons; focused tests. | P4-038, P4-010 | Equal/different path interleavings and sibling-safe cleanup are bounded and reachable. | +| LIFE-BLK-P4-038 | 038 | Define canonical raw-to-durable call identity and collision witnesses. | tool-ID utility, parser, Task history, results, pending actions; duplicate-ID tests. | None | Adversarial IDs preserve or explicitly reject one-to-one call/result/replay correspondence. | + +## P5: Tool-owned task state and queueing + +| Block | GAP | 1-SP increment | Production/model/test mapping | Depends on | Acceptance | +| --------------- | --- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | ---------- | -------------------------------------------------------------------------------------------- | +| LIFE-BLK-P5-003 | 003 | Map every queue consumer to claim/persist/ack or dequeue-before-submit. | `MessageQueueService`, Task queue paths; failure tests. | None | Every consumer is classified and message-retention failure evidence is specified. | +| LIFE-BLK-P5-007 | 007 | Inventory mode-sensitive readers and authoritative task/provider source. | handoff selector, environment/tool consumers; divergent tests. | P4-010 | Each reader is classified; model premise is not described as reader refinement. | +| LIFE-BLK-P5-031 | 031 | Define intentional versus accidental queue loss across task disposal/restart. | queue service disposal and task lifecycle; E2E boundary. | P5-003 | Product contract, excluded durability, and restart witness are explicit. | +| LIFE-BLK-P5-035 | 035 | Specify durable child initialization precedence using initial todos as witness. | `NewTaskTool`, Task constructor, history/messages, rehydration, UI state. | P2-005 | Omitted, explicit-empty, initial, updated, switched, and restarted cases are mapped. | +| LIFE-BLK-P5-036 | 036 | Model two approval identities and stale/cross-task todo edits. | `approvedTodoList`, webview handler, approval callbacks/tests. | P4-038 | Two-task schedules require task/action/call correlation; current unsafe witness is explicit. | + +## P6: Event and ingress contracts + +| Block | GAP | 1-SP increment | Production/model/test mapping | Depends on | Acceptance | +| --------------- | --- | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | -------------- | -------------------------------------------------------------------------------------- | +| LIFE-BLK-P6-009 | 009 | Classify lifecycle events as awaited barriers or notifications. | Task/provider/public emitters and listeners; event tests. | P1-012 | Every consequential listener has settlement and rejection semantics. | +| LIFE-BLK-P6-011 | 011 | Inventory `TaskCompleted` consumers and required durable observations. | completion tool, provider status, public API/IPC/telemetry; completion model/tests. | P6-009, P2-006 | Each consumer’s ordering requirement maps to focused evidence or exclusion. | +| LIFE-BLK-P6-022 | 022 | Compare public and webview clear histories. | API eviction versus webview removal; provider tests. | P1-001 | Identical inputs produce an explicit same-or-deliberately-different persisted outcome. | +| LIFE-BLK-P6-027 | 027 | Establish one owner for delegation event emission. | task-level untyped and provider-level listeners; API tests. | P6-009 | Exactly-one source and no duplicate/dead listener are objective acceptance criteria. | +| LIFE-BLK-P6-028 | 028 | Normalize `TaskSpawned` payload semantics in the contract map. | task/provider/public event types and adapters. | P6-027 | Parent/child fields are explicit at each boundary with compatibility requirements. | +| LIFE-BLK-P6-029 | 029 | Document exact predicates behind `taskStatus` and `getRunning`. | Task ask markers, registry abort flags; caller inventory. | None | No caller may infer scheduler admission or persisted status without separate evidence. | +| LIFE-BLK-P6-032 | 032 | Decide supported reachability for webview abandonment. | protocol, handler, UI sender search; host tests. | P6-022 | Add sender evidence or deprecation criteria; no unreachable feature claim remains. | +| LIFE-BLK-P6-033 | 033 | Build a resume-ingress contract matrix. | webview/API/IPC resume adapters; provider/E2E tests. | P1-012, P6-009 | Awaiting, errors, publication, and rehydration results are explicit for each ingress. | + +## P7: Serial baseline and optional fan-out + +| Block | GAP | 1-SP increment | Production/model/test mapping | Depends on | Acceptance | +| --------------- | --- | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | ---------- | ----------------------------------------------------------------------------------------- | +| LIFE-BLK-P7-014 | 014 | Ratchet the current singular-child/one-permit baseline and its cross-host exclusions. | lifecycle reducers/checker, provider scheduler, shared-store witnesses. | P1-001 | Baseline property, bounds, scheduler assumption, and stale-write exceptions are explicit. | + +Optional future fan-out blocks do not own `LIFE-GAP-014` and do not participate in baseline closure: + +| Optional block | Increment | Prerequisites | Acceptance | +| -------------- | ------------------------------------------------------------ | ----------------------------- | ---------------------------------------------------------------------------------- | +| FANOUT-BLK-001 | Map live-parent and two-sibling production boundaries. | LIFE-BLK-P7-014, P1 ownership | No production claim; all missing adapters are named. | +| FANOUT-BLK-002 | Specify reservation, rollback, and permit-release failures. | FANOUT-BLK-001, P2 recovery | Every acquisition/create failure has a legal terminal state. | +| FANOUT-BLK-003 | Specify result writer, explicit routing, and orphan cleanup. | FANOUT-BLK-001, P4 identity | Existing abstract model landmarks map to required production APIs/tests. | +| FANOUT-BLK-004 | Define extension/webview task-scoping E2E matrix. | FANOUT-BLK-001–003 | Focus, messages, profiles, results, cancellation, and orphan behavior are covered. | + +## P8: Verification and traceability platform + +| Block | GAP | 1-SP increment | Production/model/test mapping | Depends on | Acceptance | +| --------------- | --- | ------------------------------------------------------------------- | ----------------------------------------------- | ----------------------- | ----------------------------------------------------------------------------- | +| LIFE-BLK-P8-015 | 015 | Select one cross-model claim and define executable boundary events. | Two relevant checkers plus production adapters. | Owning workstream block | Joint strategy is bounded or the claim remains explicitly local. | +| LIFE-BLK-P8-016 | 016 | Add a machine-readable GAP-to-symbol/test/checker manifest design. | report, scripts, package, workflows. | None | CI validation rules detect missing paths, duplicate ownership, and stale IDs. | +| LIFE-BLK-P8-034 | 034 | Define emitted checker metadata for bounds/actions/landmarks. | all checker scripts and docs. | P8-016 | One schema represents model metadata and docs consume or validate it. | + +## Mechanical coverage check + +The primary tables above map the closed integer range `001..038` exactly once. Reviewers should verify this mechanically before changing the register: + +```sh +rg -o 'LIFE-BLK-P[0-9]-[0-9]{3}' docs/architecture/task-lifecycle-remediation-blocks.md \ + | sort \ + | uniq -d +``` + +The command must print nothing. Also compare the final three digits of every primary block with the GAP register; optional `FANOUT-BLK-*` rows are excluded. + +## Block completion template + +- [ ] Stable block and parent GAP IDs are in the PR description. +- [ ] One bounded behavior/property and its exclusions are stated. +- [ ] Production symbols and ownership boundary are linked. +- [ ] Model/checker change is included, or non-applicability is justified. +- [ ] Focused test, E2E, and CI evidence requirements are explicit. +- [ ] Actions/landmarks remain reachable; bounds cannot truncate silently. +- [ ] Completion does not overstate parent GAP closure. +- [ ] Dependencies are satisfied or carried as explicit blockers. From 3d79df48df05e8516d4fe63d0035381d449e4657 Mon Sep 17 00:00:00 2001 From: Roomote Date: Sun, 13 Sep 2026 21:22:44 +0000 Subject: [PATCH 17/17] docs(lifecycle): fix remediation block ordering --- docs/architecture/task-lifecycle-gap-report.md | 8 ++++---- docs/architecture/task-lifecycle-remediation-blocks.md | 10 ++++++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/architecture/task-lifecycle-gap-report.md b/docs/architecture/task-lifecycle-gap-report.md index b3523b525f..77a71467ec 100644 --- a/docs/architecture/task-lifecycle-gap-report.md +++ b/docs/architecture/task-lifecycle-gap-report.md @@ -242,13 +242,13 @@ The 38 IDs are not 38 independent projects. They group into eight programs with ### Sequencing and critical path -1. **Foundation:** establish the current serial scheduler baseline (P7), define lifecycle ownership/generation (P1), and define canonical request/tool identity (P4). +1. **Foundation:** define lifecycle ownership/generation (P1) and canonical request/tool identity (P4), then ratchet the current serial scheduler baseline (P7) against the P1 ownership vocabulary. 2. **Integrity:** build durable operation recovery (P2) on P1. Run P3 in parallel once legacy-data policy is settled. 3. **Task isolation:** implement P5 using P4 identity and P1/P2 persistence rules. 4. **Surface convergence:** implement P6 after barrier/notification and generation semantics are known. 5. **Mechanical assurance:** start P8 metadata early; add cross-model refinement as production owners land. -Critical path: **P7 serial baseline → P1 ownership/generation → P2 recovery → P5 durable task state → P6 public contracts**. P3 and P8 metadata can run in parallel from the first tranche. P4 can run beside P1 after agreeing how task and request generations relate. +Critical path: **P1 ownership/generation → P7 serial baseline → P2 recovery → P5 durable task state → P6 public contracts**. P3 and P8 metadata can run in parallel from the first tranche. P4 can run beside P1 after agreeing how task and request generations relate. ### Quick wins versus architectural programs @@ -271,8 +271,8 @@ Four workstreams can proceed concurrently after foundation decisions: ### Recommended first tranche -1. Ratchet current serial behavior for 014 and leave fan-out to its separately scoped ticket. -2. Add deterministic failing tests for 001/002/012, then implement their shared ownership/generation primitive. +1. Add deterministic failing tests for 001/002/012, then define their shared ownership/generation primitive. +2. Ratchet current serial behavior for 014 against that ownership contract and leave fan-out to its separately scoped ticket. 3. Define canonical call identity and adversarial tests for 038/008; reuse it for 037 and 036. 4. Land independent hardening for 013, 025, 029, and 034. 5. Add P8 machine-readable mapping incrementally so closure PRs name symbols, witnesses, tests, bounds, and evidence class. diff --git a/docs/architecture/task-lifecycle-remediation-blocks.md b/docs/architecture/task-lifecycle-remediation-blocks.md index 3f5c2924b5..ca89541ec5 100644 --- a/docs/architecture/task-lifecycle-remediation-blocks.md +++ b/docs/architecture/task-lifecycle-remediation-blocks.md @@ -111,12 +111,18 @@ Optional future fan-out blocks do not own `LIFE-GAP-014` and do not participate The primary tables above map the closed integer range `001..038` exactly once. Reviewers should verify this mechanically before changing the register: ```sh -rg -o 'LIFE-BLK-P[0-9]-[0-9]{3}' docs/architecture/task-lifecycle-remediation-blocks.md \ +rg -o '^\| LIFE-BLK-P[0-9]-[0-9]{3} \|' docs/architecture/task-lifecycle-remediation-blocks.md \ | sort \ | uniq -d ``` -The command must print nothing. Also compare the final three digits of every primary block with the GAP register; optional `FANOUT-BLK-*` rows are excluded. +The command must print nothing. It matches only primary table rows, so dependency references and optional `FANOUT-BLK-*` rows are excluded. + +Separately compare block suffixes with the GAP column to detect omissions or mismatches: + +```sh +node -e 'const fs=require("fs");const s=fs.readFileSync("docs/architecture/task-lifecycle-remediation-blocks.md","utf8");const rows=[...s.matchAll(/^\| LIFE-BLK-P\d-(\d{3}) \| (\d{3}) \|/gm)];const gaps=rows.map(r=>r[2]);const want=Array.from({length:38},(_,i)=>String(i+1).padStart(3,"0"));if(rows.length!==38||rows.some(r=>r[1]!==r[2])||want.some(id=>!gaps.includes(id)))process.exit(1)' +``` ## Block completion template