Skip to content

fix(daemon): preserve active holds when renewal is canceled - #4163

Merged
thymikee merged 1 commit into
mainfrom
fix/human-control-hold-reupsert
Nov 2, 2026
Merged

thymikee merged 1 commit into
mainfrom
fix/human-control-hold-reupsert

Conversation

@thymikee

@thymikee thymikee commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

Summary

An aborted renewal could delete an active human-control hold and reopen mutation admission. Keep the active entry until renewal activates atomically; cancellation preserves its metadata and TTL.

Two files; production changes stay inside LeaseRegistry. FU-HOLD audit follow-up. No scope expansion. Validation blocker: #4060.

Validation

  • Base: a0e7c7dc239922bf93adaceff20a2f7e78579146.
  • Tested head: 6f028bf6c305aa25288dce7b161f0884fa2b2a6f.
  • pnpm exec vitest run --project unit-core packages/daemon/src/resources/__tests__/lease-registry-human-control.test.ts: added before implementation; base 1 failed (hold list became empty); head 1 passed. Registry/expiry/HTTP focused suites: 52 passed.
  • pnpm format and pnpm build: passed.
  • pnpm check:affected --run --head 6f028bf6c305aa25288dce7b161f0884fa2b2a6f: base-blocked, receipt .tmp/validation/run-nlfJ5l/receipt.json, exactCommit=true, checkoutChanged=false. Six checks passed; related tests 303 passed / 1 failed. Identical command on freshly installed/built unchanged base reproduced apple-platform-output-guard.test.ts:385, record timeout after 20s, 303 passed / 1 failed (test(provider): fix recording timeout reproduced by the related-test gate on unchanged base #4060). pnpm gate unit: passed, 16,966 tests passed / 1 skipped; smoke 76 passed / 6 skipped.
  • Live iOS 26.2, local / CAE86E90-9D1D-42A4-B020-179F0FE092DE: worktree-built HTTP daemon, private /private/tmp/fu-hold-live state. Worktree CLI takeover renewed the same active hold; takeover status showed advanced updatedAt. Bound device client back returned DEVICE_IN_USE, reason human_control_active. After Ctrl+C, status was empty and back reached the native runner, which reported no back control on Settings' root. Sessions, hold, and daemon cleaned up. Cancellation timing is proven by the unit regression.
  • Cold self-review only; no independent reviewer. GitHub Integration Tests, Coverage, and device lanes remain CI obligations; local skips do not claim those passed.

View guided diff Turn on auto-fix

@vercel

vercel Bot commented Nov 2, 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 2, 2026 9:33am UTC

Request Review

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

No issues found across 2 files

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.

Verdict: approve. No concrete regression findings.

Reviewed head 6f028bf6c305aa25288dce7b161f0884fa2b2a6f; merge-base with origin/main: a0e7c7dc239922bf93adaceff20a2f7e78579146 (also the PR base).

The active entry remains authoritative during renewal, including its original metadata and expiration. In lease-registry.ts:395–432, the only await between selecting that entry and replacing it is mutations.wait. After it, cancellation and entry identity are checked before the synchronous active-state construction and map write. Cancellation cleanup removes only this request's published pending entry, so it cannot delete the retained active entry or a successful competing renewal.

Adversarial checks:

  • Successful renewal persists the replacement and extends expiration from 10,000 to 21,000; admission stays blocked beyond the old deadline. Before publication, admission errors expose the original active hold and TTL.
  • A non-abort validation error retains the original hold. Concurrent successful attempts produce one committed renewal and one explicit COMMAND_FAILED, without deleting the winner. Aborting either contender preserves the other renewal.
  • Release racing renewal invalidates the old entry identity, so the renewal cannot resurrect the released hold or overwrite a replacement.
  • An already-expired hold takes the fresh activating path, fences mutation admission while draining, and acquires a fresh TTL on success. Canceling that activation leaves the expired hold absent. Sweeping the old TTL during an active renewal prevents stale publication.
  • The existing tests also exercise mutation drain, activation cancellation, and superseded activation cleanup.

Design: retaining the committed entry while building one unpublished candidate is a small, sufficient state model. It avoids rollback state and keeps the registry map as the admission authority; no additional hold state is needed.

Validation: installed and built both head and an isolated unchanged base. Ran one focused Vitest invocation using scratch configuration: all 59 head-side tests passed (51 lease-registry tests plus eight adversarial scratch cases). The invocation overall failed because the base scratch test could not resolve @agent-device/kernel/errors; the base regression did not execute. No rerun was made. Static base tracing does establish that the added regression would fail at its hold-retention assertion: base publishes the replacement pending entry before the await, then cancellation deletes it, leaving []. This is static evidence, not an independently reproduced base test failure. Log: /private/tmp/r4163-tests.log.

I see no plausible connection between this human-control renewal-only diff and the reported Apple record output-guard timeout in #4060. The reported affected-gate blocker remains unverified by this review; I did not investigate it further or wait for CI. Reviewed code remained unchanged.

@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.69 MB 5.69 MB +46 B
Package (unpacked) 5.69 MB 5.69 MB +46 B
Package (download) 1.73 MB 1.73 MB +24 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 44.4 ms 43.3 ms -1.0 ms
CLI --help 96.9 ms 89.4 ms -7.5 ms

@thymikee

thymikee commented Nov 2, 2026

Copy link
Copy Markdown
Member Author

The code looks good at 6f028bf: canceling a renewal now keeps the active hold, and I found no problems in the code. All 18 checks pass. The one local failure I saw on #4060 (apple-platform-output-guard record timeout) is on a provider recording route this diff does not touch. I did not run the regression test on base or head; I read the old code path and DeviceMutationDrain.wait to confirm it fails on base. The live iOS run and the check:affected and gate results are reported by the author, not seen by me. The cancel window is a single await, so a live run cannot reach it and the unit test is the practical proof. No conflicts are known.

Not blocking, take or leave: in https://github.com/callstack/agent-device/blob/6f028bf/packages/daemon/src/resources/lease-registry.ts#L397 the renewal now publishes the existing active entry with its old expiresAt, so if that TTL runs out in the same millisecond as a sweep, the renewal fails with hold changed before activation completed where base would have renewed. If you want to close it, renew an active hold without the drain wait, since an active hold already means no mutations are admitted. The self-review also lists release-racing-renewal and concurrent-renewal checks, but only the cancel path is a committed test, so a concurrent-renewal test that asserts which caller wins and that the loser gets COMMAND_FAILED would be a nice addition.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Nov 2, 2026
@thymikee
thymikee merged commit 38fe9b9 into main Nov 2, 2026
18 checks passed
@thymikee
thymikee deleted the fix/human-control-hold-reupsert branch November 2, 2026 11:53
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