Skip to content

[Bug]: archive-completed of a Todo with a released lease record drifts the runtime-shadow qualification (orphaned lease stays in the candidate head) #4315

Description

@wchwawa

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)

  1. 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.
  2. 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.
  3. 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).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingcontrol-planeQuota, todo, scheduler, registry, state, goal, or runtime control-plane change.direction/shared-coordinationShared-goal authority and cross-host coordination contracts.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions