Skip to content

[codex] guard the worker against unhandled async crashes (BLO-20618) - #949

Merged
kkroo merged 13 commits into
masterfrom
kkroo/blo-20618-process-crash-guard
Aug 2, 2026
Merged

[codex] guard the worker against unhandled async crashes (BLO-20618)#949
kkroo merged 13 commits into
masterfrom
kkroo/blo-20618-process-crash-guard

Conversation

@kkroo

@kkroo kkroo commented Aug 2, 2026

Copy link
Copy Markdown

Thinking Path

  • The worker process supervises in-flight agent runs and their Kubernetes jobs, so silent worker death leaves every active run to be rediscovered later as a symptom like job_missing.
  • The app-authored implementation in fix(server): guard the worker against unhandled async crashes (BLO-20618) #925 fixed the crash breadcrumb and probe behavior, and CI went green, but Ally cannot formally approve a PR authored by app/allyblockcast.
  • This PR reopens the same reviewed change under an independent kkroo branch, merged with current master, so the normal review gate can attest the exact head.

Linked Issues or Issue Description

What Changed

  • Adds process-level crash handling for uncaught exceptions and unhandled rejections, including bounded crash bookkeeping and durable stderr breadcrumbs.
  • Installs the guard at the main server entrypoint.
  • Widens worker liveness probe tolerance for the singleton supervisor.
  • Preserves the fix(server): guard the worker against unhandled async crashes (BLO-20618) #925 graceful-shutdown stderr durability follow-up and merges current master.

Verification

  • PATH=/opt/homebrew/bin:$PATH pnpm --filter @paperclipai/server typecheck
  • PATH=/opt/homebrew/bin:$PATH pnpm exec vitest run server/src/__tests__/process-crash-guard.test.ts server/src/__tests__/process-crash-guard-exit.test.ts server/src/__tests__/graceful-shutdown-exit.test.ts server/src/__tests__/server-startup-feedback-export.test.ts — 37 passed
  • PATH=/opt/homebrew/bin:$PATH node deploy/helm/paperclip/tests/probes.test.mjs — 2 passed
  • git diff --check

Risks

  • Installing a process-level handler changes fatal-path behavior; the tests cover serialization, bounded exit behavior, re-entrancy, and child-process stderr durability.
  • Wider worker liveness delays detection of a true wedge, but avoids killing the singleton supervisor during slow but live work.
  • No schema or API migration is introduced by this replacement PR.

Model Used

  • Codex GPT-5 via Codex CLI.

Checklist

  • I have included a thinking path that traces from project context to this change
  • I have specified the model used
  • I have searched GitHub for duplicate or related PRs and linked them above
  • I have either linked existing issues or described the issue in-PR
  • I have run tests locally and they pass
  • I have added or updated tests where applicable
  • I have considered and documented risks above
  • All Paperclip CI gates are green
  • Ally review is complete for this exact head

kkroo and others added 7 commits August 1, 2026 19:21
…618)

PR A of the BLO-19722 split — the converged half of #891, carrying only
acceptance criteria 1 and 4. The crash-time run marking (AC 2/3) and the
postgres driver patch (AC 5) stay on BLO-19722.

