Summary
Under active coordination.runtime_shadow capture, todo archive-completed on a Todo that still holds a released task-lease record makes the bounded qualification drift: coordination-shadow inspect reports drifted / shadow_projection_drift, and qualify / read-candidate reject from then on. The same archive of a Todo that never had a lease stays matched. Restoring bytes does not requalify; the only recovery is rollback --provider-revision … --execute plus a fresh bootstrap --execute, which discards the captured lineage.
This is the ordinary hard_lease flow (task-lease acquire → todo complete with the lease → todo archive-completed), so it blocks the D3 card's "audit sustained mixed-writer coverage against the final command matrix". #4167 declared it as the pending ladder row s2c2.archive_after_leased_completion_parity and made s2c2.parity_equal archive nothing rather than hide it. Nothing on main has changed the path since; #4286 isolates the canonical (post-promotion) archive transaction in todo_archive.ts and does not touch this legacy capture path.
Issue origin: observed and reproduced in a disposable environment (real CLI, production FileAuthorityStore, no live Goal).
Reproduced on: f38847b1c (first seen) and 58dbaeaec (today).
Reproduction (public CLI only)
Registry: one hard_lease Goal, registered_agents: [agent-a, agent-b], coordination.runtime_shadow = {schema_version: loopx_coordination_runtime_shadow_config_v0, enabled: true, provider: file_v0}; disposable --runtime-root.
loopx --registry R --runtime-root RT --format json coordination-shadow bootstrap --goal-id G --execute
loopx … todo add --goal-id G --role agent --text "Task to complete." --task-class advancement_task # todo_id = T
loopx … task-lease acquire --goal-id G --todo-id T --owner agent-a --idempotency-key k --ttl-seconds 120 # version 1
loopx … todo complete --goal-id G --todo-id T --agent-id agent-a --task-lease-idempotency-key k \
--task-lease-expected-version 1 --evidence validation://x --no-follow-up
loopx … coordination-shadow inspect --goal-id G # status=matched
loopx … todo archive-completed --goal-id G --max-active-done 0 --execute
# coordination_runtime_shadow.outcome=delivered, drain.last_cursor=6
loopx … coordination-shadow inspect --goal-id G # status=drifted, reason_code=shadow_projection_drift
Control: the same sequence without the lease (soft_claim, todo complete without lease flags) ends with inspect matched after the archive.
Observed difference between the two projections after the archive (todos partition equal, handoff_mode equal):
- source projection (
build_runtime_shadow_source_snapshot): leases: []
- candidate head (last transaction of the runtime-shadow lineage):
leases: [{todo_id: T, status: "released", owner: agent-a, version: 1, …}]
Root cause
- The source snapshot projects only leases whose Todo is still in the active Todo projection; a lease file whose Todo is gone stays in
lease_inventory but is not projected. test_source_snapshot_preserves_inventory_without_projecting_orphan_leases pins that rule.
todo archive-completed under capture prepares and commits one todos-partition entry. The candidate's leases partition still holds the released lease committed by the earlier fence-close transaction (task_lease_fence_close), and no writer ever retires it.
- After the archive the source therefore has zero projected leases while the head keeps one, so
qualifySnapshot compares different head digests.
Candidate fixes (owner decision)
- Archive writer retires orphaned leases. When
archive-completed moves a Todo that has a lease record, it also prepares/commits a leases-partition entry whose projection drops the archived Todos' leases. Keeps the orphan rule and the existing test; one command then produces two partition entries, so their ordering and a crash between them need the same settle-from-receipts treatment as today's single entry.
- Source snapshot keeps released leases of archived Todos. Changes the orphan rule and
test_source_snapshot_preserves_inventory_without_projecting_orphan_leases; a bootstrap baseline would then import every historical released lease, which grows the live head.
- Compose-time retirement in the TypeScript head.
composeLocalAuthorityShadowHead drops leases whose Todo left the todos partition when a todos entry commits. No new writer entry, but the head is then derived rather than captured, which weakens "one receipt per mutation" and replay determinism.
If the roadmap retires the legacy archive writer before D3 (T4), closing by retirement is also an answer; the ladder row would then need a canonical-archive twin instead.
Option 1 looks most consistent with "one receipt per mutation", but it is your call. Once decided, I can turn the pending ladder row into an executable regression with a matching mutant within the qualification scope.
Evidence
- Pending declaration and reproduction notes:
examples/shared-goal-authority-e2e/README.md (pending rows paragraph), loopx/control_plane/testing/authority_e2e_ladder.py (PENDING_ROWS), RFC §11.2 in both languages.
- Related rows that pass today:
s2c2.parity_equal (mixed writers without archive), s2c2.parity_divergent_detects_foreign_edit (drift detection and rollback recovery).
Summary
Under active
coordination.runtime_shadowcapture,todo archive-completedon a Todo that still holds a released task-lease record makes the bounded qualification drift:coordination-shadow inspectreportsdrifted/shadow_projection_drift, andqualify/read-candidatereject from then on. The same archive of a Todo that never had a lease staysmatched. Restoring bytes does not requalify; the only recovery isrollback --provider-revision … --executeplus a freshbootstrap --execute, which discards the captured lineage.This is the ordinary
hard_leaseflow (task-lease acquire→todo completewith the lease →todo archive-completed), so it blocks the D3 card's "audit sustained mixed-writer coverage against the final command matrix". #4167 declared it as the pending ladder rows2c2.archive_after_leased_completion_parityand mades2c2.parity_equalarchive nothing rather than hide it. Nothing onmainhas changed the path since; #4286 isolates the canonical (post-promotion) archive transaction intodo_archive.tsand does not touch this legacy capture path.Issue origin: observed and reproduced in a disposable environment (real CLI, production
FileAuthorityStore, no live Goal).Reproduced on:
f38847b1c(first seen) and58dbaeaec(today).Reproduction (public CLI only)
Registry: one
hard_leaseGoal,registered_agents: [agent-a, agent-b],coordination.runtime_shadow = {schema_version: loopx_coordination_runtime_shadow_config_v0, enabled: true, provider: file_v0}; disposable--runtime-root.Control: the same sequence without the lease (
soft_claim,todo completewithout lease flags) ends withinspectmatchedafter the archive.Observed difference between the two projections after the archive (todos partition equal,
handoff_modeequal):build_runtime_shadow_source_snapshot):leases: []leases: [{todo_id: T, status: "released", owner: agent-a, version: 1, …}]Root cause
lease_inventorybut is not projected.test_source_snapshot_preserves_inventory_without_projecting_orphan_leasespins that rule.todo archive-completedunder capture prepares and commits onetodos-partition entry. The candidate'sleasespartition still holds the released lease committed by the earlier fence-close transaction (task_lease_fence_close), and no writer ever retires it.qualifySnapshotcompares different head digests.Candidate fixes (owner decision)
archive-completedmoves a Todo that has a lease record, it also prepares/commits aleases-partition entry whose projection drops the archived Todos' leases. Keeps the orphan rule and the existing test; one command then produces two partition entries, so their ordering and a crash between them need the same settle-from-receipts treatment as today's single entry.test_source_snapshot_preserves_inventory_without_projecting_orphan_leases; a bootstrap baseline would then import every historical released lease, which grows the live head.composeLocalAuthorityShadowHeaddrops leases whose Todo left thetodospartition when atodosentry commits. No new writer entry, but the head is then derived rather than captured, which weakens "one receipt per mutation" and replay determinism.If the roadmap retires the legacy archive writer before D3 (T4), closing by retirement is also an answer; the ladder row would then need a canonical-archive twin instead.
Option 1 looks most consistent with "one receipt per mutation", but it is your call. Once decided, I can turn the pending ladder row into an executable regression with a matching mutant within the qualification scope.
Evidence
examples/shared-goal-authority-e2e/README.md(pending rows paragraph),loopx/control_plane/testing/authority_e2e_ladder.py(PENDING_ROWS), RFC §11.2 in both languages.s2c2.parity_equal(mixed writers without archive),s2c2.parity_divergent_detects_foreign_edit(drift detection and rollback recovery).