mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-12 06:10:44 +01:00
## 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>