Death path 1 — an unhandled async throw killed the worker with a bare
stack. `paperclip-0` terminated `exitCode: 1` on 2026-07-31 from a
TypeError raised inside a postgres.js `setImmediate` callback
(porsager/postgres#1154), which no `try`/`catch` of ours can intercept.
There was no `process.on("uncaughtException")` anywhere in `server/src`,
so one transient null socket took down the supervisor for every run it
was tracking.

`process-crash-guard.ts` installs both handlers, serialises the full
`.cause` chain (hardened against throwing getters, self-referential
causes and non-Error throwables), writes a synchronous stderr breadcrumb
so the reason survives into `kubectl logs --previous`, then exits
non-zero deliberately. Crash bookkeeping is optional, raced against a
timeout and re-entrancy guarded, so a crash while handling a crash exits
immediately rather than hanging.

It is installed under `isMainModule`, not inside `startServer()`.
`startServer()` is exported and called 16 times in-process by
`server-startup-feedback-export.test.ts`, so installing there both leaks
a listener pair per call and arms a real `process.exit(1)` inside the
test runner, where one late-handled rejection would kill the worker
mid-suite as an unrelated flake. The entrypoint also covers strictly
more of startup.

Death path 2 — `timeoutSeconds: 5 / failureThreshold: 3` let a ~90s
event-loop stall convince kubelet the worker was dead. `/healthz` is
declared by no handler in `server/src`; it falls through to the SPA
catch-all, so it measures only "can the event loop answer right now" and
cannot distinguish a busy heartbeat pass from a wedged process. An API
replica is stateless and cheap to kill early; the worker is a singleton
whose false kill orphans every in-flight run. Widened to 10s/6 (~180s)
with that rationale recorded inline.

The guard imports only `shutdown-log.js` — no edge to the heartbeat
service; crash-time marking arrives later as an injected `onCrash`.
…20618)

Ally's round-6 review is correct, and the root cause is older than this PR:
`shutdown-log.ts` documented the exact opposite of Node's stream semantics.

Node's writes are synchronous for files and POSIX TTYs but ASYNCHRONOUS for
pipes and sockets on POSIX. Container stderr under kubelet is a pipe, so the
module header's claim — "when stderr is piped … libuv issues the write
synchronously" — named the one case that is actually async. `process.exit()`
then discards pending stdout/stderr I/O, so the crash guard's breadcrumbs
could die with the process while having already suppressed Node's default
crash printer: precisely the undiagnosable failure the guard exists to stop.

Why it survived two review rounds: libuv tries an eager `uv_try_write`, so a
short line into an empty pipe does land. Only once the pipe backs up — a
crashing worker spewing a stack — is the remainder queued and dropped.
Measured on Node v24.16.0 with stderr piped: of ~200 KB across three
breadcrumbs, the first landed and the trailing `exiting 1 after
uncaughtException` line was lost (146,176 of 200,092 bytes captured).

Breadcrumbs now go through `fs.writeSync` on the raw stderr fd, looping
partial writes and retrying EAGAIN/EINTR (stderr can be non-blocking) under a
100ms deadline. The deadline is deliberate: this is the fatal path, and a
breadcrumb that blocked forever on a full pipe would wedge the crash handler,
turning a fast crash-and-restart into a silent hang — worse than the bug.
On give-up it falls back to the async stream, no worse than before.

Tests, both falsified against pre-fix source:

- `process-crash-guard-exit.test.ts` — out-of-process, per review. Spawns a
  real child that really crashes with real piped stderr and asserts the
  breadcrumb, stack and exit code survive a real `process.exit`. Pre-fix, the
  two pipe-pressure cases fail; the unpadded ones pass, pinning the asymmetry
  that hid this.
- `graceful-shutdown.test.ts` — its stderr stub was not just stale, it was
  incapable of catching this: a stub returns synchronously regardless of the
  real write, so "writes synchronously" passed for years against an async
  write. Now redirects the real fd to a temp file and asserts the degraded
  stream path is unused, so both synchronicity tests fail pre-fix.

29/29 green post-fix; tsc unchanged at the pre-existing 38 errors, none in
changed files.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
@cursor

cursor Bot commented Aug 2, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@allyblockcast

allyblockcast Bot commented Aug 2, 2026

Copy link
Copy Markdown

🔗 Paperclip issue: BLO-19722
🔗 Paperclip issue: BLO-20618

@kkroo

kkroo commented Aug 2, 2026

Copy link
Copy Markdown
Author

/test

@kkroo

kkroo commented Aug 2, 2026

Copy link
Copy Markdown
Author

@ally review exact head 6e7101e

allyblockcast[bot]
allyblockcast Bot previously approved these changes Aug 2, 2026

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

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 6e7101e

Looks good. No Critical or Important issues remain on this head.

