Skip to content

fix(cli): end a remote lease allocated by a materialization that never persisted - #4135

Open
thymikee wants to merge 8 commits into
mainfrom
fix/sm2-remote-lease-rollback
Open

thymikee wants to merge 8 commits into
mainfrom
fix/sm2-remote-lease-rollback

Conversation

@thymikee

@thymikee thymikee commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

Summary

A failed remote materialization releases leases proven created by that command before a durable session record takes ownership. A single slot covers allocation, binding, Metro preparation, and persistence. Every allocate response remains eligible for binding; omitted ownership evidence never authorizes rollback.

Spec refusal retains one rollback retry after a thrown release. Failed cleanup now has one typed shape: details.leaseRollback with reason LEASE_RELEASE_FAILED. A successful retry leaves no stale failure evidence.

26 files overall, including the mechanical split. This round touches 5: 2 production, 2 tests, 1 doc. No scope expansion or linked issue (SM-2).

Validation

Head 8ee729017c8863d052e3771c2a48ac7a956c16f9, parent c36e0f1e21ca5e66eac07ad8e1400fd73cc7178c.

  • Spec suite: parent production 2 failed / 13 passed; fixed production 15 passed. Three focused suites: 33 passed.
  • pnpm format and pnpm check:affected --run --head HEAD passed (41 runnable checks, 25 explicitly skipped), exactCommit=true, checkoutChanged=false; receipt .tmp/validation/run-YCAgiS/receipt.json. Hosted coverage/device smoke and current-head review remain pending.
  • Live Limrun at 4472e078: forced Metro failure followed successful allocation/release; instance android_euna_01m6cfayamfbv8evtry0dze6sm terminated; isolated daemon stopped without pending releases. This round preserves release ordering.
  • Hosted coverage and device smoke remain CI obligations. Coordinator review remains outstanding. The seven thread dispositions and success-path ownership proof are in the review-round comment.

connection-runtime.ts crossed the 1,000-line architecture-debt line. Split it
along the seams the file already had, with no behavior change:

- connection-lease-policy.ts: which lease a provider's connection allocates, its
  TTL, and the id it names a new lease with
- connection-lease-transaction.ts: the acquisition transaction — resolve the
  device, allocate or renew the lease, bind it to the record, and release it
- connection-metro-runtime.ts: the deferred Metro runtime preparation the record
  carries, and the tunnel teardown
- connection-previous-lease.ts: releasing the lease a reconnect replaces, with
  the previous endpoint's own credential
- connection-runtime.ts: the materialization that sequences those three and
  persists the record

Importers now name the module that owns each declaration.
…r persisted

`materializeRemoteConnectionForCommand` allocated a lease and then could fail
before `persistMaterializedConnection` wrote the record that would name it — a
rejected `prepareConnectedMetro` was the live path. The only release path ran
when the state-file write itself failed, so the new lease was neither saved nor
ended: no record named it, so `disconnect` could not reach it, and it held the
device until its TTL lapsed.

The call now owns a lease it allocated until persistence transfers ownership,
and every earlier exit ends it. A lease the call renewed was already named by
the stored record, so this path never releases it — disconnect still does.

The failure that ended the command stays the thrown error. A release that failed
too is recorded beside it as typed secondary evidence under `details.leaseRollback`
(ADR 0010 as amended by ADR 0019), never in its place.
The split left every module under the 1,000-line limit, so the 1,019-line legacy
cap for connection-runtime.ts has no subject.
@vercel

vercel Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
agent-device-website Ready Ready Preview Nov 1, 2026 11:08pm UTC

Request Review

@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.67 MB 5.67 MB +1.2 kB
Package (unpacked) 5.67 MB 5.67 MB +1.2 kB
Package (download) 1.72 MB 1.72 MB +535 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 41.8 ms 44.4 ms +2.6 ms
CLI --help 88.9 ms 90.6 ms +1.7 ms

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 13 files

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

@thymikee thymikee left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs changes: three ownership gaps remain against the coordinator’s requirement.

  1. P1 — Do not equate an allocation RPC with a newly created lease. src/cli/commands/connection-lease-transaction.ts:62 (classification originates at :349–363). If session A has persisted a lease, session B uses the same tenant/run/backend with no lease ID, and B’s Metro preparation fails, leases.allocate() can return A’s existing lease. LeaseRegistry.allocateLease() explicitly reuses that run binding (packages/daemon/src/resources/lease-registry.ts:93–94,192–209), and the handler already computes reused but does not expose it in the lease response. This client nevertheless returns acquired: true, so B’s rollback terminates A’s persisted lease. I confirmed that two allocations for the same scope return the same lease ID. Fix: carry the authoritative new/reused allocation outcome through the client and release only a lease actually created by this transaction. Add a two-session regression case.

  2. P2 — Start ownership before acquisition’s post-allocation validation. src/cli/commands/connection-runtime.ts:106–115 and src/cli/commands/connection-lease-transaction.ts:64–69. The outer rollback learns the lease only after acquireConnectionLease() returns. A proxy allocation for ios-instance that returns deviceKey: android:mobile:emulator-5554 is bound, then rejected by carriedConnectionPlatform() with CONNECTION_PLATFORM_CONFLICT; the new lease never reaches unrecordedLease, no release occurs, and no stored record names it. An isolated head probe reproduced this. Binding’s own release paths also sit before this ownership handoff. Fix: register each genuinely new lease immediately when allocation succeeds, keep ownership across binding and validation, and make binding cleanup consume that same ownership so a failure cannot leak or release twice. Preserve the primary error and attach failed cleanup as typed secondary evidence.

  3. P2 — Transfer ownership at the actual durable record write. src/cli/commands/connection-runtime.ts:131–143,171. writeRemoteConnectionState() publishes the session record and then .active-session.json (packages/remote/src/remote-connection-state.ts:254–255). If the second publication fails, the catch releases the lease even though the session record already names it. I reproduced this by making .active-session.json a directory: the call rejects, the persisted session contains lease-1, and rollback releases lease-1. Fix: expose/observe the session-record commit point and clear rollback ownership there, or make persistence restore its previous record before reporting failure. Do not treat a later pointer-write failure as proof that no ownership transfer occurred. Add a failure between the two publications.

Review evidence at head b4deffa99966087e2576ed9519606bc102497222, merge-base f7879cf2d266ee1dd476d0342b2819bdeceb647b:

  • Mechanical move verified with git diff -M90% --stat d8e81888^ d8e81888 (817 additions / 785 deletions) plus a parser-based content comparison: all 44 original declarations are byte-identical after excluding imports/export modifiers. Importer changes only redirect ownership. The gross churn is the requested split; no hidden behavior change found.
  • chore(gates) removes only the obsolete 1,019-line exemption. The five production modules are 234/95/441/217/148 lines. No gate weakening, new allowlist, or unrelated baseline regeneration found.
  • One focused Vitest invocation in a disposable checkout after frozen install/build: the PR’s three tests pass at head; the same tests against the exact pre-fix runtime/transaction declarations at d8e81888b0064aa5de5df4b707c9bc89296e1dc6 produce two intended failures and one pass. The pre-fix copies redirect only their module imports; the expected rollback reason comes from head. Both additional head probes above fail their ownership assertions. Overall: four passed / four failed, with the four failures expected from these probes. This is focused source evidence, not CI readiness.
  • Ordinary heartbeat reuse remains excluded from rollback. The previous write-failure lease rollback was removed, so that direct path does not release twice. Allocation cancellation before a result is handled by the existing daemon allocation handler (packages/daemon/src/sessions/handlers/lease.ts:117–140); no changed cancellation behavior found. The new outer rollback’s primary AppError identity/reason and structured details.leaseRollback conform to ADR 0010/0019 for the covered failure path.

No reviewed source files were edited. CI was not awaited.

@apex-sentinel-staging

apex-sentinel-staging Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Apex Sentinel

Verdict: approve

Since the last review: 7 new · 0 still open · 22 no longer raised

7 finding(s): 2 minor, 5 nit. Verdict: approve. reached max rounds (1).

🟡 Minor (2)

Where Finding
src/cli/commands/connection-lease-transaction.ts:67 🆕 Double release attempt when lease release fails in refuseUnhonouredSpecLease
src/cli/commands/proxy-lease-device.ts:172 🆕 Release failures now surface under two different shapes depending on which cleanup failed
🔵 Nit (5)
Where Finding
src/cli/commands/connection-lease-transaction.ts:55 🆕 Lease-rollback evidence is written onto the shared error instance, so a reused error carries a stale leaseRollback
src/cli/commands/connection-lease-transaction.ts:33 🆕 LeaseLedger.track() silently overwrites its single ownership slot, so the invariant that keeps the by-spec fallback from leaking a lease is unstated
src/cli/commands/connection-lease-transaction.ts:81 🆕 LEASE_ROLLBACK_RELEASE_FAILED_REASON exported constant is used only internally and in tests
src/cli/commands/connection-metro-runtime.ts:151 🆕 Metro rollback failures stay silent while lease rollback failures became typed evidence
src/cli/commands/connection-runtime.ts:133 🆕 An owned lease that survives to the success exit of materializeRemoteConnectionForCommand is neither released nor reported

Reply under an inline comment with @apex-sentinel-staging note ... or @apex-sentinel-staging pivot ... to re-check that finding.
Comment @apex-sentinel-staging review on this conversation to run a full review.

@apex-sentinel-staging apex-sentinel-staging Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apex Sentinel: 7 new findings on b4deffa. The verdict and the full list are in the summary comment.

Comment thread src/cli/commands/connection-runtime.ts Resolved
@thymikee

thymikee commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

I found no blocking problem in b4deffa. The fix ends a lease that a failed materialization allocated but never saved, and I read no path that skips it. Smoke Tests is still running. Those jobs drive local devices without --remote-config, so this diff does not touch their routes. There are no conflicts. Before merge, Smoke Tests must finish and the open threads need replies.

Not blocking, and you can take or leave these. First, carriedConnectionPlatform runs at connection-lease-transaction.ts:66 after the lease is allocated but before the caller owns it. If a by-spec lease returns a device whose platform conflicts with the backend, the throw carries no lease and the catch at connection-runtime.ts:138 cannot release it. The rule: the caller holds the acquired lease before any later statement can throw. Returning right after materialization and running the platform check inside the try would satisfy it, with a test that asserts a release request for that lease id. Second, details.leaseRollback (line 292) is a new field in the JSON error, and no doc mentions it. One sentence next to the details.released text in website/docs/docs/remote-proxy.md would cover it. Third, a failed rollback release now has two shapes: flat {leaseId, released, releaseError} in releaseUnhonouredSpecLease and nested leaseRollback here. The same lease also has three names (acquiredLeaseForCleanup, unrecordedLease, acquiredLease). One shape and one name would be simpler.

