Skip to content

fix(daemon): keep a discarded rejection from shutting the daemon down - #2532

Merged
thymikee merged 1 commit into
mainfrom
fix/discarded-rejection-daemon-2519
Sep 13, 2026
Merged

fix(daemon): keep a discarded rejection from shutting the daemon down#2532
thymikee merged 1 commit into
mainfrom
fix/discarded-rejection-daemon-2519

Conversation

@thymikee

@thymikee thymikee commented Sep 12, 2026

Copy link
Copy Markdown
Member

Summary

Closes #2519.

acquireSimulatorProcess abandoned an aborted acquisition with void started.then(rollback).
localTransport.start awaits two dynamic imports and then rejects with the abort reason, so every
aborted record start delivered that rejection to the daemon's only unhandledRejection handler
(src/daemon/server/daemon-runtime.ts:644), which exits 1 and kills every open session. Both abandon
sites now end the rollback chain in a handler, which also covers a rollback that itself fails.

Swept all 24 void … .then( sites rather than adding the proposed regex guard. Three rows changed
shape:

  • Site 2 refuted. The losing delay branch is a Promise.race member, and Promise.race
    attaches a rejection handler to every member, so the post-settlement abort rejection is already
    handled. Verified in Node and by a test that passes at origin/main. No no-op handler added.
  • Site 3 unreachable, and the proposed shape was unsafe. work is renew(...), which cannot
    reject: every path ends in a return or stop(). Resolving a rejection as "renewal settled" would
    make run re-arm pending = renew(...) with no backoff, so a broken renew becomes a renewal
    spin instead of a daemon exit. A rejection now routes through the closure's own stop as
    teardown-required/authority-unconfirmed, which is what renew's catch already reports. No
    failing test exists because no reachable rejection does.
  • Site 4 is a resolve-only deferred. Deleted the discarded continuation at its construction path —
    settled now removes itself from pending — instead of adding a dead handler.

Adds packages/provider-limrun/src/request-cancellation.test.ts, which pins the drain's
abort-while-in-flight ownership contract that site 4 edits; those three tests pass before and after,
so they are contract coverage, not a regression test. 5 files.

Two review findings were rejected as unreachable rather than guarded: child.kill('SIGINT') inside
rollbackAcquiredSimulatorProcess cannot throw for a valid signal name (ChildProcess#kill returns
false for a dead child), so the SIGKILL escalation is not reachable through a thrown kill; and a
rejected start() cannot have spawned anything, because the local transport spawns synchronously
after its last throwIfAborted.

Validation

Tested at 1326c85c71. pnpm check:affected --run passed (14 checks: format, lint, typecheck,
layering, fallow; 1,684 unit tests). Both new abort tests fail at
origin/main, capturing cancel simulator transport start as an unhandledRejection, and pass at
HEAD; the grace-race assertions pass both ways, which is the site-2 refutation. The unhandled-
rejection observer now flushes with two setImmediate hops instead of a 10 ms timer, so it is immune
to fake timers. The transport is injected and no device-facing path changed, so no live run applies.
Coverage and provider integration stay CI-owned.

@github-actions

github-actions Bot commented Sep 12, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
Installed (including dependencies) 4.50 MB 4.50 MB +32 B
Package (unpacked) 4.50 MB 4.50 MB +32 B
Package (download) 1.32 MB 1.32 MB -1 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 27.5 ms 28.6 ms +1.0 ms
CLI --help 75.4 ms 77.3 ms +1.9 ms

An aborted Apple simulator recording start discarded the transport promise:
`void started.then(rollback)` has no rejection handler, and the local transport
rejects with the abort reason after its dynamic imports settle. The rejection
reached the daemon's only unhandledRejection handler, which exits with code 1
and kills every open session.

End the rollback chain in a handler at both abandon sites so a rejected
acquisition, or a rollback that itself fails, cannot escape.

Sweep the remaining single-handler `void … .then(` sites: a rejected renewal now
fences as unconfirmed authority instead of settling as a normal renewal, which
would re-arm renewal work with no backoff, and the Limrun drain drops its
discarded continuation by deleting from `pending` when the deferred settles.
@thymikee
thymikee force-pushed the fix/discarded-rejection-daemon-2519 branch from 6557208 to 1326c85 Compare September 12, 2026 18:11
@thymikee

Copy link
Copy Markdown
Member Author

No actionable findings on 1326c85. The renewal rejection path stops admission before finishing the wait, preserving normalized failure handling without a retry loop. The focused race regressions cover the changed behavior. Current checks pass and there are no conflicts. Ready for human review and merge.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Sep 12, 2026
@thymikee
thymikee merged commit b8d70aa into main Sep 13, 2026
18 checks passed
@thymikee
thymikee deleted the fix/discarded-rejection-daemon-2519 branch September 13, 2026 05:56
@github-actions

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-09-13 05:56 UTC

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.

fix(daemon): a discarded promise rejection must not shut the daemon down

1 participant