Strengths

  • The crash guard guarantees deliberate non-zero termination while bounding both bookkeeping and pipe-backed stderr flushing.
  • Re-entrancy, hostile error getters, cyclic cause chains, stalled stderr, and real child-process exit behavior receive focused coverage.
  • The graceful-shutdown path awaits its final breadcrumb before forced exit, preserving diagnostics under stderr backpressure.
  • The worker probe tradeoff is explicit: readiness remains responsive while liveness tolerates transient singleton event-loop stalls.

Recommended Action

  1. Approve this exact head.

allyblockcast
allyblockcast previously approved these changes Aug 2, 2026

@allyblockcast allyblockcast left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 6e7101e

Looks good. No Critical or Important issues remain on this head.

Strengths

  • The crash guard guarantees deliberate non-zero termination while bounding both bookkeeping and pipe-backed stderr flushing.
  • Re-entrancy, hostile error getters, cyclic cause chains, stalled stderr, and real child-process exit behavior receive focused coverage.
  • The graceful-shutdown path awaits its final breadcrumb before forced exit, preserving diagnostics under stderr backpressure.
  • The worker probe tradeoff is explicit: readiness remains responsive while liveness tolerates transient singleton event-loop stalls.

Recommended Action

  1. Approve this exact head.

@kkroo
kkroo dismissed allyblockcast[bot]’s stale review August 2, 2026 09:45

Resolve Ally review-consistency guard: duplicate operative Ally approval for this head; keeping the later user approval.

@kkroo
kkroo dismissed allyblockcast’s stale review August 2, 2026 19:31

Dismiss stale Ally approval: body attests 6e7101e but GitHub recorded it on 35942c4; PR needs a fresh exact-head review after update.

@kkroo

kkroo commented Aug 2, 2026

Copy link
Copy Markdown
Author

/ally review

Please review exact head 35942c485b5cf2e2c79d7e1909d98f36f7ac95ff. This is the previously approved crash-guard diff updated onto current master; require a counting exact-head approval before merge.

@kkroo

kkroo commented Aug 2, 2026

Copy link
Copy Markdown
Author

@ally review exact head 35942c4

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

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 35942c4

Important Issues (1)

  • [native-codex] server/src/index.ts:1775 — A fatal error during graceful shutdown can be reported as a clean exit. The crash guard sets process.exitCode = 1 and begins bounded asynchronous flushing, but the SIGINT/SIGTERM handler now yields at this awaited breadcrumb and later calls process.exit(0) unconditionally at line 1788. If an uncaught exception or rejection arrives while shutdown is in progress, whichever continuation exits first wins; the shutdown continuation can therefore overwrite the fatal status, truncate crash diagnostics, and make Kubernetes classify the crash as clean.
    • Coordinate the shutdown and crash paths, or make the shutdown exit preserve a non-zero process.exitCode (for example, process.exit(process.exitCode ?? 0)). Add a real child-process test that starts SIGTERM shutdown, triggers a fatal error while the final stderr write is pending, and asserts exit code 1 plus the crash breadcrumb.

Suggestions (1)

  • [native-codex] server/src/process-crash-guard.ts:287 — Consider covering NODE_OPTIONS=--unhandled-rejections=strict; Node emits both uncaughtException(..., "unhandledRejection") and unhandledRejection for one rejection in that mode, so the second event currently takes the immediate re-entry path. The repository does not currently configure strict mode, so this is not blocking this deployment.

Strengths

  • The real child-process tests exercise actual exit behavior and pipe-backed stderr pressure rather than relying only on mocked streams.
  • Cause-chain serialization is bounded and hardened against cycles and hostile getters.
  • Separating worker readiness from the wider liveness budget is operationally well motivated and documented.

Recommended Action

  1. Address the Important shutdown/crash race before merge.
  2. Consider the strict-rejection coverage opportunistically.

