Skip to content
This repository was archived by the owner on Oct 23, 2026. It is now read-only.

fix(control-plane): STALE_PHYSICAL_LEASE for overlapping stale leases; sidecar refuses closed file with held lease (#415, #516) - #525

Merged
james3014-nexus-agent[bot] merged 4 commits into
mainfrom
fix/issue-415-516-orphan-lease
Oct 16, 2026
Merged

james3014-nexus-agent[bot] merged 4 commits into
mainfrom
fix/issue-415-516-orphan-lease

Conversation

@james3014-nexus-agent

Copy link
Copy Markdown

Closes #415
Closes #516

Summary

The lease state machine for stale and orphan leases now ends in an actionable terminal path. Nothing is auto-released and no SQLite manual edit is needed.

Already on main (#419, 709368f). The Owner-local, CLI-only hygiene command devspace carrier release-revoked-idle-cutover-lease <lease-id> … covers every #415 precondition:

  • exact lease and version CAS;
  • exact revoked root carrier, with its version and validity version;
  • expired lease and carrier;
  • matching operation id;
  • no durable operation, terminal witness, or cutover-state reference;
  • --confirm set to the lease id.

It writes a durable receipt (migration 26), and replay returns replayed:true. It also made the overlap scan skip an expired, disjoint lease whose path is gone. This PR does not add a second hygiene authority.

Added here:

  • P0: orphan expired leases can permanently block control-plane ownership #415(b), src/control-plane-ownership.ts. When an expired lease whose path no longer resolves overlaps the requested scope (judged by stored identity), acquisition now fails with the new bounded code STALE_PHYSICAL_LEASE instead of a generic conflict. The error names the lease id and the exact hygiene command syntax. It still fails closed and never auto-releases.
  • m5-cutover sidecar: closed file with unreleased lease is treated as a new attempt (overlapping-scope, no recovery hint) #516, scripts/ops/m5-cutover/m5-cutover-lib.mjs. selectMode stays pure and takes a new closedLease input. A closed cutover file whose state-root lease is not terminal now throws Refuse with the cutover release-terminal-lease … replay hint. Before, it started a brand-new attempt and failed on overlapping scope. A terminal lease, a missing lease row, or no binding still returns full.
  • m5-cutover sidecar: closed file with unreleased lease is treated as a new attempt (overlapping-scope, no recovery hint) #516, m5-cutover-sidecar.mjs. Before selectMode, a read-only probe of the closed file's coordinationBinding.leaseId, using the same CarrierBindingStore the sidecar already opens afterwards.
  • Tests. Hygiene fail-closed matrix in carrier-binding.test.ts: non-revoked carrier, non-expired lease, wrong confirm/carrier/version/lease, pinned lease, terminal witness, and no state change on denial. Stale overlapping lease blocks with STALE_PHYSICAL_LEASE while a disjoint one does not (control-plane-ownership.test.ts). Closed file plus held lease gives Refuse (m5-cutover.test.mjs).
Lease state Path Result
closed cutover, completed generation cutover release-terminal-lease (#477/#506) released
revoked root carrier, expired, unpinned, prepare-only, no durable effect carrier release-revoked-idle-cutover-lease (#419) released + receipt, idempotent
expired, path gone, disjoint untouched acquisition proceeds
expired, path gone, overlapping untouched STALE_PHYSICAL_LEASE (new)
expired, path present, overlapping untouched OWNERSHIP_CONFLICT
pinned or durable effect reconcile paths only never hygiene

No change to cli.ts, durable-operations.ts, cutover-state.ts or server.ts, so it does not overlap with #478. No schema change.

Evidence

  • RED→GREEN: m5-cutover.test.mjs 11/1 → 12/0; control-plane-ownership.test.ts 36/1 → 37/0. carrier-binding.test.ts: 52/0. The new denial matrix was green immediately because fix: recover stale carrier leases safely #419 already enforced it, so it adds coverage only.
  • npm run typecheck clean; check-test-chain OK; merged with main 67907f2 (refactor: C4 retire active Carrier governance from direct Dev MCP operations #478) and re-run.
  • Locally, full npm test stops in process-sessions-ci, local-agent-process-tree and host-operation-policy with "detached grandchild survives". These fail identically on clean main on this macOS host (environment), so they are unrelated.

Not done here

The live M5 orphan lease from the #415 body is not touched. Cleaning it is an Owner-local operation with the existing command and needs no code change.

🤖 Generated with Claude Code

James3014 and others added 4 commits October 16, 2026 11:31
…verlap robustness; sidecar lease-aware closed mode (#415, #516)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…LI syntax (#415)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ct recovery command shape in tests (#516)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@james3014-nexus-agent

Copy link
Copy Markdown
Author

Review record — head 828b6045df598c21078d64e4d37d331c7da058fc

Independent Claude review (Claude Opus 5.5, bundle-M2-cutover lead; the implementation was done by a separate Sonnet agent): ACCEPT at 828b604.

  • STALE_PHYSICAL_LEASE fires only inside the existing storedConflict branch, and only for an expired lease whose assertPhysicalBinding fails with exactly "resource identity cannot be resolved". Every other case still throws OWNERSHIP_CONFLICT. There is no fail-open and no auto-release.
  • Found and fixed during review (f83126b): the hint named the hygiene command with the wrong argument order. It now matches the real CLI usage at src/cli.ts:447.
  • selectMode stays pure. A closed file with a non-terminal lease gets Refuse, with the release-terminal-lease hint copied from m5-cutover.sh step 10. A terminal lease, an absent lease, or no binding still returns full.
  • The sidecar probe reuses the CarrierBindingStore the sidecar already opens afterwards, so no new DB writer is added. It runs only for phase === "closed".
  • RED→GREEN: m5-cutover.test.mjs 11/1 → 12/0; control-plane-ownership.test.ts 36/1 → 37/0; carrier-binding.test.ts 52/0. Re-run after merging main 67907f2.

Cross-model review (Gemini 3.8 Flash High via agy; Codex substitute, Codex out of credits)

  • r1 @f83126b: REJECT, four findings:
    • F1/F2, "--confirm takes no value": rebutted. The real CLI parses --confirm with value() (src/cli.ts; usage --confirm <lease-id> / --confirm <id>), and the shipped m5-cutover.sh calls --confirm "$CLOSED_ID". The r1 prompt had summarised the flags without their values.
    • F3, cutoverId missing from the refusal receipt: fixed in 828b604 (Refuse extra carries cutoverId).
    • F4, loose test regexes: fixed in 828b604. The tests now pin the exact anchored command shape and extra.cutoverId.
  • r2 @828b6045d: ACCEPT. No remaining fail-open: an overlapping stale lease always blocks, and the probe is scoped and closed in finally.
  • Verdict: ACCEPT at 828b604

Delta since review: none. The macOS smoke is red because of a Node 24 OpenSSL sandbox environment error; it is also red on main 67907f2 and is not a required check.

@james3014-nexus-agent
james3014-nexus-agent Bot merged commit 1f944c4 into main Oct 16, 2026
13 of 14 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

1 participant