Skip to content

fix(daemon): keep an idle daemon alive only for retained leases - #3227

Merged
thymikee merged 2 commits into
mainfrom
claude/idle-reap-retained-leases
Oct 5, 2026
Merged

thymikee merged 2 commits into
mainfrom
claude/idle-reap-retained-leases

Conversation

@thymikee

@thymikee thymikee commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

Summary

Follow-up to #3208, which merged while this was in review. It answers the open question there: idle reap now waits only while an unexpired retainOnClose lease is active. As merged, #3208 counted every lease, so a human-control hold with no expiresAt kept an otherwise idle daemon alive, where before it was reaped. Other leases now reap as they did before #3208.

  • LeaseRegistry.hasRetainedLeases() owns the rule. Reading it also expires leases past their window, so an abandoned retained lease still ends. The daemon wiring is a one-line call.
  • ADR 0007 says that only retainOnClose leases keep an idle daemon alive.
  • A lifecycle-shutdown test restores the coverage the old beforeDelete assertions gave: a session-bound lease whose session teardown rejects is still released by the shutdown sweep.

7 files, 91 gross lines.

Validation

At e0d5f4c2d3: pnpm check:affected --run passes. A retained lease counts only while its own expiresAt is in the future, so a human-control hold cannot keep an idle daemon alive. Registry, runtime idle-reap and shutdown tests each fail under their mutation.

Review in cubic

Idle reap now waits only while an unexpired retainOnClose lease is active,
through LeaseRegistry.hasRetainedLeases, which also expires leases past their
window. Other leases, including human-control holds with no expiry, reap as
before; ADR 0007 says so. A lifecycle-shutdown test restores the coverage that
a session lease is released when its session teardown rejects.

@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 7 files

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

Re-trigger cubic

Comment thread src/daemon/lease-registry.ts Outdated
Comment thread src/daemon/__tests__/lease-registry.test.ts Outdated
@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.07 MB 5.07 MB +109 B
Package (unpacked) 5.07 MB 5.07 MB +109 B
Package (download) 1.52 MB 1.52 MB +8 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 28.9 ms 29.1 ms +0.2 ms
CLI --help 84.4 ms 84.9 ms +0.5 ms

@thymikee

thymikee commented Oct 5, 2026

Copy link
Copy Markdown
Member Author