kkroo added 3 commits August 2, 2026 20:15
Signed-off-by: Omar Ramadan <omar@blockcast.net>
Signed-off-by: Omar Ramadan <omar@blockcast.net>
Signed-off-by: Omar Ramadan <omar@blockcast.net>
allyblockcast[bot]
allyblockcast Bot previously requested changes Aug 2, 2026

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

Exact-head review

Reviewed head: 0dc6a17

Important

server/src/index.ts:1792 still allows graceful shutdown to preempt the crash guard's bounded diagnostic flush. If a fatal event arrives after the graceful handler's final shutdown breadcrumb has drained, especially while shutdownInstrumentation() is pending, the guard sets process.exitCode = 1 and starts its asynchronous initial/final breadcrumb sequence. The graceful continuation can then call process.exit(process.exitCode ?? 0) first. That preserves the non-zero status, but it can discard the crash stack or the final exiting 1 after ... breadcrumb.

Please coordinate exit ownership when the crash guard is active, or otherwise await its bounded completion before the graceful path force-exits. Extend the real-process race test to trigger the fatal event after the graceful breadcrumb and assert the complete crash record, including [shutdown] exiting 1 after uncaughtException.

Non-blocking test hardening

The child-process helpers in process-crash-guard-exit.test.ts and the first graceful-shutdown-exit.test.ts case have no watchdog/cleanup path. A regression where the child does not exit can leave open stdio handles after Vitest's timeout. Please add bounded child cleanup while touching these tests.

CI is not terminal green yet; multiple required jobs remain queued. I did not approve this head.

Signed-off-by: Omar Ramadan <omar@blockcast.net>
@kkroo

kkroo commented Aug 2, 2026

Copy link
Copy Markdown
Author

Addressed the exact-head review on e9c1773d480614cbb33b8aae9259327c2d0b5066.

  • Graceful shutdown now yields exit ownership to the crash guard once fatal handling starts and awaits its bounded completion instead of calling process.exit first.
  • The real-process race now triggers the fatal event after the final graceful breadcrumb while instrumentation-like work is pending, and asserts the complete [shutdown] exiting 1 after uncaughtException record.
  • Added bounded kill/cleanup watchdogs to the remaining child-process helpers.

Verification: 3 focused files / 22 tests passed; pnpm --filter @paperclipai/server typecheck passed; git diff --check passed.

@ally please re-review exact head e9c1773d480614cbb33b8aae9259327c2d0b5066 and replace the prior change request if no Critical or Important findings remain.

allyblockcast[bot]
allyblockcast Bot previously approved these changes Aug 2, 2026

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

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: e9c1773

Looks good. No Critical or Important issues remain on this head.

Prior Findings Dispositioned (1)

  • prior:35942c4 important 1 — fixed — server/src/index.ts:1793 — graceful shutdown now observes the active crash path, awaits its bounded completion, and returns without issuing its own exit; the exact-head child-process test at server/src/__tests__/graceful-shutdown-exit.test.ts:84 verifies the crash record survives through the final exiting 1 after uncaughtException breadcrumb.

Suggestions (2)

  • [error handling] server/src/process-crash-guard.ts:283 — When the future onCrash bookkeeping hook is wired, consider keeping its timeout referenced until finish() starts. With no other referenced handles, the current unref() permits a natural code-1 exit before the timeout emits the final crash breadcrumb.
  • [error handling] server/src/process-crash-guard.ts:293 — Consider formatting a rejected bookkeeping value through the same non-throwing serializer used for the original crash; a hostile message getter or string coercion could otherwise take the re-entry path and truncate diagnostics.

Strengths

  • Exit ownership is now explicit: once fatal handling starts, graceful shutdown cannot preempt or truncate it.
  • The real-process race fires after the final graceful breadcrumb and asserts both code 1 and the complete fatal record.
  • Child-process helpers now have bounded watchdogs, and the strict unhandled-rejection pair is covered without duplicate crash handling.
  • Pipe-backed stderr writes are deadline-bounded while regular-file writes preserve synchronous durability.

Recommended Action

  1. Approve this exact head.

allyblockcast
allyblockcast previously approved these changes Aug 2, 2026

