fix(daemon): keep lease deferral and claim verdicts identity-safe
What
Two daemon accounting bugs that keyed state on a value that is not unique per owner.
Bug A — a cancelled command could stop deferring its lease. LeaseInFlightWorkRegistry.releasePass deleted the whole entriesByLeaseId key whenever the releasing pass left its set empty. Pruning a cancelled pass can leave that set empty while a later pass has already claimed the same lease id through a new set, so the release dropped a live request's deferral and the leased device's lease could expire under the command still running on it. The key is now deleted only while it still holds the set the pass was issued out of (this.entriesByLeaseId.get(leaseId) === entries).
Bug B — shutdown could report an orphan claim as released. The shutdown claim ledger recorded clear outcomes under SessionState.name, which two cwd-scoped sessions share as default. One session's verdict was read as the other's, so a claim that was never cleared was reported as released — hiding exactly the orphan daemon stop exists to surface. Outcomes are now keyed by the claim's ownerToken, which acquireDeviceClaim mints per acquisition.
Base / head
- Base:
ff8c2285c808135b592e77f53ae3ce28d31786b8 - Head:
664888aedcf7eed2e7c971955cc329ff88556fa9
Both fixes are at the owning type. No compat shims, no new shared helpers, no error-text matching; the existing typed reasons (LEASE_SCOPE_REQUIRED, etc.) are untouched. daemon-runtime.ts is deliberately not touched, so this does not collide with #4063. Upstream has moved to f7879cf2d2 since base; its 124 changed files intersect none of the five files here, so the branch merges cleanly and the gate receipt is for the exact head.
Regression tests: failing on base, passing on head
Tests first. Each was recorded failing on the unfixed sources, then passing with the fix. Re-checked on this head after rebase-ready: reverting just the two fixed files to base reproduces all three failures.
Bug A, owning type:
FAIL |unit-core| packages/daemon/src/resources/__tests__/lease-in-flight-work.test.ts > releasing a cancelled pass keeps a later pass deferring its lease
AssertionError: expected false to be true // Object.is equality
Bug A, device-costing seam (inspectLeaseBackend + consumeExpiredLeases — the "this lease is now free to take" path):
FAIL |unit-core| packages/daemon/src/resources/__tests__/lease-registry.test.ts > a cancelled request finishing late keeps a later request deferring its lease
AssertionError: a request still working on the lease must keep it alive past its own window
+ actual - expected
Bug B, driving the production ordering (session B clears and finalizes, then session A finalizes with no clear of its own), two default sessions on different devices in one shared claims store:
FAIL packages/daemon/src/server/http/daemon-shutdown-claims.test.ts > ...
- "orphaned": [ { "deviceId": "emulator-5554", ... "session": "default" } ],
+ "orphaned": [],
+ "released": [ { "deviceId": "and-1", ... }, { "deviceId": "emulator-5554", ... } ],
After the fix:
Test Files 3 passed (3)
Tests 58 passed (58)
Gate receipt
pnpm format, then pnpm check:affected --run --head HEAD (foreground, blocked on the shared validation lock):
[passed] format; [passed] lint; [passed] typecheck; [passed] layering; [passed] fallow;
[passed] build; [passed] vitest-related; [passed] unit
check:affected: all runnable checks passed.
[passed] exactCommit=true; receipt=.tmp/validation/run-TpjQgZ/receipt.json
The first run failed only on apple-platform-output-guard.test.ts crossing 2x its wall-clock budget while unrelated gate work shared the machine; it passed alone at 20.2s and the full gate passed on re-run. Nothing in that test is touched by this diff.
Live evidence, and where the route stops
Against a daemon from this checkout (0.21.36-dev, codeOrigin: checkout) on the assigned iOS Simulator 40A3A3C9-658F-4F37-BE02-F45794C2B49E, in a separate --state-dir, with AGENT_DEVICE_LEASE_TTL_MS=5000 / AGENT_DEVICE_LEASE_MIN_TTL_MS=5000:
lease.allocate: {"leaseId":"7d2b41e9...","deviceKey":"ios:mobile:40A3A3C9-...","expiresAt":1793517222356}
wait(abort): AbortError # client hung up mid-command, daemon marked the request canceled
other-tenant.allocate (device should be free again): {"leaseId":"9f4c2a6b...", ...} # the expired lease was swept
original.release: {"released":false} # the aborted run's lease was already gone, not leaked
sessions: No active session. Run open first.
daemon stop → {"claimsReleased":[],"claimsOrphaned":[]}
So the live route holds: allocation, client cancellation, TTL expiry, sweep and claim release all behave on real transport, with no leaked lease or claim on the simulator.
Exact limit: the cancellation-during-lease interleaving itself is not reachable through the sanctioned device command. The T3-provided wrapper drives published 0.21.31 (not this build), refuses connect / daemon / device / proxy, and lease admission needs a provider connection it will not make; a local unleased command never takes an in-flight work pass at all (runAdmittedLeaseWork returns early when req.internal.admittedLease is undefined). I could not reach the pruning-then-later-release ordering on a real device with the tools available, so Bug A's regression proof is the two tests above, not a device trace.
Review
The coordinator arranges the independent review for this PR.