You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
agent-device front ignores endedLeases, so close no longer frees the device #607
Found by the slow lane on PR #606 (stack #600). The stack does not change the code involved, so main has this bug too since #603.
What happens
e2e/slow-agent-device.test.ts, the iOS test that ends with close, then waits for the device to be free. close returns 200 and the agent-device session ends. But the simlock lease stays leased, and no release reaches simlock. The device is freed only when the 300 s lease TTL runs out, too late for the test's 330 s wait.
Why
agent-device 0.21.28 (callstack/agent-device#3477) no longer sends lease_release after close. Instead, the daemon's command reply carries endedLeases: [{ leaseId, outcome }]: in data on success, in error.details on failure, and in data next to results for a batch. The gateway in front of the daemon is expected to end its own lease from it. simlock's agent-device front never reads endedLeases (grep -rn endedLeases src is empty). It releases only on an explicit lease.release RPC (src/agent-device/front/release.ts).
Likely fix
In the front's command relay, read endedLeases from all three places. For each lease this token holds, release it in simlock and mark it ended by the agent. The fake in src/agent-device/testing.ts needs to return endedLeases on close.
Defect, confirmed as reported. On bug/607-repro, src/agent-device/edge.test.ts, a table of three cases: "%s: a command that ends the agent-device lease and names it in endedLeases releases the simlock lease and frees the device though the CLI sends no release, and a later heartbeat answers LEASE_NOT_FOUND". The cases are close (in data), a failed close (in error.details), and a batch with a close step (beside results). FakeAgentDevice now does what 0.21.28 does: it ends the lease on close and reports it.
pnpm exec vitest run --project unit src/agent-device/edge.test.ts -t endedLeases
× all 3 cases
AssertionError: expected [ '00000000000000000000000000000001' ] to deeply equal []
❯ src/agent-device/edge.test.ts:1256 await expect.poll(async () => listed(rig)).toEqual([]);
The other 209 tests in that file and the gateway, lease-end and main tests still pass with the changed fake.
Root cause
src/agent-device/front/front.ts:150 sends every command through runCommand, which hands back agent-device's reply unread (:169). The only code that frees the simlock lease when the agent ends a lease is release.ts:24, and it runs only on an explicit lease.release RPC. agent-device 0.21.28 no longer sends that RPC after close; it reports the end in endedLeases instead (its client-types.d.ts, EndedLease). So the lease stays until its TTL runs out. docs/HTTP-API.md:1580 promises that close frees it.
Simplest fix
Move release.ts:17-28 (map entry check, releaseSimlock, mark ended by agent, cancel renewals) into one function. In runCommand, read the reply and collect endedLeases from result.data and from error.data.details. Call that function for each id the map holds for this token. Gateway and worker share this front, so both paths are covered.
Alternatives rejected
Read it in the host's recordingFetch: the host has no front map, and a gateway's map would stay live.
A streamed (ndjson) reply has its result in the last frame. Reading it must not hold back the stream (untilBodyEnds).
endedLeases is wire input. Act only on leases the map holds for this token.
The release scope stays recorded, so lease-end sends agent-device a second release. Check that it answers released: false and does not delay the reclaim, or drop the scope.
Activity
V3RON commentedon Oct 15, 2026
Reproduction
Defect, confirmed as reported. On
bug/607-repro,src/agent-device/edge.test.ts, a table of three cases: "%s: a command that ends the agent-device lease and names it inendedLeasesreleases the simlock lease and frees the device though the CLI sends no release, and a later heartbeat answersLEASE_NOT_FOUND". The cases areclose(indata), a failedclose(inerror.details), and abatchwith a close step (besideresults).FakeAgentDevicenow does what 0.21.28 does: it ends the lease on close and reports it.The other 209 tests in that file and the gateway, lease-end and main tests still pass with the changed fake.
Root cause
src/agent-device/front/front.ts:150sends every command throughrunCommand, which hands back agent-device's reply unread (:169). The only code that frees the simlock lease when the agent ends a lease isrelease.ts:24, and it runs only on an explicitlease.releaseRPC. agent-device 0.21.28 no longer sends that RPC afterclose; it reports the end inendedLeasesinstead (itsclient-types.d.ts,EndedLease). So the lease stays until its TTL runs out.docs/HTTP-API.md:1580promises thatclosefrees it.Simplest fix
Move
release.ts:17-28(map entry check,releaseSimlock, mark ended byagent, cancel renewals) into one function. InrunCommand, read the reply and collectendedLeasesfromresult.dataand fromerror.data.details. Call that function for each id the map holds for this token. Gateway and worker share this front, so both paths are covered.Alternatives rejected
recordingFetch: the host has no front map, and a gateway's map would stay live.lease_releaseagain: chore(agent-device): require agent-device 0.21.28 and take its proxy from npm #603 adopted 0.21.28 on purpose.Risk
untilBodyEnds).endedLeasesis wire input. Act only on leases the map holds for this token.released: falseand does not delay the reclaim, or drop the scope.Written by an agent.