@allyblockcast allyblockcast left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: e9c1773

Looks good. No Critical or Important issues remain on this head.

Prior Findings Dispositioned (1)

  • prior:35942c4 important 1 — fixed — server/src/index.ts:1793 — graceful shutdown now observes the active crash path, awaits its bounded completion, and returns without issuing its own exit; the exact-head child-process test at server/src/__tests__/graceful-shutdown-exit.test.ts:84 verifies the crash record survives through the final exiting 1 after uncaughtException breadcrumb.

Suggestions (2)

  • [error handling] server/src/process-crash-guard.ts:283 — When the future onCrash bookkeeping hook is wired, consider keeping its timeout referenced until finish() starts. With no other referenced handles, the current unref() permits a natural code-1 exit before the timeout emits the final crash breadcrumb.
  • [error handling] server/src/process-crash-guard.ts:293 — Consider formatting a rejected bookkeeping value through the same non-throwing serializer used for the original crash; a hostile message getter or string coercion could otherwise take the re-entry path and truncate diagnostics.

Strengths

  • Exit ownership is now explicit: once fatal handling starts, graceful shutdown cannot preempt or truncate it.
  • The real-process race fires after the final graceful breadcrumb and asserts both code 1 and the complete fatal record.
  • Child-process helpers now have bounded watchdogs, and the strict unhandled-rejection pair is covered without duplicate crash handling.
  • Pipe-backed stderr writes are deadline-bounded while regular-file writes preserve synchronous durability.

Recommended Action

  1. Approve this exact head.

@kkroo
kkroo dismissed allyblockcast[bot]’s stale review August 2, 2026 22:01

Dismiss duplicate Ally bot approval; keeping operative Ally user approval 4839735105 for head e9c1773.

@kkroo

kkroo commented Aug 2, 2026

Copy link
Copy Markdown
Author

/ally review

Please review exact head ddf1dd69f9335fcee875153e774cf6c26bd48fcf after update to current master. Prior approvals were for older heads.

@kkroo
kkroo dismissed allyblockcast’s stale review August 2, 2026 22:09

Dismiss stale Ally approval after branch update: body attests e9c1773 but current head is ddf1dd6; needs a fresh exact-head review.

@kkroo

kkroo commented Aug 2, 2026

Copy link
Copy Markdown
Author

/ally review

Please review exact current head ddf1dd69f9335fcee875153e774cf6c26bd48fcf. The carried-forward approval was dismissed as stale.

@kkroo
kkroo dismissed allyblockcast[bot]’s stale review August 2, 2026 22:19

Dismiss stale requested-changes review: it reviewed head 0dc6a17, while current PR head is ddf1dd6. Waiting on fresh exact-head Ally review.

@kkroo

kkroo commented Aug 2, 2026

Copy link
Copy Markdown
Author

The branch was updated onto current master; exact head is ddf1dd69f9335fcee875153e774cf6c26bd48fcf. The only old-to-new tree delta is the already-merged #935 plugin-store test timeout change. The crash-guard diff remains unchanged from approved head e9c1773d480614cbb33b8aae9259327c2d0b5066.

@ally please replace the dismissed approval with an exact-head review for ddf1dd69f9335fcee875153e774cf6c26bd48fcf. The current Typecheck + Release Registry failure is unrelated infrastructure: sccache setup hit HttpError: Connect Timeout Error before typecheck ran.

@kkroo

kkroo commented Aug 2, 2026

Copy link
Copy Markdown
Author

/ally review

Please review exact current head ddf1dd69f9335fcee875153e774cf6c26bd48fcf. I dismissed the obsolete requested-changes review for 0dc6a179...; this PR needs a fresh counting Ally approval before merge.

@kkroo

kkroo commented Aug 2, 2026

Copy link
Copy Markdown
Author

@ally review exact head ddf1dd6

allyblockcast[bot]
allyblockcast Bot previously approved these changes Aug 2, 2026

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

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: ddf1dd6

Looks good. No Critical or Important issues remain on this head.