The retained-lease idle check in 887243c needs one more change before merge: a human-control hold can still keep the daemon alive. hasRetainedLeases counts every lease that listActiveLeases keeps, and isLeaseProtected keeps a retained lease past its expiresAt while hasHumanControl is true (https://github.com/callstack/agent-device/blob/887243c/src/daemon/lease-registry.ts#L413-L419). A hold with no ttlMs never expires, so an idle daemon would never exit. The rule is that only retainOnClose leases whose own expiresAt is still in the future may keep the daemon alive. Please apply it in hasRetainedLeases and add a registry test with a hold on the retained lease's device. I did not confirm this on a live Limrun or proxy setup, but the registry code allows it.

The cubic-dev-ai P2 thread on the hold case (#3227 (comment)) and the P3 thread on the expiry-on-read test (#3227 (comment)) both still apply. If you filter by expiresAt, the doc-comment claim goes away and the P3 thread is moot.

Not blocking, and fine to take or leave: the idle-reap tests stub hasRetainedLeases and allocate no lease, so reverting the wiring to listActiveLeases().length > 0 would fail no test. One case in daemon-runtime-idle-reap.test.ts would cover it: allocate a plain lease on the runtime's registry and assert the daemon still exits after the idle window. The session-lease shutdown test also restores coverage that #3208 dropped, which is fine to keep since the PR body already says so.

I did not run any tests. CI was still running on 887243c with no failures, and I see no overlap between the smoke routes and this change. No conflicts. Once the P2 fix lands and checks pass, it can go to human review.

hasRetainedLeases counts a retainOnClose lease only while its own expiresAt
is in the future, so a human-control hold (or admitted work) that keeps a
past-due lease registered cannot keep an idle daemon alive forever. Tests: a
registry case with a hold on the retained lease's device, and a runtime case
where a plain lease does not block self-reap but a retained one does.
@thymikee

thymikee commented Oct 5, 2026

Copy link
Copy Markdown
Member Author

Addressed at e0d5f4c. pnpm check:affected --run passes.

  • Hold case (the blocker): hasRetainedLeases counts a retainOnClose lease only while its own expiresAt is in the future. A human-control hold or admitted work can keep a past-due lease registered, but no longer keeps an idle daemon alive. A registry test puts a no-ttl hold on the retained lease's device, and it fails without the bound.
  • Cubic threads: both are answered and resolved. The P3 is moot because the doc comment no longer claims expiry-on-read.
  • Runtime wiring (optional): a new test in daemon-runtime-idle-reap.test.ts starts the real runtime with an 80 ms idle window. A plain lease doesn't stop the self-reap; a retained lease holds the daemon until it's released. Reverting the wiring to listActiveLeases().length > 0 fails it.

@thymikee

thymikee commented Oct 5, 2026

Copy link
Copy Markdown
Member Author

This PR is ready. The two earlier threads are fixed at e0d5f4c: only a retained lease still inside its expiresAt now defers the idle reap, and the expiry-on-read claim and the masking assertion are gone. Both new tests cover the past-due case, though I did not run them or pnpm check:affected, and I did not try a held lease on a live Limrun or proxy setup. The change only touches the daemon idle-reap predicate, so no device-facing path moves.

Not blocking: the doc on the hasRetainedLeases param at https://github.com/callstack/agent-device/blob/e0d5f4c/src/daemon/server/daemon-idle-reap.ts#L88 still says the check must expire leases past their window, so please reword it to say that only a retainOnClose lease inside its own expiresAt defers the reap, or leave it as is.

The two cubic-dev-ai threads are fixed at this commit, so you can resolve them: the past-due retained lease now stops deferring the reap (#3227 (comment)), and the expiry-on-read claim is gone (#3227 (comment)).

No conflicts. The two Smoke Tests jobs are still queued or running, and no failure has shown up. The diff does not overlap with what they exercise, since they use the default 5-minute idle window and create no retainOnClose leases. Once they finish green, this is ready for human review.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Oct 5, 2026
@thymikee
thymikee merged commit 3f3594e into main Oct 5, 2026
19 checks passed
@thymikee
thymikee deleted the claude/idle-reap-retained-leases branch October 5, 2026 17:44
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-10-05 17:44 UTC

thymikee added a commit to okwasniewski/agent-device that referenced this pull request Oct 6, 2026
* origin/main: (77 commits)
  fix(apple-runner): fence prep spawns behind a start-owned admission (callstack#3239)
  0.21.22
  test(apple): own the simctl settings plan tests in simctl-settings.test.ts (callstack#3244)
  fix(limrun): report the session device id in iOS settings refusals (callstack#3243)
  0.21.21
  feat(remote): add a host-allocated macos-app lease backend (callstack#3236)
  test(android): bound the screenshot write wait by wall time, not event-loop turns (callstack#3250)
  feat(recording): cap the touch overlay frame rate at the caller's --fps (callstack#3241)
  fix(ad-script): let .ad scripts carry scroll --until and wait capture flags (callstack#3197) (callstack#3234)
  feat(provider-webdriver): keyboard enter, dismiss, and status over WebDriver (callstack#3233)
  feat(selectors): match role= against snapshot kind with a node-scoped alias window (callstack#3232)
  fix(provider-webdriver): read field values, placeholders, secure fields, and checked state from page source (callstack#3231)
  feat(replay): accept --test-ime on test and replay so flow-owned Android opens opt into the test IME (callstack#3235)
  refactor(daemon): route daemon-level diagnostics through one scope helper (callstack#3242)
  docs(adr): correct ADR 0031 pointer event delivery evidence (callstack#3245)
  fix(ios): stop a tap's post-gesture lookup from recording an XCTest failure (callstack#3060) (callstack#3237)
  fix(recording): render the touch overlay at most 30 fps and inside the record request (callstack#3219)
  fix(daemon): keep an idle daemon alive only for retained leases (callstack#3227)
  fix(provider-webdriver): send an empty JSON object on bodyless POSTs (callstack#3230)
  Feat/maestro repeat while (callstack#3214)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant