diff --git a/docs/architecture/task-lifecycle-gap-report.md b/docs/architecture/task-lifecycle-gap-report.md index 66db3eb608..e8e5217643 100644 --- a/docs/architecture/task-lifecycle-gap-report.md +++ b/docs/architecture/task-lifecycle-gap-report.md @@ -168,46 +168,46 @@ 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 | Medium | High | Three confirmed task-local mode readers are fixed and production-tested, but the repository-wide reader inventory and enforceable task-local boundary remain incomplete. | Merged focused tests cover environment details, built-in validation, and custom-tool execution; the pure checker proves only divergence and selector storage. | Reader inventory and task-local API boundary. | Every mode-sensitive reader is classified; required task-local consumers have divergent production tests in both permission directions; checker/docs distinguish executed readers from proxy evidence. | -| 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. | +| 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; specified as `LIFE-BLK-P1-001` in the [persisted ownership model](./task-lifecycle-persisted-ownership-model.md). | 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; field ownership and monotonic-detachment invariant specified as `LIFE-BLK-P1-002` in the [persisted ownership model](./task-lifecycle-persisted-ownership-model.md). | 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 | Medium | High | Three confirmed task-local mode readers are fixed and production-tested, but the repository-wide reader inventory and enforceable task-local boundary remain incomplete. | Merged focused tests cover environment details, built-in validation, and custom-tool execution; the pure checker proves only divergence and selector storage. | Reader inventory and task-local API boundary. | Every mode-sensitive reader is classified; required task-local consumers have divergent production tests in both permission directions; checker/docs distinguish executed readers from proxy evidence. | +| 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; two-generation contract and future checker shape specified as `LIFE-BLK-P1-012` in the [persisted ownership model](./task-lifecycle-persisted-ownership-model.md). | 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; direct-caller inventory and immutable-read decision recorded as `LIFE-BLK-P1-017` in the [persisted ownership model](./task-lifecycle-persisted-ownership-model.md). | 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; named stale-cache/convergence histories specified as `LIFE-BLK-P1-020` in the [persisted ownership model](./task-lifecycle-persisted-ownership-model.md). | 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 diff --git a/docs/architecture/task-lifecycle-model.md b/docs/architecture/task-lifecycle-model.md index 355b08aa7e..8a75116bcd 100644 --- a/docs/architecture/task-lifecycle-model.md +++ b/docs/architecture/task-lifecycle-model.md @@ -75,6 +75,8 @@ Two desired properties are currently false and remain issue-keyed shortest-witne - [#1469](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1469): an old completion can commit after a newer handoff and clear it because disk revalidation checks status legality, not exact-child ownership. - [#1021](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1021): after abandonment and cache refresh, a stale live-task save can preserve the new interrupted status while restoring old lineage fields. +The lock-time awaited-child revalidation boundary required to promote the #1469 witness to a universal invariant, the production two-store test required for that promotion, and the lineage field-ownership contract and monotonic-detachment invariant behind #1021 are specified in the [persisted ownership and generation model](./task-lifecycle-persisted-ownership-model.md) (`LIFE-BLK-P1-001`, `LIFE-BLK-P1-002`). + CI fails if either exact causal witness or violation class changes, a witness disappears without being promoted to a universal invariant, a named semantic landmark or modeled phase becomes unreachable, a new safety violation appears, or exploration truncates. Raw reachable-state totals are printed as diagnostics, not used as ratchets: harmless representation changes can alter them without weakening protocol coverage. The known-unsafe witnesses currently compare exact shortest action sequences. This is intentionally simple and reviewable, but brittle to harmless action renames or serialization refactors. A causal partial-order comparator would reduce that brittleness but would add a second trace-equivalence protocol to maintain. Until that complexity is justified, update an exact witness only after confirming the terminal violation class and required causal ordering are unchanged. @@ -143,7 +145,7 @@ The completion persistence checker additionally enforces: 4. Delegated completion crosses the same durability boundary as standalone completion and requires successful parent reopen. 5. A failed delegated parent reopen cannot emit the delegated completion event. -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. +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. The two-generation contract, acceptance/rejection landmarks, and bounded future checker shape are specified in the [persisted ownership and generation model](./task-lifecycle-persisted-ownership-model.md) (`LIFE-BLK-P1-012`). ## Coverage audit diff --git a/docs/architecture/task-lifecycle-persisted-ownership-model.md b/docs/architecture/task-lifecycle-persisted-ownership-model.md new file mode 100644 index 0000000000..c678e0afb0 --- /dev/null +++ b/docs/architecture/task-lifecycle-persisted-ownership-model.md @@ -0,0 +1,141 @@ +# Task lifecycle persisted ownership and generation model + +This document is the specification deliverable for the P1 workstream, "Persisted ownership and generation" (tracked by [#1689](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1689) under umbrella [#1688](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1688)). It discharges the documentation/formal-model scope of five remediation blocks from the [remediation block register](./task-lifecycle-remediation-blocks.md): + +- `LIFE-BLK-P1-001` for `LIFE-GAP-001` (stale cross-host completion ownership); +- `LIFE-BLK-P1-002` for `LIFE-GAP-002` (stale message save restoring detached lineage); +- `LIFE-BLK-P1-012` for `LIFE-GAP-012` (attempt/generation identity for completion); +- `LIFE-BLK-P1-017` for `LIFE-GAP-017` (mutable store cache reads); +- `LIFE-BLK-P1-020` for `LIFE-GAP-020` (stale-cache and convergence histories). + +Like the parent [gap report](./task-lifecycle-gap-report.md), this is a documentation and formal-model artifact. It does not change production behavior, and completing these blocks does not close any parent `LIFE-GAP`: every closure criterion below names production evidence that belongs to a later implementation PR. Where a closure criterion names interruption, stale-write, or recovery evidence, the required tests are deterministic fault-injection tests — interruption at each persistence step with rollback verification — never timing-based sleeps or smoke tests. + +## Shared ownership vocabulary + +All five blocks use one vocabulary, established here so that dependent workstreams (P2 recovery, P7 serial baseline) can reference it without re-deriving it. + +- **Authoritative record.** Each task's `history_item.json` file on disk is the authoritative record. Every extension host's `TaskHistoryStore` cache is a replica that may lag disk (stale) and may contain fields no peer has committed. +- **Stale snapshot.** A `HistoryItem` captured from a host cache, or rebuilt from a live `Task`, before a peer host's latest commit. Completion (`ClineProvider.reopenParentFromDelegation`) and message saves (`Task.saveClineMessages`) build writes from stale snapshots by construction: the snapshot is taken outside the per-file disk lock. +- **Lock-time revalidation.** The check executed inside `safeWriteJson`'s merge callback while holding the per-task advisory file lock, implemented by `mergeHistoryDelta` (`src/core/task-persistence/taskStoreConcurrency.ts`). Today it enforces only status-transition legality (`VALID_TASK_STATUS_TRANSITIONS`) plus `childIds` union semantics. It does not check exact-child ownership. +- **Lifecycle-owned fields.** The fields whose authoritative writer is the lifecycle layer: `status`, `rootTaskId`, `parentTaskId`, `delegatedToId`, `childIds`, `awaitingChildId`, `completedByChildId`, `completionResultSummary`, and `pendingAction`. The block-level ownership table is under LIFE-BLK-P1-002 below. +- **Attempt generation.** A monotonically increasing per-task token distinguishing one execution attempt of the same task ID from the next after interruption and resume. No such persisted token exists today; `PendingTaskAction.actionId` is pending-approval identity, not attempt identity (see LIFE-BLK-P1-012). + +## LIFE-BLK-P1-001 — authoritative awaited-child revalidation (GAP-001) + +**Bounded property.** In every reachable two-host history, a child completion may mutate the parent record only while the authoritative (on-disk) parent record awaits that same child. Exclusions: crash atomicity of the pair write itself (`LIFE-BLK-P2-004`), more than two hosts, advisory-lock implementation semantics, and power-loss durability. + +**Production symbols and ownership boundary.** `ClineProvider.reopenParentFromDelegation` captures the parent and child from the host cache outside the disk lock, then calls `TaskHistoryStore.atomicUpdatePair`. Inside `atomicUpdatePair`, each `writeTaskFile` call runs `mergeHistoryDelta` under that file's advisory lock. The stale-completion window is the gap between the unlocked snapshot and the lock-time merge. + +**Model boundary.** The `revalidate` phase of the shared-store checker (`scripts/check-task-store-concurrency.ts`) is the faithful model of this boundary: it executes the production `mergeHistoryDelta` against current disk state while holding the modeled per-file lock. The checker's `stale completion ownership` scenario drives host A's completion of `child-a` against host B's re-delegation to `child-b`. + +- **Lock-time ownership check (named, required).** When a delta writes lifecycle fields that assume the parent awaits child `c` — the completion pair produced by `completeDelegatedChild` — lock-time revalidation must verify `disk.parent.awaitingChildId === c` (with the recovery-compatible `active` parent carve-out already recognized by the production reducer) and reject the delta otherwise. Today's check verifies only status-transition legality, which is exactly why the witness below exists. This check is the disk-authoritative ownership guard named by `LIFE-GAP-001`'s dependency column. +- **Witness (retained).** The checker's `#1469` known-unsafe witness — the exact 14-action shortest trace ending in `complete-a.commit(parent)` after `redelegate-b.commit(parent)` — remains the ratchet. CI fails if the witness's causal order changes or disappears without promotion. +- **Bounds.** Two hosts, three task slots, depth ≤ 32, ≤ 100,000 states, six scenarios; all seven phases and three semantic landmarks must remain reachable. These bounds are unchanged by this block. +- **Production test required for promotion.** A deterministic two-store test (the `TaskHistoryStore.crossInstance.spec.ts` pattern, two real stores on one storage path): store A captures a stale completion snapshot for `child-a`, store B completes the interrupt-and-redelegate sequence to `child-b`, then store A's `atomicUpdatePair` completion attempt must be rejected at lock-time revalidation and must leave B's committed handoff intact (parent still `delegated` to `child-b`, `child-b` active and linked). Only after that test exists and passes may the checker witness be promoted to a universal invariant. + +**Model/checker change.** None in this PR, by design: encoding the ownership check as a model transition now would specify behavior production does not implement, violating the suite's faithfulness rule. The witness stays a witness until the production guard lands; this block names the boundary, check, bounds, and promotion test so the later implementation PR has an objective target. + +## LIFE-BLK-P1-002 — lifecycle-owned fields versus metadata writes (GAP-002) + +**Bounded property (monotonic detachment).** Once a detach commit lands — `abandonDelegatedChild` clears the child's `parentTaskId`/`rootTaskId` — no later write whose lineage fields derive from a pre-detach snapshot may reintroduce them. This property is **currently violated in production**; this block makes the invariant and its ownership contract explicit and claims no present safety. + +**Field ownership table.** Authoritative-writer assignment for every `historyItemSchema` field (`packages/types/src/history.ts`): + +| Field | Owner | Metadata writers may write? | +| ------------------------------------------------------------------------- | --------------------------------------------------------------- | ---------------------------------------------------------------- | +| `status` | Lifecycle reducers via `atomicReadAndUpdate`/`atomicUpdatePair` | Preserve current value only (`Task.saveClineMessages` does this) | +| `rootTaskId`, `parentTaskId` | Lifecycle reducers (`delegateTaskToChild` chain, abandonment) | **No** — today `taskMetadata` writes them from live `Task` state | +| `delegatedToId`, `awaitingChildId` | Lifecycle reducers only | No | +| `childIds` | Lifecycle reducers; merge unions concurrent additions | No | +| `completedByChildId`, `completionResultSummary` | `completeDelegatedChild` (and reconciliation repair) | No | +| `pendingAction` | Delegation/completion paths (create/finish subtask actions) | No | +| `id`, `number`, `ts`, `task` | Creation-time identity | Creation only | +| `tokensIn`, `tokensOut`, `cacheWrites`, `cacheReads`, `totalCost`, `size` | Metadata (`taskMetadata` accounting) | Yes | +| `workspace`, `mode`, `apiConfigName` | Metadata (`taskMetadata`, mode/profile updates) | Yes | + +Administrative repair paths (`reconcileDelegationStateCore`, `repairActiveDelegation`, migration) write lifecycle fields with `skipTransitionCheck: true`; they are lifecycle-owned writes, not metadata writes. + +**Current deviation and witness.** `Task.saveClineMessages` rebuilds `rootTaskId`/`parentTaskId` from the live `Task` via `taskMetadata`, preserving only `status` from the store (`Task.ts`: the `existingStatus` spread). `computeHistoryDelta` therefore includes lineage fields whenever the live snapshot diverges from cache, and `mergeHistoryDelta` applies them last-writer-wins. The shared-store checker's `#1021` known-unsafe witness (`stale save detachment`, the exact 11-action shortest trace through `A.refresh` into `stale-save-a.commit(child-a)`) shows a stale save re-attaching abandoned lineage while preserving the newer `interrupted` status. The witness stays ratcheted until the closure criteria below are met. + +**Model/checker change.** None in this PR: the field ownership table and monotonic-detachment invariant are the deliverable, and the checker already retains the exact witness. The later implementation PR must either strip lifecycle fields from metadata deltas at the `taskMetadata`/`updateTaskHistory` boundary or make lock-time revalidation reject stale lineage deltas (tombstone or generation), then promote the witness to a universal invariant. Depends on the LIFE-BLK-P1-001 vocabulary above — satisfied within this document. + +**Test evidence required for closure (deterministic fault injection).** Interrupt a save at each persistence step of `Task.saveClineMessages` (message write, metadata build, `updateTaskHistory` upsert) with an interleaved abandonment commit, and assert that after every interruption point the detached lineage is never restored and any partial write rolls back or is superseded by the authoritative record. + +## LIFE-BLK-P1-012 — attempt-generation state (GAP-012) + +**Bounded property.** A completion produced by attempt `g` of a child may be accepted only while the persisted attempt generation of that child is `g`. A delayed pre-interruption completion (generation `g0`) arriving after resume (generation `g1`) must be rejected; the post-resume completion (`g1`) must be accepted. Exclusions: wall-clock timing, pending-action replay identity (`LIFE-BLK-P4-038`), and cross-host visibility of the generation, which is owned by the LIFE-BLK-P1-001 lock-time check once the field exists. + +**Production symbols and boundary.** `PendingTaskAction.actionId` (`packages/types/src/history.ts`) correlates one pending create/finish approval and is cleared on settlement; it is not an attempt identity and does not survive into the resumed attempt. The interruption, resume, and completion paths are the `interruptDelegatedChild` reducer, webview/API/IPC resume into `resumeTask`/rehydration, and `completeDelegatedChild` via `reopenParentFromDelegation`. + +**Specified generation semantics.** One future optional persisted field, `attemptGeneration`, on the child record (lazy optional-field migration per the gap report's planning assumptions; downgrade readers ignore it): + +- Delegation (first attempt) establishes generation `g0`. +- Interruption preserves the interrupted attempt's generation — the interrupted record still names `g0`, so a late `g0` completion remains attributable. +- Resume of an interrupted child increments to `g1` before the new attempt can produce completion. +- Completion carries the producing attempt's generation; reducers and lock-time revalidation accept it only when it equals the persisted generation. + +**Acceptance/rejection landmarks.** Two named landmarks must be reachable in the future checker: `stale-generation-completion-rejected` (a `g0` completion offered after resume created `g1` is rejected at both the reducer and the lock-time boundary) and `resumed-generation-completion-accepted` (a `g1` completion moves child → `completed`, parent → `active`). + +**Bounded future checker shape.** Extend `scripts/check-task-lifecycle.ts` — not the shared-store checker — with a per-child generation counter and one new action `resume(child)` (enabled when the child is `interrupted`; bumps the generation and returns the child toward `active` under the existing re-delegation rules). `complete(child, g)` becomes enabled only when `g` equals the modeled current generation. Keep the existing three task slots; depth may rise to at most 14 to cover one interrupt/resume/complete cycle per child; the 10,000-state budget and landmark reachability rules apply unchanged. Bounds cannot truncate silently: the existing frontier check already fails on any unseen successor. + +**Model/checker change.** None in this PR, justified: no persisted generation token exists in `historyItemSchema`, so there is no faithful production boundary to import, and adding generation state to the checker now would model unimplemented behavior — the same reason the lifecycle model page documents this exclusion today. This specification is the dependency input for `LIFE-BLK-P2-006` (completion commit phases) and `LIFE-BLK-P6-009`/`P6-033` (event and resume-ingress contracts). + +## LIFE-BLK-P1-017 — immutable store read semantics (GAP-017) + +**Bounded property.** A caller that mutates an object returned by `TaskHistoryStore.get`, `getAll`, or `getByWorkspace` cannot alter the cache or disk state of the store. Exclusions: consumers of downstream serialized copies (webview and IPC payloads are already structured-cloned at the message boundary) and the `onWrite` write-through callback, which receives a freshly built array per mutation. + +**Current behavior.** `get` returns the live cache object by reference; `getAll` and `getByWorkspace` allocate a new array containing those same live objects. The store's own atomic updaters are safe (`atomicReadAndUpdate`/`atomicUpdatePair` pass `structuredClone` snapshots to updaters), but the public read surface is not. + +**Direct-caller inventory (exhaustive over `src/`, `packages/`, `apps/` at the authoring commit).** `TaskHistoryStore` is constructed per `ClineProvider` instance (one per extension-host view) and reached as `provider.taskHistoryStore`. + +| Caller | Use of returned record | Mutates? | +| ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | --------------------- | +| `ClineProvider` (~25 sites: delegation guards, completion, abandonment, eviction, mode/profile updates, webview/API state, broadcasts, write-through) | Reads fields; every write path spreads into a new object first | No | +| `ClineProvider.getTaskWithId` | Returns the cached reference onward to its own callers (which spread before writing) | No (shared reference) | +| `Task.clearPendingActionAfterDurableResult` | Assigns the nested `pendingAction` object to an in-memory field | No | +| `Task.saveClineMessages` | Reads `status` to preserve it in the rebuilt item | No | +| `extension/api.ts` `getTaskHistoryItem` | Returns `structuredClone(item)` — already defensive | No | +| `extension/api.ts` `TaskCompleted` listener | Reads `parentTaskId` | No | +| Test suites (`TaskHistoryStore.*.spec.ts`, provider and delegation specs) | Read-only assertions, setup upserts, or stubbed mocks | No | + +`getByWorkspace` has no production callers (only a `TaskHistoryStore.spec.ts` suite exercises it); `packages/` and `apps/` contain no `TaskHistoryStore` usage. No current caller, production or test, mutates a returned record. The risk is latent: the API hands out live references, so any future or unspotted mutator bypasses the store lock entirely. + +**Immutable read semantics (decision to implement).** Records must leave the store as immutable snapshots. Evaluate in the implementation PR, in order of preference: (a) deep-freeze records at cache-write time and return the frozen references — zero per-read cost and turns mutation into a strict-mode `TypeError`; (b) `structuredClone` on read — strongest isolation, per-read cost on the hot `getStateToPostToWebview` path; (c) type-level `ReadonlyDeep` plus lint — no runtime safety, weakest. Option (a) with (c)'s types is the recommended combination given the all-read-only inventory. + +**Clone/freeze test criteria (store tests).** A focused `TaskHistoryStore` test must obtain a record via `get`/`getAll`, attempt to mutate a scalar field and the nested `childIds` array, and assert that a second `get` and a subsequent `reconcile` from disk both show the unmutated record; under the freeze option the mutation attempt must throw in strict mode (or no-op) with the record unchanged. Compatibility exclusions recorded: webview/IPC serialization is unaffected; the `onWrite` array contract is unaffected; `getByWorkspace` may be removed or receive identical semantics; no test-suite rewrites beyond the new mutation-regression test are anticipated. + +**Model/checker change.** Not applicable: this is a read-side API contract, not a transition protocol, and no faithful model boundary exists for object-reference mutability in the explicit-state explorers. The evidence is the caller inventory above plus the specified mutation-regression test. + +## LIFE-BLK-P1-020 — stale-cache and convergence histories (GAP-020) + +**Bounded property.** Watch/reconcile convergence is eventual and failure-tolerant, not coherent. For any finite, quiescing sequence of peer writes, a host whose next `reconcile()` completes successfully converges its cache to disk for every task ID. During the stale window there is no coherence guarantee: same-field conflicts remain last-writer-wins at the `mergeHistoryDelta` boundary. Exclusions: repeated reconcile failure (no convergence bound), network filesystems, process crash/power loss mid-write (`LIFE-BLK-P2-004`), and lock-implementation semantics. + +**Observable histories (named, bounded).** + +1. **Watcher-refresh history.** Peer commit → `fs.watch` event → 500 ms debounce → `reconcile()` → cache updated. Stale-read window bound: debounce interval plus one reconcile pass, when the watcher fires. +2. **Missed-watch history.** `fs.watch` error or platform unreliability (handled in `startWatcher`, which logs and falls back) → no event → cache stays stale until the periodic reconcile (`RECONCILE_INTERVAL_MS`, 5 minutes) or an explicit refresh. Stale-read window bound: one periodic interval plus one reconcile pass, when reconciles succeed. +3. **Explicit-refresh history.** `invalidate(taskId)` (used by `delegateParentAndOpenChild` and the `reopenParentFromDelegation` continuation before reading authoritative state) or webview-triggered `invalidateAll()` + `reconcile()` (`webviewMessageHandler`) → cache re-read from disk inside the store lock. Window bound: zero for the invalidated entry once the call resolves. + +`reconcile()`'s mtime skip is exact for this property because `taskFileMtimes` records the last observed mtime per task; the lock-file liveness fallback keeps a task live across a peer's atomic rename window instead of evicting it. + +**Objective convergence evidence (existing).** `TaskHistoryStore.crossInstance.spec.ts` covers peer create/delete/update detection through reconciliation and invalidation visibility with two real stores; `TaskHistoryStore.realConcurrency.spec.ts` exercises the real `proper-lockfile` plus rename path; `TaskHistoryStore.reconciliation.spec.ts` covers drift repair. These are focused production tests, not exhaustive interleavings. + +**Additional test evidence required for closure (deterministic, no timing sleeps).** + +- _Missed-watch:_ construct a store whose watcher is stubbed to error or never fire, commit a peer write through a second real store, advance fake timers past the periodic interval (or invoke `reconcile()` directly), and assert byte-level convergence with disk. +- _Missed-watch with concurrent update:_ during the stale window, perform a local delta write and assert the peer's fields survive (delta merge preserves fields absent from the delta), then assert convergence after the next successful reconcile. +- _Reconcile-failure injection:_ inject a reconcile failure inside the window and assert only that the stale window extends — never that state diverges — with a successful reconcile restoring convergence. Rollback verification: after each injected failure, the cache must equal either the pre-write or post-write disk state, never a partial merge. + +**Shared-store landmarks.** The existing `stale-cache-newer-disk` semantic landmark already requires a reachable state where a host cache diverges from newer disk state; this block names the histories that produce and resolve it. No checker change in this PR: the watcher, debounce, and timers are environment effects, and the explicit-state explorer models `refresh` as an environment action already; a timing-faithful watcher model would require the temporal semantics the suite deliberately defers (see the lifecycle model page's tooling rationale). Depends on the LIFE-BLK-P1-001 vocabulary — satisfied within this document. + +## Traceability + +| Block | GAP | Delivered here | Closure evidence deferred to implementation PR | +| --------------- | --- | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | +| LIFE-BLK-P1-001 | 001 | Lock-time ownership check named; witness, bounds, and promotion test specified | Disk-authoritative guard, two-store regression test, witness promotion | +| LIFE-BLK-P1-002 | 002 | Field ownership table; monotonic-detachment invariant; stale-save witness linked | Owner enforcement or tombstone/generation; fault-injection save tests | +| LIFE-BLK-P1-012 | 012 | Two-generation semantics, landmarks, bounded future checker shape | Persisted generation field; reducer/store/API rejection; restart E2E | +| LIFE-BLK-P1-017 | 017 | Exhaustive caller classification; immutable-read decision; test criteria | Freeze/clone implementation plus mutation-regression test | +| LIFE-BLK-P1-020 | 020 | Named stale-cache/convergence histories; bounded properties; test criteria | Missed-watch and fault-injection convergence tests | + +No block in this document claims its parent GAP is closed or that current production behavior is safe against the named witnesses. diff --git a/docs/architecture/task-lifecycle-remediation-blocks.md b/docs/architecture/task-lifecycle-remediation-blocks.md index ffbcbb334b..1e7c9d9041 100644 --- a/docs/architecture/task-lifecycle-remediation-blocks.md +++ b/docs/architecture/task-lifecycle-remediation-blocks.md @@ -29,6 +29,8 @@ Completing one block does not close its `LIFE-GAP` unless the parent GAP closure | 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. | +The P1 block specifications live in [Task lifecycle persisted ownership and generation model](./task-lifecycle-persisted-ownership-model.md) (added under [#1689](https://github.com/Zoo-Code-Org/Zoo-Code/issues/1689)): the shared ownership vocabulary, the lock-time awaited-child revalidation boundary, the lineage field-ownership table and monotonic-detachment invariant, the attempt-generation semantics and future checker shape, the immutable-read caller inventory, and the named stale-cache/convergence histories. + ## P2: Durable operation and crash recovery | Block | GAP | 1-SP increment | Production/model/test mapping | Depends on | Acceptance |