Suggestions (2)

  • [error handling] server/src/process-crash-guard.ts:283 — When the future onCrash bookkeeping hook is wired, consider keeping its timeout referenced until finish() starts; with no other referenced handles, the current unref() can permit natural process exit before the final crash breadcrumb.
  • [error handling] server/src/process-crash-guard.ts:293 — Consider formatting a rejected bookkeeping value through a non-throwing serializer; a hostile message getter or string coercion could otherwise take the re-entry path and truncate diagnostics.

Strengths

  • Graceful shutdown now yields exit ownership to an active crash guard, preserving the non-zero status and complete fatal record.
  • A real child-process regression test triggers the fatal event after the final graceful breadcrumb and verifies code 1 plus the final crash breadcrumb under stderr backpressure.
  • Re-entrancy, strict unhandled-rejection pairing, hostile original-error getters, cyclic cause chains, and stalled stderr all receive focused coverage.
  • The only change since the previously reviewed clean tree is an unrelated merged-master test timeout adjustment; the crash-guard implementation is unchanged.

Recommended Action

  1. Approve this exact head.

@allyblockcast allyblockcast left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: ddf1dd6

Looks good. No Critical or Important issues remain on this head.

Suggestions (2)

  • [error handling] server/src/process-crash-guard.ts:283 — When the future onCrash bookkeeping hook is wired, consider keeping its timeout referenced until finish() starts; with no other referenced handles, the current unref() can permit natural process exit before the final crash breadcrumb.
  • [error handling] server/src/process-crash-guard.ts:293 — Consider formatting a rejected bookkeeping value through a non-throwing serializer; a hostile message getter or string coercion could otherwise take the re-entry path and truncate diagnostics.

Strengths

  • Graceful shutdown now yields exit ownership to an active crash guard, preserving the non-zero status and complete fatal record.
  • A real child-process regression test triggers the fatal event after the final graceful breadcrumb and verifies code 1 plus the final crash breadcrumb under stderr backpressure.
  • Re-entrancy, strict unhandled-rejection pairing, hostile original-error getters, cyclic cause chains, and stalled stderr all receive focused coverage.
  • The only change since the previously reviewed clean tree is an unrelated merged-master test timeout adjustment; the crash-guard implementation is unchanged.

Recommended Action

  1. Approve this exact head.

@kkroo
kkroo dismissed allyblockcast[bot]’s stale review August 2, 2026 22:54

Dismiss duplicate exact-head Ally approval; keeping the user-side allyblockcast approval 4839914566 for head ddf1dd6.

@kkroo
kkroo merged commit 1d33e2c into master Aug 2, 2026
35 of 37 checks passed
@kkroo
kkroo deleted the kkroo/blo-20618-process-crash-guard branch August 2, 2026 23:01
kkroo pushed a commit that referenced this pull request Aug 3, 2026
… (BLO-19722 AC 2/3)

`markRunsInterruptedByWorkerCrash` had no production caller. The reconciler only
processes rows already marked `interrupted / worker_crashed`, so a real worker
death still fell through to being rediscovered minutes later as `job_missing` —
latching the agent to `error` with a reason naming the symptom rather than the
crash. This is the wiring commit the #891 split deferred until PR A
(`installProcessCrashGuard`, #949) had merged; it now has.

The guard is installed at the process entrypoint, before `startServer()` has
built the heartbeat service, so the marker is registered once that service
exists and resolved at crash time.

`crash-run-marking.ts` is a separate module on purpose. The entrypoint install
site sits under `isMainModule`, which no test executes, so an `onCrash` passed
inline there could only ever be verified by reading the file — and "verified by
reading the file" is exactly how this hook stayed disconnected through three
review rounds while its own unit tests passed. `installWorkerCrashGuard()` gives
that link a seam a test can hold.

Marking failures are deliberately not swallowed: the guard catches them and
appends `crash bookkeeping failed: …` to the crash record, so a failed mark stays
distinguishable from "nothing to mark".
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.

3 participants