Files
PaperClipAI/packages/shared
DottaandClaude Opus 5.5 4a8c8003fd feat(recovery): add the crashPoint helper and crash-point registry (#19937)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A control-plane restart can land between two durable writes and
leave an in-progress record owned by a dead process
> - The restart-chaos campaign found these windows (20 ms–1 s) by luck,
with random kill timing
> - The durable ownership plan (Phase 5, "Testing: enumerate crash
points") asks for named crash points at each durable write boundary, so
a suite can kill the process at each one on purpose
> - Phases 2–4 run in parallel and need the helper early for their
per-kind tests
> - This pull request adds only the `crashPoint` helper and the registry
of named points
> - The benefit is one shared, zero-cost way to mark a boundary and to
arm it in a test

## Linked Issues or Issue Description

Refs #19831 (plan, "Testing: enumerate crash points", Phase 5). Refs
#19846 (Phase 0 inventory). Refs #19861 (Phase 1 registry; the
crash-point `kinds` use its kind ids).

**Problem**
No code marks the durable write boundaries. A test cannot stop the
control plane at a chosen boundary, so restart bugs are found only when
random chaos timing hits a window.

**Expected behavior**
A call `crashPoint("<name>")` at each boundary. In test mode an
environment variable makes the process SIGKILL itself at the nth hit of
one name. In production the call does nothing and costs nothing
measurable.

**Scope**
Helper and registry only. The call sites, the suite runner and the CI
workflow follow in separate pull requests.

## What Changed

- `packages/shared/src/crash-points.ts` (subpath export
`@paperclipai/shared/crash-points`, server-side only):
- `crashPoint(name)`. Disarmed, it is one comparison against a module
constant.
- `PAPERCLIP_CRASH_POINT=<name>[:<nth>]` arms one point when the module
loads. The nth hit appends a JSON receipt to
`PAPERCLIP_CRASH_POINT_RECEIPT` (optional), writes a `[crash-point]`
line to stderr, and sends SIGKILL to the process.
- `PAPERCLIP_CRASH_POINT_GATE=<file>` (optional): hits before the file
exists do not count, so setup work cannot fire the point.
- The variable is ignored when `NODE_ENV=production`. An unknown name
throws at import, so a typo cannot arm nothing.
- `armCrashPointForTest(spec)` throws `CrashPointReached` at the nth hit
instead, for in-process per-kind tests (Phases 2–4). It returns a disarm
function.
- `packages/shared/src/crash-point-registry.ts` (browser-safe data): 20
named points from the inventory's write-ahead table. Each has a
description, the environments that reach it (local, Daytona), the
ownable kind ids it affects, and the invariants (I1–I8) at risk. Names
follow `<subsystem>.<record>.<before|after>_<step>`.
- `packages/shared/src/crash-points.test.ts`: registry shape, spec
parsing, throw mode, real SIGKILL of a child process with a receipt, the
gate, production disarm, unknown-name refusal, and a disarmed-cost
check.

## Verification

- `cd packages/shared && npx vitest run src/crash-points.test.ts` → 11
passed.
- Disarmed cost on an M-series Mac: `crashPoint disarmed: 3.66 ns/call
(empty call 0.61 ns)`. A turn passes a handful of points, so the added
time before `turn.start` is in nanoseconds (plan invariant 8). The test
fails if the cost exceeds an empty call by more than 10 ns.
- `cd packages/shared && npx tsc --noEmit -p .` → clean.

## Risks

- Low risk. Nothing calls `crashPoint` yet, and nothing reads the
variable in production.
- An operator who sets `PAPERCLIP_CRASH_POINT` on a non-production
server makes it kill itself. The variable name and the `[crash-point]
armed` stderr line make this visible.
- No persisted run state changes (rules/upgrade-safe-run-state.mdc: no
previous-release story is needed).

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with
tool use (shell, file edits) and a read-only exploration subagent.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 20:37:32 -06:00
..
2026-03-12 13:09:22 -05:00