On the open threads: the P1 about the silently dropped release failure at proxy-lease-device.ts:135 still applies (#4135 (comment), release failure dropped). The fix is to record the evidence locally, as releaseUnhonouredSpecLease does, and not to hand the lease to the outer rollback, which would release it twice. Two lower-priority nits still apply (duplicated platform narrowing, and the name releaseLeaseThisCommandAllocated). These do not apply, so please resolve them: #4135 (comment) (the daemon returns released:false only when no active lease exists), #4135 (comment) and #4135 (comment) (code moved unchanged from main), #4135 (comment) (release mirrors allocate), #4135 (comment) (the bind branches already release locally), #4135 (comment) (no path drops the Metro cleanup), #4135 (comment) (in-place error mutation is intended), #4135 (comment) (a fresh lease id cannot match the stored one).

I did not run the regression tests, so their pre-fix and post-fix results come from reading the code. The live Limrun run and the check:affected receipt are your reports, and I could not reproduce them. A SIGINT or SIGKILL during Metro preparation bypasses the catch, and I could not tell whether a signal handler turns SIGINT into a rejection before exit.

…he durable record

The independent review found three ownership gaps in the lease rollback.

- The daemon now answers each lease_allocate with whether its request created
  the lease (reused on the wire, Lease.created on the client). A run's repeat
  allocation gets the run's existing lease back, and that lease belongs to
  whoever holds it: this path ends only a lease the daemon says it created.
- A lease enters the new ledger the moment allocation returns, before any
  later validation or binding can fail, so a platform conflict after a
  successful allocation can no longer leak it. Binding cleanup consumes the
  same ownership, so a lease is never released twice.
- Ownership transfers at the durable session record, not at the whole write:
  the commit-point callback runs between the record and the active-session
  pointer, so a pointer failure leaves a named lease to disconnect.

A release that did not end the lease keeps it owned, and the failure path
reports it beside the command's own failure as typed details.leaseRollback.
@thymikee

thymikee commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

All three needs-changes findings accepted and fixed in 3df3025ce7 (new head). Each has a regression test that fails on the previous head (b4deffa9) for the intended reason and passes at the new head; the fix commit touches no other commit's content.

1. P1 — allocation ≠ newly created lease. ACCEPTED. The authoritative answer now comes from the call, not from the returned lease. lease_allocate answers with reused (computed by allocateOrRenewLease, previously discarded); normalizeLease maps it to Lease.created (false on reuse, absent on heartbeat and on daemons older than the field — never guessed). connection-lease-transaction owns a lease only when lease.created === true, so session B's failed open can no longer end session A's persisted, run-bound lease.
Tests: a lease the daemon says this request did not create is kept when Metro preparation fails (client boundary, fails at b4deffa9 by releasing lease-held-by-another-session); lease_allocate answers whether its request created the lease or got back an existing one (handler: first reused: false, repeat same run binding reused: true; fails at base without the wire field); a wire pin in daemon-client-lease-device-spec.test.ts (created: true for a fresh allocate, false for the repeat through the real client).

2. P2 — ownership started too late. ACCEPTED. New LeaseLedger in connection-lease-transaction.ts: acquireConnectionLease tracks the created lease the moment allocate returns — before platform narrowing, target/backend validation, and binding — so a CONNECTION_PLATFORM_CONFLICT after a successful allocation can no longer leak it. bindLeaseNamedDevice consumes the same ledger for its own cleanup (leaseLedger.end), so binding and the outer rollback can never release the same lease twice; a release that did not end the lease keeps the lease owned, and the failure path reports it as typed details.leaseRollback beside the primary error (ADR 0010/0019). An in-flight release is never retried, reported as a release for this lease was still in flight.
Test: a lease allocated before a platform conflict is still released when acquisition fails (proxy grants android:mobile:emulator-5554 for ios-instance; fails at b4deffa9 with no release at all).

3. P2 — ownership transferred at the wrong point. ACCEPTED. writeRemoteConnectionState gained onSessionRecordCommitted, fired after the session record lands and before .active-session.json is written; the ledger transfers there. The record is the durable commit point: a pointer failure now leaves the lease the record names to disconnect, and never releases it.
Test: a lease the session record already names survives an active-pointer write failure (fails at b4deffa9 by releasing the persisted lease-1).

Cubic violations — dispositions:

  • connection-lease-transaction.ts:161 (bind-release swallowed before outer rollback sees the lease) — fixed by finding 2's ledger: binding's release is the ledger's end, an unended lease stays owned, and the rollback attaches leaseRollback evidence.
  • connection-lease-transaction.ts:267 ({ released: false } treated as success) — fixed: the ledger inspects every release outcome; released: false keeps the lease owned and produces details.leaseRollback with reason: LEASE_RELEASE_FAILED.
  • connection-metro-runtime.ts:105 (kind-only Metro profile classified deferred but preparation silently skipped) — not reproducible as silent: hasDeferredMetroConfig admits kind-only profiles, and prepareConnectedMetro then refuses with INVALID_ARGS (requires metroPublicBaseUrl or metroProxyBaseUrl when Metro settings are provided) rather than proceeding without a runtime. The guard that cubic read as a skip is the earlier !flags.metroProjectRoot && !flags.metroPublicBaseUrl && !flags.metroProxyBaseUrl check, which covers only the genuinely empty profile. Kind-only is already refused, and hasDeferredMetroConfig's kind-only positive is pinned in remote-connection.test.ts. No change made.
  • connection-previous-lease.ts:30 (two missing daemon URLs compared equal, ambient token could ride to an unknown endpoint) — valid observation about === on two undefineds, but not reachable: both sides are normalized RemoteConnectionState.daemon?.baseUrl, and both connections materialize daemon from buildRemoteConnectionDaemonState(flags), so a recorded state without any endpoint does not exist on this path. Both connections being truly endpoint-less means one local daemon, which the ambient token is issued for. Pre-fix line predates this PR's scope; if defense-in-depth is wanted, requiring both URLs to be present should land as its own small change with a test — happy to do it in a follow-up.

Verification at head 3df3025ce73a41330213c38a85d7c246cae2975d:

  • pnpm check:affected --run --head 3df3025ce7… — all runnable checks passed, exactCommit=true, receipt .tmp/validation/run-fcHps9/receipt.json.
  • Full vitest --project unit-core (1888 files), format, lint, typecheck, layering, fallow (0 findings), production-exports, build, bundle-owner-files, package — all green.
  • Limrun live check rerun (the release path changed): open on a metro-failure profile failed with package.json not found; session events show lease_allocate ok → lease_release ok; the record holds no leaseId; provider-side, the run's instance android_euna_01m6baj3mmftebcmwrsb92s1d3 was created 10:30:24Z and terminated 10:30:27Z — released by the ledger, no leaked instance.

@thymikee thymikee left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs changes at 3df3025ce73a41330213c38a85d7c246cae2975d (round-2 range b4deffa9..3df3025c).

  1. P2 — Stop fallback when retiring its first lease fails. src/cli/commands/proxy-lease-device.ts:163–167. When the supported keyless-lease fallback receives a newly created lease without deviceKey, leaseLedger.end() now catches a throwing release and returns { released: false }. This caller ignores that outcome and allocates another lease. If the second allocation and persistence succeed, only the replacement is transferred; the first stays in the local ledger, which is then discarded without failure cleanup. Previously a throwing release stopped this path. A head probe reproduced 2 allocations, 1 throwing release, successful materialization, and only lease-2 persisted; lease-1 has no durable owner. This also applies when the first release is canceled/rejected but a subsequent allocation can proceed. Fix: require a confirmed successful retirement before entering fallback, or fail with typed release evidence and let the outer ledger perform cleanup. Do not return success while another unrecorded lease remains owned. Cover the fallback's throwing/refused release case.

  2. P2 — Correct and document the published Lease.created contract. packages/contracts/src/client-lease.ts:18–22. The shipped JSDoc says false means newly created and true means reused, while normalizeLease() implements the opposite. Lease is publicly exported through src/client/client-types.ts → src/sdk/index.ts → the agent-device entrypoint; I also confirmed the reversed prose survives in the built .d.ts. A consumer following this guidance can release another session's reused lease and keep its own newly created lease. Fix: document true = created by this allocation, false = reused, and undefined = unknown / not an allocation; show the created === true release guard in the owning lease documentation at website/docs/docs/client-api.md:347. That page currently has no explanation of the new field. No CLI flag/help syntax changed, so an unrelated help rewrite is unnecessary.

Round-1 disposition check:

  • P1 resolved for the reported two-session failure: the handler returns authoritative reused; the client maps it to created; the CLI tracks only created === true. A missing reused leaves created undefined, which is conservatively excluded from release, never treated as created.
  • P2a's late-registration scenario resolved: tracking is immediately after allocation returns, before binding/platform validation. Binding and outer rollback share the ledger. Successful end removes its entry, preventing a second release from the later catch; failed end remains owned for a retry. Finding 1 is the remaining exit where ownership is abandoned. All production end callers await it; the new inFlight guard concerns a pending client promise, not proof that a canceled transport request has stopped executing remotely.
  • P2b resolved: onSessionRecordCommitted executes immediately after the first session-record publication returns and before active-pointer publication. A session-record write failure does not transfer ownership; a pointer-write failure does. The existing pointer-failure regression passes.

Wire/public surface assessment:

  • reused is an additive optional response field. ADR 0006 explicitly excludes such additions from protocol bumps. Existing clients ignore it; the new client handles omission conservatively, so neither a protocol bump nor a capability gate is needed for safe ownership classification. Older daemons cannot supply affirmative ownership evidence, so failure cleanup may leave an unrecorded lease to expire rather than risk releasing someone else's lease.
  • ADR 0007's lazy allocation and durable connection ownership remain consistent in the reported scenarios, subject to finding 1. The public API runtime addition is optional and compatible; its documentation is unsafe until finding 2 is fixed.
  • No contracts export subpath changed. scripts/layering/contracts-exports.snapshot.json snapshots package export keys, not fields inside Lease, so leaving it unchanged is correct. The wire manifest/ledger tracks shared envelope declarations, not this untyped command-result property or normalizeLease; none of its tracked declarations changed. No unrelated snapshot regeneration or gate weakening appears in this round.

Evidence: reviewed production source stayed read-only and the checkout is clean at the named SHA. In a disposable archive of that SHA, pnpm install --frozen-lockfile && pnpm build passed. One focused Vitest invocation ran the allocation handler, real-client lease wire test, rollback tests, proxy allocation tests, and two scratch probes: 46 passed / 1 failed, with the sole failure proving finding 1 (45 existing tests passed; boolean/omission normalization probe passed). Command: pnpm exec vitest run --project unit-core src/__tests__/r4135b-probe.test.ts src/cli/commands/connection-runtime-lease-rollback.test.ts packages/daemon/src/sessions/handlers/__tests__/lease.test.ts src/__tests__/daemon-client-lease-device-spec.test.ts src/__tests__/remote-connection-proxy-allocate-by-spec.test.ts. No CI wait and no live-device claim.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 12 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread src/cli/commands/connection-runtime.ts Resolved
Comment thread src/cli/commands/proxy-lease-device.ts Resolved
Comment thread packages/contracts/src/client-lease.ts Resolved

@apex-sentinel-staging apex-sentinel-staging Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apex Sentinel: 9 new findings on 3df3025. The verdict and the full list are in the summary comment.

Comment thread packages/client/src/lease-client.ts Resolved
Comment thread packages/contracts/src/client-lease.ts Resolved
Comment thread src/cli/commands/connection-runtime.ts Resolved
…es, and document created correctly

The round-2 review found two gaps.

- The keyless-lease fallback released the daemon-granted lease and ignored
  whether that release ended it. A throwing or refused release left the first
  lease owned by no durable record while the fallback allocated a replacement
  that became the connection. The fallback now requires a confirmed retirement;
  otherwise the command fails with PROXY_LEASE_RETIREMENT_FAILED, the ledger
  keeps the lease, and the failure path retries the release and reports it as
  details.leaseRollback beside the refusal.
- The published JSDoc on Lease.created stated the mapping backwards; it now
  reads true = created by this allocation, false = an existing lease, absent =
  the daemon did not answer. website/docs/docs/client-api.md describes the
  field and the release guard: release only when created is true, and a
  missing answer is not permission to release.
@thymikee

thymikee commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

Thanks for the update. At 3df3025 the lease is now tracked at allocation, so a binding failure ends it, and a release that returns released: false now reports leaseRollback. Two problems remain, and both need a fix before merge.

A rebind can still leave the first lease running. proxy-lease-device.ts:163 ignores the release outcome and allocates a replacement at :167. Because attempt() catches errors (connection-lease-transaction.ts:67), a release that threw and failed the command on main is now swallowed. The first lease then stays owned, and the success exit at connection-runtime.ts:175 neither releases nor reports it. The rule: at the success exit the ledger owns nothing except the lease the record names. A daemon released: false counts as ended, and only a thrown release aborts the rebind. Please fix the class rather than adding a throw on !released. This closes the three threads on this root, linked below.

An allocate answer without created now changes binding, not only rollback (connection-lease-transaction.ts:445-450 sets acquired to created === true). With an older daemon, the rebind fallback at proxy-lease-device.ts:162 throws unboundLeaseError. A spec request that is granted a device takes the renewed branch and can throw CONNECTION_DEVICE_CONFLICT at :142. The rule: acquired means "returned by this allocate", and ownership is decided separately from created. Please add a regression test that omits the field, since all three shared fixtures inject created: true. Please also fix the reversed created doc at client-lease.ts:18-19.

Could the ledger be one owned slot instead of a Map? Keep the two facts apart: acquired is "returned by this command's allocate" (true for every allocate response, as on main), and ownership is decided at the allocate site from created. Once a rebind refuses to allocate while the first lease is unended, at most one lease is ever owned. The ledger then becomes {leaseId, release} | undefined, with no Map, no inFlight and no additionalLeaseIds, and the success-path leak cannot be represented. A daemon released: false ends ownership, and only a thrown release keeps the lease owned. That also stops an already-expired lease from aborting the rebind or being reported as LEASE_RELEASE_FAILED (connection-lease-transaction.ts:63-66).

Not blocking: inFlight and its two refusals (connection-lease-transaction.ts:80 and :98) are dead, because every ledger.end call in proxy-lease-device.ts:138/163/183 is awaited and attempt() never throws. Dropping them is optional, and the single slot above removes them anyway. The two narrowConnectionPlatform blocks at transaction:187-200 and :233-246 are a nit, and the pointer write at remote-connection-state.ts:261-262 could move before the transfer. Take or leave these.

Open threads: the two P1 threads on the rebind release still apply (#4135 (comment), #4135 (comment)). The P2 threads on the reversed created doc (#4135 (comment)), the lease test's explicit-id path (#4135 (comment)) and the duplicate rebind report (#4135 (comment)) also apply. Five lower-priority threads still apply. These threads do not apply and can be resolved: fixed, the release goes through the ledger: #4135 (comment); fixed, released: false now reports: #4135 (comment); code moved unchanged from main: #4135 (comment); code moved unchanged from main: #4135 (comment); outdated, the release mirrors the allocate request: #4135 (comment); fixed, the lease is tracked before binding: #4135 (comment); outdated, Metro cleanup still stops on write failure: #4135 (comment); intended shape for leaseRollback details: #4135 (comment); a created lease has a fresh id: #4135 (comment); outdated, that helper was deleted: #4135 (comment); a crash loses the in-memory ledger either way: #4135 (comment); macos-app throws before any allocate: #4135 (comment); created leases never share an id: #4135 (comment); intended shape for leaseRollback details: #4135 (comment); nextFlags is the returned flags contract: #4135 (comment); covered by the rebind case: #4135 (comment).

The only check not passing is the review gate, which reflects the open threads above, including the one on lease-client.ts:96. No test or smoke job is failing. The next step before merge is the two fixes above, then a push so the open threads can be re-checked.

I did not run the regression tests. I judged they fail before the change and pass at 3df3025 by reading the code. The check:affected receipt and the Limrun run (lease_allocate then lease_release, instance terminated) are your reports, and I could not reproduce them. I could not tell whether a spec-honouring backend in front of the daemon forwards reused, so the CONNECTION_DEVICE_CONFLICT impact depends on it, while the older-daemon impact does not.

@thymikee

thymikee commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

Both round-2 findings accepted and fixed in cffa60dbad (new head). Minimal change: one production guard, its doc comment, one docs paragraph, and two regression tests.

1. P2 — fallback abandoned a lease it could not retire. ACCEPTED. bindLeaseNamedDevice now ends the keyless lease through retireKeylessLeaseBeforeFallback and requires a confirmed release before the local-pick fallback allocates a replacement. An unconfirmed retirement fails the command with COMMAND_FAILED / reason: PROXY_LEASE_RETIREMENT_FAILED, naming the session, leaseId, released: false, and releaseError. The ledger keeps that lease, so the command's failure path retries the release and attaches it to the refusal as details.leaseRollback (reason: LEASE_RELEASE_FAILED). No path returns success with an unrecorded lease still owned.

Tests (remote-connection-proxy-allocate-by-spec.test.ts, both failing at 3df3025c with "Missing expected rejection" — your reproduction):

  • throwing release → 1 allocation (never a second), release attempted twice (['lease-1','lease-1']: the fallback's, then the failure path's), refusal carries the rollback evidence, record holds no leaseId;
  • refused release ({ released: false }) → same stop, no replacement allocation.

The existing success-path test still proves a confirmed retirement enables the fallback (2 allocations, lease-1 released, lease-2 persisted).

2. P2 — published Lease.created doc was reversed. ACCEPTED. packages/contracts/src/client-lease.ts now states true = the daemon created the lease for this request, false = an existing lease was handed back, absent = not an allocation or a daemon older than the field — matching normalizeLease() (reused: false → created: true), and it says release on created === true and nothing else. Verified corrected in the built packages/contracts/dist-types/client-lease.d.ts.

Owning user docs at website/docs/docs/client-api.md:347 now cover the field with the lease result: which answer you get from allocate() only, that heartbeat() returns no answer, and the rule — release only when created is true, because a lease you did not create belongs to whoever did, and a missing answer is not permission to release one. No CLI flag or help text changed.

Verification at head cffa60dbada966f69d5cd42529e124e364445491:

  • pnpm check:affected --run --head cffa60dbad… — all runnable checks passed, exactCommit=true, receipt .tmp/validation/run-xVpA7c/receipt.json.
  • format, lint, typecheck, build, fallow (0 findings in changed files), production-exports, layering, bundle-owner-files, package, doc prose and doc links — all green; targeted suites (rollback, both proxy-spec files, remote-connection.test.ts) 78 passed.
  • No new live run: the change is confined to the proxy local-pick fallback and docs. The Limrun release path is unchanged from 3df3025ce7, where it was verified live (allocate → ledger release, provider instance terminated seconds after creation, no leak).

@thymikee

thymikee commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

Coordinator check of round 3 (3df3025c..cffa60db), in place of a third full review per the round cap.

  • The fallback in bindLeaseNamedDevice now runs only after the keyless lease is confirmed released. If that release fails, the command fails with COMMAND_FAILED and details.reason: 'PROXY_LEASE_RETIREMENT_FAILED', which matches ADR 0010. The ledger still owns the lease, so the failure path retries the release and reports leaseRollback. Nothing is abandoned.
  • The Lease.created doc comment now has the right polarity and says that an omitted answer never permits release. client-api.md documents the field in user terms.

No blocking findings. All findings from rounds 1–2 are resolved. The wire addition was already judged backward compatible in round 2.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 4 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread website/docs/docs/client-api.md Resolved

@apex-sentinel-staging apex-sentinel-staging Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apex Sentinel: 6 new findings on cffa60d. The verdict and the full list are in the summary comment.

Comment thread packages/client/src/lease-client.ts Resolved
Comment thread packages/client/src/lease-client.ts Resolved
@thymikee

thymikee commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

This is a follow-up on the earlier review (#4135 (comment)). The lease release now goes through the ledger, the rebind no longer allocates a replacement while the first lease is unended, and the created polarity in the client docs is fixed. At cffa60d one defect remains, and the checks show 21 passing and 0 failing, with no conflicts.

connection-lease-transaction.ts:450 returns acquired: lease.created === true. reused is new on the wire in this PR, so any daemon deployed before it, including every daemon that predates device picking, sends no created, and every fresh allocate becomes acquired: false. bindLeaseNamedDevice then treats the new lease as renewed. A keyless lease from such a daemon throws LEASE_SCOPE_MISMATCH at proxy-lease-device.ts:162 instead of using the documented local-pick fallback. A spec request that the old daemon grants a device skips the refusal at :130, reaches assertLeaseSpecMatchesRequest with an empty bound and throws CONNECTION_DEVICE_CONFLICT, and leaseDeviceSpec is never recorded. On main acquired was true for every allocate response, so open and connect fail with a new CLI and an older remote daemon where main succeeded. The tests for the pre-pick fallback in remote-connection-proxy-allocate-by-spec.test.ts (:186, :220, :262) inject created: true through grantedLease, but a daemon that predates picking can never send that, so the real old-daemon route is untested. The rule should be that acquired means "returned by this command's allocate" and stays true for every allocate response, while ownership (ledger.track) is decided only at the allocate site from created === true. Please return { lease, acquired: true } there, keep the track guard at :446-449, and add one regression where allocate omits created (a pre-pick keyless lease and a by-spec granted lease) and assert that the fallback and the binding succeed and nothing is released.

Once that is done, could the ledger be a single slot, { leaseId, release } | undefined, filled at the allocate site when created === true? The fallback now refuses to allocate while the first lease is unended, so at most one lease is ever owned. A thrown release would keep the slot and abort, while released:false or released:true would empty it, since the daemon's released:false means "no such lease" (lease-registry.ts:256-259). That would remove retireKeylessLeaseBeforeFallback (proxy-lease-device.ts:178-196), inFlight, additionalLeaseIds and the success-path leak, and it would settle r4435499738 and r4435319144. This was asked in the earlier review and is still unanswered.

On the other threads, these still apply: the released:false abort (#4135 (comment)), the missing omitted-created test (#4135 (comment)), the older-daemon rollback note (#4135 (comment)), the same-leaseId test gap (#4135 (comment)), the refused-release test that asserts no release calls (#4135 (comment)), and the released:false ownership thread (#4135 (comment)). Three lower-priority threads also apply: the duplicate narrowConnectionPlatform blocks, the nested ternary at lease-client.ts:99, and a docs note on omitted created in client-api.md:347. These do not apply and can be resolved:

I did not run any tests, and I judged the old regression tests by reading the code against them. I could not reproduce the author's check:affected receipt or the Limrun live run. I could not tell whether third-party proxies in front of a daemon forward reused, but the defect above holds for any daemon that predates this PR. I also could not tell how often a fresh keyless proxy lease is already gone from the registry at retire time, which bounds how often the released:false abort occurs. Before merge, please restore acquired at :450, add the omitted-created regression, and answer the one-slot question.

@thymikee

thymikee commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

Addressed the 2026-11-01 12:40 maintainer review in 4472e0782c4fdc33b7484b40cf00f74efff5a6eb (parent cffa60dbada966f69d5cd42529e124e364445491). This round changes 9 files: production +48/-138, tests +163/-31, docs +2/-2. No rebase was needed; GitHub reported MERGEABLE.

  1. Older-daemon binding: every allocate response again returns acquired: true; only created === true fills rollback ownership at the allocate site. Added pre-pick keyless and by-spec granted leases that omit created; fallback/binding succeed, the granted spec is recorded, and neither route releases anything.
  2. One-slot ledger: adopted { leaseId, release } | undefined. A resolved release clears the slot for both released:true and released:false (no such lease). A thrown release retains ownership and aborts fallback; outer rollback retries and attaches typed evidence if that also throws. Removed the map, inFlight, additionalLeaseIds, and retireKeylessLeaseBeforeFallback. Identity refusal preserves its primary error and leaves cleanup to outer rollback. No replaced lease remains owned after successful fallback.
  3. r4435499738 / r4435319144 / r4435482284: an already-absent keyless lease now succeeds with exactly one recorded release before allocating the replacement. An absent lease during Metro rollback adds no false leaseRollback failure. The thrown-release test asserts COMMAND_FAILED, two attempts for the same lease, typed rollback evidence, no replacement, and no persisted lease. The former “refused” boolean test encoded the wrong daemon contract and was replaced.
  4. r4435499746 / r4435319129: added allocate-with-omitted-created plus Metro-failure coverage: no release, no invented ownership, original failure preserved. Client/proxy docs explicitly state the older-daemon limitation: an unrecorded lease remains until expiry. Binding still works.
  5. r4435314003: the second handler allocation omits leaseId, proving run-binding reuse rather than explicit-id retry.
  6. Lower-priority threads: both platform-narrowing sites call one decision body; the client uses a boolean check and !data.reused; client docs distinguish omitted ownership evidence from an unanswered RPC. Replied to and resolved all nine applicable threads and the 25 listed non-applying/already-fixed threads, with individual reasons.

Regression evidence: before production edits, the same focused Vitest command against production at cffa60db produced 4 failed / 56 passed: keyless omission hit LEASE_SCOPE_MISMATCH, by-spec omission hit CONNECTION_DEVICE_CONFLICT, absent-keyless release aborted fallback, and absent-lease rollback attached false failure evidence. After the fix: 60 passed across the four files below. The old-daemon Metro test and corrected run-binding handler test also pass.

pnpm exec vitest run --project unit-core \
  src/__tests__/remote-connection-proxy-allocate-by-spec.test.ts \
  src/cli/commands/__tests__/proxy-lease-device-spec.test.ts \
  src/cli/commands/connection-runtime-lease-rollback.test.ts \
  packages/daemon/src/sessions/handlers/__tests__/lease.test.ts

Live Limrun: loaded only LIMRUN_API_KEY with the requested extraction command; used this worktree's node bin/agent-device.mjs and fresh state /private/tmp/pr4135-limrun.Zv6XZ1. connect limrun --platform android --session pr4135-rollback --state-dir … --json verified credentials without allocation. Set that isolated generated profile's metroProjectRoot to its nonexistent missing-metro-project and metroPublicBaseUrl to https://metro.invalid, then refreshed it with connect --remote-config <generated-profile> --force (an initial open correctly refused the changed profile). open --session pr4135-rollback --state-dir … --json exited 1 with the intended missing-package.json Metro preparation error. Session events show lease_allocate ok (21:13:07.097Z) → lease_release ok (21:13:07.238Z). Limrun API metadata matches lease ff3f95ec52851504e817706e40a1afdc, run cli-mvybtiet, instance android_euna_01m6cfayamfbv8evtry0dze6sm, state terminated, terminatedAt=2026-11-01T21:13:07.293641Z. The stored connection has no leaseId. daemon stop --state-dir … --json returned stopped:true, cleanupConfidence:known, providerReleases completed with no pending entries or warnings.

Validation: pnpm format, build, and pnpm check:affected --run --head 4472e0782c passed: 41 runnable checks, 25 explicitly skipped; exactCommit=true, checkoutChanged=false. Receipt: .tmp/validation/run-uJtXTv/receipt.json. Hosted coverage and device smoke remain CI obligations at this head.

Cold self-review found no additional issue. This implementation sub-agent did not start an independent reviewer; the coordinator owns that review. Not merged; labels unchanged.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 9 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

@thymikee

thymikee commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

Addressed r4437455877 in c36e0f1e21ca5e66eac07ad8e1400fd73cc7178c: the omitted-created fallback regression now checks both returned and persisted deviceKey against ios:mobile:SIM-LOCAL. Test-only change: one file, +4/-1.

Mutation proof: clearing the materialized binding fails the new assertion (undefined vs expected device); mutation reverted byte-for-byte. Restored test file: 10 passed. pnpm format and pnpm check:affected --run --head HEAD passed (41 checks, 25 skipped), exactCommit=true, checkoutChanged=false; receipt .tmp/validation/run-g9CHkZ/receipt.json. Hosted CI remains pending.

@apex-sentinel-staging apex-sentinel-staging Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apex Sentinel: 7 new findings on c36e0f1. The verdict and the full list are in the summary comment.

Comment thread src/cli/commands/proxy-lease-device.ts Resolved
Comment thread src/cli/commands/connection-runtime.ts Resolved
@thymikee

thymikee commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

Thanks for the update. The earlier findings from cffa60d are fixed, but c36e0f1 still has two cases where the CLI abandons a lease it allocated itself. The code change needs another pass. Smoke Tests is still running with no failure recorded, and I expect no overlap because the change only touches the remote lease path. There are no conflicts.

The refusal paths now skip the release when created is omitted (proxy-lease-device.ts:180). Main released every lease its own allocate returned and then discarded: the unhonoured-spec refusal and the identity-conflict refusal. At head both go through leaseLedger.end, which returns { released: false } without calling release when the slot is empty (connection-lease-transaction.ts:40). The slot is empty whenever created is omitted, which covers daemons before this PR and any proxy service that does not send reused. For example, open --os-version >=26 against a service that ignores the spec returns a lease with no lease.device and no reused. The refusal then attempts no release but says "releasing the lease failed, so it may stay held until it expires". The identity-conflict throw at :135 leaves a new lease for the wrong device held in the same way. Nothing records either lease, so disconnect cannot reach it, and remote-proxy.md:78 still says the CLI releases it. The rule should be that a lease this command's allocate returned and this command discards (spec refusal, identity refusal, keyless replacement) is ended unless the daemon said created:false. One discard path in the transaction module that takes the lease can enforce that, and failure rollback can stay gated on created === true. Please add one omitted-created regression per refusal, since grantedLease defaults to created: true today. If you decide not to end leases of unknown ownership, each site should say "not attempted" instead of "failed", and remote-proxy.md and client-api.md should list these sites under the older-daemon limitation.

The keyless fallback has the same gap (proxy-lease-device.ts:158). Against a pre-#3487 daemon, which also omits reused, lease-1 is untracked, so await leaseLedger.end(lease.leaseId) does nothing and its result is ignored. The fallback then allocates by deviceKey, and the daemon binds that under a different run-binding key (deviceKey ?? '*' in lease-registry-scope.ts:341), so it mints lease-2. Lease-1 stays live and no record names it, where main released it. Every open --device <name> through such a proxy then leaves one orphan lease that counts against backend capacity until its TTL lapses. The test "a pre-pick daemon omitting created falls back locally without releasing either allocation" (remote-connection-proxy-allocate-by-spec.test.ts:220) asserts releases: [], so it encodes the orphan. My earlier ask for "nothing is released" there was wrong for the discarded keyless lease, and only the successful binding should be kept. Please end the keyless lease through the same discard path before the replacement is allocated, or refuse the fallback. The test should then expect releases to equal ['lease-1'] and still assert that lease-2 is bound and persisted.

Not blocking, and fine to take or leave: when the keyless retirement release throws, the raw provider error is now the primary error and leaseRollback repeats it, so the typed PROXY_LEASE_RETIREMENT_FAILED context that said why the fallback stopped is gone. Keeping the release error only in leaseRollback, under a primary error that names the stopped fallback, would avoid the duplication.

The open inline thread on the pre-pick test (#4135 (comment)) is fixed at head and can be resolved. The thread on the spec-refusal release error showing up twice (#4135 (comment)) still applies as a low-priority note. These do not apply and can be resolved: the retry after a thrown release (#4135 (comment)) keeps the slot by design, so it is not a double release; attaching leaseRollback to the primary error (#4135 (comment)) is the intended shape; track runs only at allocate (#4135 (comment)) and the one second allocate follows end(); the shared constant import in the rollback test (#4135 (comment)) is a nit at most; the Metro cleanup split (#4135 (comment)) is a mechanical move of main's code; and a created lease always carries a fresh id (#4135 (comment)), so the write transfers it.

I read the head code and main's release sites but did not run tests, the mutation proof, the Limrun run, a pre-#3487 daemon or a third-party proxy service. I could not tell whether production proxy services forward reused, but the first finding applies to any that do not. Before merge, the lease should be ended at each discard site (spec refusal, identity refusal, keyless fallback) whenever created is not false, or the non-release should be reported and documented truthfully. The pre-pick test also needs to stop asserting the orphaned lease-1.

@thymikee

thymikee commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

Settled the seven Apex threads in 8ee729017c8863d052e3771c2a48ac7a956c16f9 (parent c36e0f1e21). This round: 5 files; production +12/-17, tests +59/-11, docs +1/-1.

  • r4437614604: retained the intentional single retry after a thrown spec release. Tests now pin exactly two attempts for both permanent failure and second-attempt recovery.
  • r4437614607: removed the spec path's flat failure fields. Outer rollback alone writes details.leaseRollback with reason LEASE_RELEASE_FAILED; a recovered retry leaves no stale failure evidence. Completed local release results retain details.released. Docs updated.
  • r4437614633: no supported success path retains ownership: created allocations have fresh IDs and force changed=true; the record commit transfers that ID before success. Heartbeat/run-binding reuse is not tracked. Fallback ends its predecessor before another allocation. A detailed producer-to-commit proof is in the thread; no speculative success drain added.
  • r4437614620 / r4437614624: documented track()'s one-slot precondition; made the reason constant private, with tests asserting the wire literal independently.
  • r4437614614 / r4437614626: retained the maintainer's primary-error identity contract and main's unchanged best-effort Metro cleanup behavior, with reasons in each thread.

Regression proof: the spec suite at parent production had 2 failed / 13 passed, detecting duplicate flat evidence and stale failure evidence after recovery. The same spec command now has 15 passed. Fixed focused suites: 33 passed across spec refusal, general rollback, and keyless fallback. The release sequence is unchanged; this round changes error evidence only. Cold self-review found no additional issue; coordinator review remains separate.

pnpm format and pnpm check:affected --run --head HEAD passed (41 runnable checks, 25 explicitly skipped), exactCommit=true, checkoutChanged=false; receipt .tmp/validation/run-YCAgiS/receipt.json. Hosted coverage/device smoke and current-head review remain pending.

@thymikee

thymikee commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

This follow-up covers 8ee7290. The new commit fixes the false "releasing the lease failed" text and the open threads below, but the two release findings from the earlier review (#4135 (comment)) are still open.

First, the spec refusal and the identity refusal still leave the lease held (proxy-lease-device.ts:178). The ledger tracks a lease only when created === true, and the client sets created only when the allocate reply carries reused. A daemon or proxy service that predates reused yields an untracked lease, so leaseLedger.end() returns {released:false} without calling release. In open --os-version >=26, the service ignores the spec and refuseUnhonouredSpecLease releases nothing. The error now says released:false, but that looks the same as "the daemon had no such lease". In the identity refusal, a spec plus --udid grant for another device throws at :135 and releaseOwnedOnFailure finds an empty slot. Main released the lease at both sites. The spec refusal fires only for a backend that did not understand the spec, and such a backend most likely also lacks reused, so the release is skipped for most backends that reach it. A refused lease that no record names stays held until its TTL lapses, and disconnect cannot reach it. remote-proxy.md:78 still says "The CLI releases that lease", and its older-daemon note covers only failed preparation. The rule should be: a lease that this command's allocate returned and then discards without recording (spec refusal, identity refusal, keyless replacement) ends unless the daemon said created:false. Rollback of a bound lease after a later preparation failure stays gated on created === true. The ledger owns this: track every acquired lease whose created is not false, with a rollbackOnFailure: created === true bit. end() releases whatever is tracked, and releaseOwnedOnFailure releases only when the bit is set. Please add one omitted-created regression per refusal that asserts the release and the refusal code, because grantedLease defaults to created:true today. If you keep the no-release rule instead, omit released when no release was attempted and list both refusals under the older-daemon limitation in remote-proxy.md and client-api.md.

Second, the keyless replacement leaks the first lease (proxy-lease-device.ts:158). A pre-#3487 daemon never picks a device and never sends reused, so its keyless lease-1 is untracked and await leaseLedger.end(lease.leaseId) is a no-op whose result is ignored. allocateByDeviceKey then mints lease-2 under a different run-binding key, and only lease-2 is persisted. Main released lease-1 here. The test "a pre-pick daemon omitting created falls back locally without releasing either allocation" (remote-connection-proxy-allocate-by-spec.test.ts:220-253) asserts releases: [] at :251, so it encodes the orphan. The keyless-release tests pair a keyless lease with created:true, so the release at :158 has no test on the route that reaches it. Every open --device <name> through a proxy backed by a pre-pick daemon leaves one unnamed lease that holds backend capacity until its TTL lapses. Please apply the same discard rule: end the replaced keyless lease before allocating the replacement whenever created is not false, or refuse the fallback. Then change that test to expect releases == ['lease-1'] and keep its lease-2 binding and persistence assertions.

Not blocking: if the first release throws and the rollback retry succeeds, the refusal carries neither released nor leaseRollback (proxy-lease-device.ts:188), so the user cannot tell the lease ended. releaseOwnedOnFailure could record released:true on a successful retry, or you can leave it. Take or leave this.

Hosted CI is green, with 20 checks and none failing at 8ee7290. There are no conflicts. I did not run the focused Vitest suites, a mutation, or a pre-#3487 daemon. I judged the two new spec tests by reading the parent code. I found no reply that accepts or rebuts the two earlier findings, so I treat them as unaddressed. Before merge, both findings need the fix above or the documented non-release, plus the regression tests.

On the inline threads: the Apex thread about the second release attempt (#4135 (comment)), the releaseOwnedOnFailure error annotation (#4135 (comment)), the track overwrite (#4135 (comment)), the Metro cleanup (#4135 (comment)) and the created-lease ownership transfer (#4135 (comment)) do not apply: each is benign as written, and they are already resolved. The releaseError thread (#4135 (comment)), the constant visibility thread (#4135 (comment)) and the deviceKey assertion thread (#4135 (comment)) are fixed at 8ee7290, and they are already resolved too.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant