Commit Graph
676 Commits
Author SHA1 Message Date
DottaandClaude 7964433cd0 test(restart-chaos): arm a provider-fault Stop on the first live provider turn (#20512)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart-chaos harness (`tests/restart-chaos`) injects provider
message faults (PV1–PV3) to test recovery
> - A board Stop task (S1) with a PV `interrupt_ack` fault needs the
Stop to reach a live provider turn
> - The harness clicked Stop only after a seeded wall-clock window, so a
fast agent finished first and nothing was injected
> - That task then failed `provider_fault_applied`, which made a clean
wave exit 1 with no product failure
> - This pull request arms the Stop on the first live provider turn for
PV tasks
> - The benefit is that PV S1 tasks inject their fault every time, so a
wave result means what it says

## Linked Issues or Issue Description

Refs #20498 (the product fix whose live waves showed this harness gap).

**What happened?**

In waves `w-20261110T082702-s212` and `w-20261110T091057-s213` (Claude,
`--workloads Q1,S1 --faults PV2 --provider-fault-message
interrupt_ack`), task t03 (S1) failed only `provider_fault_applied` with
`applied=0`. Its run finished before the seeded Stop window, so the Stop
was missed and the fault never fired. Every product rule (R1–R6, U1)
passed. The wave still exited 1.

**Expected behavior**

A PV S1 task injects its fault on every run. A missed injection is a
harness gap, not a product failure.

**Steps to reproduce**

Run `--adapters claude --workloads Q1,S1 --faults PV2
--provider-fault-message interrupt_ack --concurrency 1 --tasks 4`. The
second S1 task often finishes before its Stop window.

## What Changed

- `observer.ts`: the run-event cursor now tracks a live provider turn
(`turn.started` with no `turn.completed/failed/cancelled/interrupted`
yet). Run samples expose it as `providerTurnActive`.
- New `stopArmed()`: a task with a provider fault arms its Stop on the
first poll that shows a live provider turn. Other tasks keep the seeded
wall-clock window, unchanged.
- A runner replacement or stop clears the live-turn mark, as it already
clears open tools.
- A PV Stop still counts as missed when the run ends with no live turn
seen and the task rests after the window.
- The continue comment still waits for the seeded window and
`STOP_CONTINUE_AFTER_MS`.
- README note under the provider fault section.

No product code changes.

## Verification

- `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts`:
35 files, 438 tests pass.
- New tests in `observer.test.ts`:
- `S1 with a provider fault: stops on the first live provider turn, not
the wall-clock window`
- `S1 with a provider fault: does not stop after a turn terminal, and
stops once when the next turn starts`
- `S1 with a provider fault: records a missed Stop when the run ends
before any live turn and the task rests after the window`
- `noteLivenessEvent: a runner replacement or stop clears the live
provider turn`
- `stopArmed: a provider-fault Stop needs a live provider turn; others
keep the window`
- Red against master `observer.ts`: the first three new tests fail.
Green with the change.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos` passes.
- No live wave was run (the wave queue was full); the coordinator asked
for a test-only change.

## Risks

- Low. This is test-harness code only.
- A PV S1 Stop can now land earlier in the turn than before. That is the
intended test point for `interrupt_ack`.
- A turn shorter than the poll interval (`--poll-ms`, 2–5 s) can still
be missed. The README states this limit.
- If a provider never records `turn.started`, the PV Stop waits until
the task rests and is then recorded as missed, the same result as
before.

## Model Used

- Anthropic Claude, model ID `claude-opus-5-5`, used through Claude Code
with tool use (shell, file edit, code execution).

## 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
- [x] All Paperclip CI gates are green
- [x] 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 <noreply@anthropic.com>
2026-11-10 04:09:54 -06:00
DottaandClaude 1a89fca1ce test(restart-chaos): add --rr-stop-on recovering so RR follow-up stops land inside recovery (#20511)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The restart chaos harness (`tests/restart-chaos/`) tests if agent
tasks survive control-plane restarts.
> - #20480 added repeated restarts (RR1–RR3): a K1, then more restarts
while the replacement recovers.
> - In the first four live waves, no follow-up stop landed inside
recovery. Every stop saw `/api/ready` report `ready`.
> - The saved receipts show why. `/api/ready` reports `recovering` only
15–80 ms before `/api/health` answers, and `ready` 0.25–0.5 s after it.
A gap measured from health, even 1 s, always lands after recovery.
> - This pull request adds `--rr-stop-on recovering`. Each follow-up
stop fires on the first `recovering` status, polled from the
replacement's spawn.
> - The benefit is that RR waves now test a restart that interrupts
recovery itself.

## Linked Issues or Issue Description

Refs #20480

**What is the problem?**
RR follow-up stops wait a gap from the replacement's health. On a local
host recovery ends about 0.3 s after health, so no stop in four waves
(14 tasks, 18 follow-up stops) landed inside recovery.

**What is the solution?**
Add `--rr-stop-on recovering`. It replaces the gap: the harness polls
`/api/ready` every 50 ms from spawn and stops the replacement on its
first `recovering` status. This is the phase poll that K6 already uses.

**Why is it needed?**
The deploy loops of 2026-11-08 and 2026-11-09 restarted the server while
it recovered interrupted runs. The harness must reach that window to
test it.

## What Changed

- `repeated-restarts.ts`: `--rr-stop-on recovering` is parsed by
`parseRepeatedRestartOptions`. It needs an RR fault and does not combine
with `--rr-gap-seconds` or `--replay`. `runRepeatedRestarts` takes
`stopOn`: with it, no gap is slept, each stop waits on the new
`waitForStopPhase` hook, and only the last replacement is waited on to
health.
- `supervisor.ts`: `waitForStopPhase` reuses the K6 phase poll
(`waitForCrashLoopPhase`, 50 ms). The record carries `stopOn`.
- `types.ts`, `config.ts`, `main.ts`: the `rrStopOn` option, its flag,
and one line to pass it to the supervisor.
- The evidence shows each stop's `/api/ready` status (`phaseAtStop`).
With `stopOn`, `gapMs` is null, and the earlier entries have no
`healthyAt`.
- README: the option and the measured timings.

## Verification

- `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts`:
35 files, 438 tests pass. New tests cover option parsing, stops on the
first `recovering` with no sleep, health waits only on the last
replacement, a `ready` stop when recovery was too short to see, an exit
before a status, and a missing watcher.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos` passes.
- A live wave from this branch (`--workloads A1,B5,C1 --faults RR1,RR3
--rr-stop-on recovering`, codex, 4 tasks) follows; its results go to a
comment.

## Risks

- Harness-only change; no product code changes. RR waves without the
flag are unchanged.
- A stop on `recovering` lands before the replacement answers health.
This is the K6 behavior, and the server log shows it. A replacement that
never reports a status fails the fault after the 5-minute ready timeout,
as K6 does.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- Anthropic Claude, model ID `claude-opus-5-5`, used through Claude Code
with tool use (shell, file edits) and extended thinking.

## 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 <noreply@anthropic.com>
2026-11-10 03:56:25 -06:00
DottaandClaude Opus 5.5 7eae4bc2ea test(restart-chaos): add ST4 partial multipart and ST7 read-after-write lag storage faults (#20504)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart-chaos harness runs real agent tasks while it injects
infrastructure faults
> - `--storage s3` puts a fault proxy between the server and a loopback
MinIO (ST1–ST3, ST5/ST5R)
> - Remote workspace snapshots over 16 MiB upload as S3 multipart
uploads, and saves are read back right after they are written
> - No fault hit single upload parts or the completion, and no fault
made a just-written object briefly invisible
> - Nothing checked that a wave left no orphaned multipart uploads or
that every committed snapshot reads back
> - This pull request adds ST4 (partial multipart) and ST7
(read-after-write lag), and a teardown store audit
> - The benefit is that waves can prove snapshot saves and restores
survive part failures and read lag without data loss or leaked uploads

## Linked Issues or Issue Description

**What existing behavior does this improve?**
The restart-chaos storage faults treat every request the same. They
cannot target one multipart part or the completion, and they cannot lag
reads behind writes.

**Subsystem affected**
tests/restart-chaos (storage chaos).

**Current behavior**
A wave cannot fail the Nth `UploadPart` of a snapshot upload, cut a part
body mid-transfer, or fail `CompleteMultipartUpload`. A wave cannot make
a `GET` or `HEAD` answer `NoSuchKey` right after a `PUT`. No wave checks
for incomplete multipart uploads or unreadable committed snapshots.

**Proposed behavior**
`--storage-faults ST4` fails a seeded subset of snapshot upload parts
(500/503, reset, or reset after 1–4 MiB of the body), and sometimes the
completion. `--storage-faults ST7` answers 404 `NoSuchKey` to reads of
an object for 5–30 s after its write succeeded. With either fault, the
wave reads every committed workspace snapshot back from MinIO (size and
SHA-256 against its row) and, after the server stops, lists incomplete
multipart uploads under the wave prefix. Either problem fails the wave.

**Reason and benefit**
These are the storage failures that can lose a workspace or leak storage
cost without any visible error.

## What Changed

- `storage-s3-ops.ts` (new): names the S3 operation from the method and
query, and reads the part number.
- `storage-multipart.ts` (new): ST4 rule planning.
- `storage-lag.ts` (new): ST7 rule planning, and a tracker of successful
`PutObject` and `CompleteMultipartUpload` writes per key.
- `storage-audit.ts` (new): teardown audit (committed snapshot
read-back, incomplete multipart uploads), `storage-audit.json`, and a
`Storage audit` section in `SUMMARY.md`. MinIO lists multipart uploads
only for an exact key or no prefix, so the audit lists the wave-owned
bucket and filters by the wave prefix.
- `storage-proxy.ts`: rules can match `operations` and `partNumbers`.
New faults `truncate` (reset after N body bytes) and
`read_after_write_lag`. `storage-proxy.json` reports `lagged`.
- `storage-chaos.ts`, `main.ts`: registry lines for ST4/ST7 and the
audit calls.
- README: ST4/ST7 rows and the audit.

## Verification

- `PAPERCLIP_IN_WORKTREE=false npx vitest run --config
tests/restart-chaos/vitest.config.ts`: 35 files, 423 tests pass. The new
`storage-multipart-lag.test.ts` fails before this change (the modules
and kinds do not exist) and passes after. It includes real MinIO tests
with the AWS SDK: part 2 fails once with 500, a part body is cut after 1
MiB, the completion fails once, the audit finds the incomplete upload
and then finds none after completion; a just-written object answers
`NoSuchKey` to `GET` and 404 to `HEAD` until the lag passes; the
snapshot check reports `sha256_mismatch` and `object_missing`.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos` passes.
- Live waves run from master after merge (local ST7, Daytona R1 ST4 and
ST7). Results go in a follow-up comment.

## Risks

- Test harness only. No product code changes.
- The audit runs only on waves with ST4 or ST7, so other storage waves
do not change.
- The snapshot check verifies the stored ciphertext against its row. It
does not decrypt, because the key lives in the instance.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code
execution in Claude Code.

## 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-10 03:49:17 -06:00
DottaandClaude 82abb3ea04 fix(native): classify a Codex API-key unknown model as a model rejection (#20488)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runner agents run on a provider (here Codex) with a model the
user saves on the agent
> - When the provider rejects that model, Paperclip should classify the
run as `native_provider_model_rejected`, ask the user to choose a model,
and let Retry finish the task
> - Paperclip recognized only the ChatGPT-account rejection (a 400 JSON
envelope). With API-key auth, Codex returns a different terminal error
for an unknown model
> - So the run failed as a generic `adapter_failed` ("Native session
failed"), and recovery opened a reconciliation incident ("Recovery
needed") for a turn that never did any work
> - This pull request recognizes the real API-key shape, bound to the
configured model, and names the rejected model on the task
> - The benefit is that the task asks for a model choice instead of a
reconciliation, and Retry completes the task after the user picks a
valid model

## Linked Issues or Issue Description

**What happened?**
The restart-chaos config fault CF3 `model_id` saves a Codex agent with
the model `chaos-missing-model-<nonce>`. The run stays "running" for
about 60 seconds, then fails with `errorCode: adapter_failed`, "Native
session failed". The task shows "Recovery needed", cause
`native_continuation_requires_reconciliation`. The grader fails as
`CF3/model_id:actionable_message` (waves `w-20261110T061914-s206-cf` t01
and `w-20261110T053831-s205-cf` t02).

Root cause: the wave runs Codex with an OpenAI API key. I drove the
bundled `codex app-server` (0.160.0) directly with an unknown model:
- With API-key auth, Codex retries the Responses API 404 ten times
(about 59 s), then fails the turn with `{"message":"unexpected status
404 Not Found: The model \`<id>\` does not exist or you do not have
access to it., url: https://api.openai.com/v1/responses, cf-ray: …,
request id:
req_…","codexErrorInfo":{"httpConnectionFailed":{"httpStatusCode":404}}}`.
- With ChatGPT auth, the turn fails in about 3 s with `message` =
`{"type":"error","status":400,"error":{"type":"invalid_request_error","message":"The
'<id>' model is not supported when using Codex with a ChatGPT
account.","code":"model_not_found"}}`. Paperclip already classified this
one.

`createNativeProviderFailureObservation` parsed only the JSON envelope,
and the runner's provider-failure category for an HTTP 404 is `unknown`.
The terminal therefore stayed unclassified.

**Expected behavior**
The run is `native_provider_model_rejected`, the task shows `userAction:
"choose_model"` and names the model, no reconciliation incident opens,
and Retry completes the task after the user picks a valid model.

**Steps to reproduce**
`node cli/node_modules/tsx/dist/cli.mjs
tests/restart-chaos/config-wave.ts --faults CF3 --adapters codex
--references model_id --tasks 1 --concurrency 1 --restarts K2 --seed
206` on `master` (with `OPENAI_API_KEY` set).

**Paperclip version or commit**
`master` at 5ab98b0.

## What Changed

- `server/src/services/native-runtime/native-provider-failure.ts`: the
Codex observation recognizes the API-key terminal shape
(`codexErrorInfo` `httpConnectionFailed` or `responseStreamDisconnected`
with `httpStatusCode: 404`, and the message prefix for the configured
model only). It classifies it as `native_provider_model_rejected` with a
new closed diagnostic `NATIVE_MODEL_NOT_FOUND_DIAGNOSTIC` (`codex`,
`model_not_found`, `404`, `api_key`). No provider text or model name is
stored in the diagnostic. A recoverable turn, another model's 404, or
another status is not classified.
- `readNativeModelRejectionDiagnostic` accepts both closed diagnostics,
so run failure diagnostics show it.
- `TaskInterruptionPresentation.rejectedModel` (shared type): with
`userAction: "choose_model"`, the task query carries the model id from
the run's native execution input. Only a
`native_provider_model_rejected` run discloses it.
- `TaskChatThread` names the model in the "Model unavailable" marker:
`The provider rejected the model "<id>".`
- The restart-chaos config wave reads `interruption.rejectedModel` as
task-level text. For `choose_model`, its repair step uses the Retry that
the "Model unavailable" marker offers (`POST /agents/{id}/wakeup` with
`reason: "retry_failed_run"`, the same request as
`agentsApi.retryFailedRun`), because a model rejection does not offer
the task-level Retry.

## Verification

- Red before, green after (the classification line disabled makes both
fail):
- `native-provider-failure.test.ts` — "classifies the API-key 404 for
the configured model as a model rejection (codex 0.160.0 shape)", with
the exact terminal error captured from `codex app-server` 0.160.0, plus
negative cases.
- `native-provider-failure.integration.test.ts` — new `api-key` scenario
through `executePaperclipNativeSession`, the persisted evidence reader
and `finalizeNativeRun`: the run ends `native_provider_model_rejected`,
the issue is `blocked` with that decision, and no issue recovery action
is created.
- `task-interruption-query.test.ts` — the real task query carries
`rejectedModel` only for a model rejection. `TaskChatThread.test.tsx` —
the marker names the model.
- `pnpm --filter @paperclipai/shared typecheck`, `pnpm --filter
@paperclipai/server typecheck`, `pnpm --filter @paperclipai/ui
typecheck` pass.
- Live proof: config wave `w-20261110T090801-s206-cf` (CF3 `model_id`,
codex with an OpenAI API key, K2, seed 206) on commit 05e8938: **1/1
pass**. The run ends `native_provider_model_rejected`; the task rests as
`waiting_for_user` / `choose_model` with `rejectedModel:
"chaos-missing-model-472e1851"` and execution cause
`native_provider_model_rejected` (no reconciliation incident), rests
again after the K2 restart, and finishes 27 s after the model is
restored and the marker's Retry is used. On `master` the same wave
failed as `actionable_message` with "Recovery needed" /
`native_continuation_requires_reconciliation`
(`w-20261110T061914-s206-cf`).

## Risks

- Low risk. The match is bound to the configured model, a 404 status and
a non-recoverable turn, so other provider failures keep their current
behavior.
- The match depends on the Codex terminal text. If a later Codex release
changes it, the run falls back to the old generic failure, not to a
wrong classification.
- Claude (ACPX) unknown models are not changed here. Claude Code reports
an unknown model as a synthetic assistant message ("There's an issue
with the selected model …") rather than a provider error; this PR does
not change or live-verify that path.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code with tool use (shell, file edits, test runs, a direct `codex
app-server` probe).

## 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 <noreply@anthropic.com>
2026-11-10 03:11:10 -06:00
DottaandClaude 182a7cd4fe test(restart-chaos): keep process start identity out of TM2/TM3 aging (#20500)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart-chaos harness (`tests/restart-chaos/`) checks recovery
under faults. TM2 (host sleep) and TM3 (aged outage) move every database
time back by an age.
> - The aging also moved process start times:
`controller_process_started_at`, `process_started_at`, and
`processStartedAt` copies in JSON.
> - A process start is identity, not a deadline. The server reads its
own start from the OS once and compares it with the stamp an owner
saved. A real sleep never changes it.
> - After a shift, every live owner looked like a different process and
failed its identity check, whatever its lease said. This harness
artifact hid whether a lease fix works (#20474).
> - This pull request keeps process start identity unaged and makes the
grader's unshift skip it too.
> - The benefit is that TM2/TM3 failures now come only from deadlines
that really lapse, as on a real laptop.

## Linked Issues or Issue Description

Refs #20458 (adds TM1–TM3) and #20474 (the lease fix whose proof wave
exposed this).

**What happened?**
A TM2 wave against #20474 still failed at the 300 s renewal. The fix
takes back a lapsed lease only for the same owner (token, attempt,
generation, boot, pid and process start). The aging had moved the saved
`controller_process_started_at` back by one hour. The live process still
reported its real start, so the identity comparison failed.

**Expected behavior**
Aging moves deadlines and event times. It never moves facts that a real
sleep cannot change, such as when a process started.

**Steps to reproduce**
Run `node cli/node_modules/tsx/dist/cli.mjs tests/restart-chaos/main.ts
--env local --adapters codex --workloads B5 --faults TM2
--time-age-hours 1 --concurrency 1 --tasks 2 --seed 304 --server-from
<#20474 checkout>`. Before this change it fails 0/2
(w-20261110T072106-s304).

## What Changed

- `time-faults.ts`: add `isProcessIdentityTime`. `agingStatements` skips
columns named for a process start. `paperclip_shift_json` keeps JSON
keys named for a process start (any spelling) unchanged. `unshiftJson`
skips the same keys, so the grader does not move them forward.
- `time-faults.test.ts`: a unit test for the statements and the unshift.
- `time-faults-db.test.ts`: the embedded-Postgres aging test now checks
that a `controller_process_started_at` column and `processStartedAt` /
`processStartedAtMs` JSON keys keep their values.
- `README.md`: one paragraph under **Aging** that says why.

## Verification

- `npx vitest run -c tests/restart-chaos/vitest.config.ts
tests/restart-chaos/time-faults-db.test.ts
tests/restart-chaos/time-faults.test.ts`: 26/26 pass. Without the
`time-faults.ts` change, 2 fail (the new unit test, and the DB aging
test's identity check).
- `npx tsc --noEmit -p tests/restart-chaos` is clean.
- Test-only change. No product code.

## Risks

- Low. Only the harness changes. A column or key whose name contains
"process started at" is no longer aged. If such a field were really a
deadline, it would no longer lapse, but none is.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used in Claude Code
with tool use (shell, file editing).

## 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
- [ ] I will address all Greptile and reviewer comments before
requesting merge

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-11-10 02:43:55 -06:00
DottaandClaude Opus 5.5 2f59b9c67c test(chaos): fire a planned restart while the server is mid S3 transfer (#20451)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart-chaos harness restarts the control plane while agent
runs are in flight, and `--storage s3` runs the server against a
loopback MinIO behind a fault proxy
> - Planned restarts fire by time or by run events. A time-based restart
seldom lands while the server is moving bytes to or from the store
> - The riskiest storage moments are a workspace snapshot upload, a
snapshot restore, and a run-log journal write. A restart in the middle
of one must not lose or corrupt the workspace or the log
> - This pull request adds a storage-triggered restart: the planned
fault fires the moment the proxy sees a matching S3 request, and the
proxy holds that request so the restart interrupts it
> - The benefit is that restart-during-transfer becomes a repeatable
wave with receipts that name the interrupted request

## Linked Issues or Issue Description

**Subsystem affected**
Restart-chaos test harness (`tests/restart-chaos`), cloud storage mode.

**Problem or motivation**
A K1/K2/K4 restart fires on a timer or on a run trigger. With `--storage
s3`, the timer almost never lands during a snapshot upload, a snapshot
download, or a run-log write. So the harness does not prove that a
restart in the middle of a transfer is safe.

**Proposed solution**
Add `--storage-trigger <target>`. At its planned time, each task's fault
is armed. The next server request that matches the target fires it. The
proxy holds the request open (up to 60 s) while the harness snapshots
the tasks and restarts the server.

**Alternatives considered**
A new `--triggers storage` id. It needs edits to the observer (near its
1,000-line limit), the trigger registry and the delay table. The storage
trigger is a wave-level mode in a new module instead.

## What Changed

- New `tests/restart-chaos/storage-trigger.ts`: targets
`snapshot-upload`, `snapshot-part` (multipart `UploadPart`),
`snapshot-download` and `run-log`; the `StorageTrigger` that arms
faults, fires on a match, holds the request and records receipts.
- `storage-proxy.ts`: an `observe(listener)` hook. The listener sees
every request and can hold it (the existing latency path).
- `main.ts`, `config.ts`, `types.ts`: registry lines only.
`--storage-trigger` (needs `--storage s3` and `--triggers time`); a due
fault is armed instead of fired; the wave writes `storage-trigger.json`.
- `storage-trigger.json` records the matching request (method, path with
upload ids hidden, declared bytes, outcome `aborted` or `completed`,
status), every other request in flight, and the fault id. A fault that
never matched is recorded with `missedReason`.
- README: "Restart during a transfer".

## Verification

- `PAPERCLIP_IN_WORKTREE=false npx vitest run --config
tests/restart-chaos/vitest.config.ts` (25 files, 304 tests pass).
- Red/green: `storage-trigger.test.ts` against the master
`storage-proxy.ts` fails 3 of 6 tests (no hook, so nothing is held or
fired); it passes with this change.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos` passes.
- Live waves (local run-log, Daytona snapshot upload and download) are
recorded in a PR comment as they finish.

## Risks

- Low risk. Test harness only. Without `--storage-trigger`, the proxy
has no listener and behaves as before.
- A replay re-arms its faults at their observed offsets. It fires on the
next matching request, not on the exact same request.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code
execution in Claude Code.

## 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-10 02:40:54 -06:00
DottaandClaude Opus 5.5 7fef6321fe test(restart-chaos): remove storage fault window time from speed budgets (#20501)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart-chaos harness grades each wave's speed against local
budgets (`wave-speed.ts`)
> - The speed grade already removes restart outage time (fault start to
server ready) from spans and restart recoveries
> - It did not remove storage fault window time (ST1 outage, ST5
rejected credentials, and so on)
> - A restart recovery that correctly waits for the store inside a
storage fault window then counts as a speed breach
> - This pull request removes the overlap with storage fault windows the
same way, and shows the removed time on its own line
> - The benefit is that storage waves fail only on real slowness, not on
correct waiting

## Linked Issues or Issue Description

**What existing behavior does this improve?**
The wave speed grade treats time spent inside a storage fault window as
product slowness.

**Subsystem affected**
tests/restart-chaos (speed grade).

**Current behavior**
The ST5 waves w-20261110T050044-s503 and w-20261110T062313-s506 exited 1
on speed breaches only. Their `restart_recovery_control_plane` p95 was
62–67 s against a 4 s budget. The recoveries were inside an ST5R window
and correctly waited for a valid key.

**Proposed behavior**
Spans and restart recoveries do not count time inside storage fault
windows (from the `storage-faults.json` receipts). Restart outage and
storage windows that overlap are removed once. `SUMMARY.md` shows the
storage time that was removed, and `speed.json` has
`storageFaultWindows` and `storageFaultRemovedMs`.

**Reason and benefit**
Storage waves must fail on real product failures and real slowness, not
on the harness's own fault windows.

## What Changed

- `wave-speed.ts`: `storageFaultWindows()` turns receipts into windows
(an open window ends now). `mergeWindows()` joins overlapping windows,
so `outageFreeClock()` removes shared time once. `collectWaveSpeed()`
takes the storage receipts and removes their time from spans;
`gradeWaveSpeed()` removes it from restart recoveries. The storage share
is reported apart from the restart outage share.
- `main.ts`: one line passes `storageChaos.receipts` to
`collectWaveSpeed()`.
- README: one paragraph in the Cloud storage section.

## Verification

- `PAPERCLIP_IN_WORKTREE=false npx vitest run --config
tests/restart-chaos/vitest.config.ts
tests/restart-chaos/wave-speed.test.ts`: 20 pass. The 3 new tests fail
before this change (`storageFaultWindows is not a function`, overlap
removed twice, span not trimmed) and pass after.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos` passes.

## Risks

- Test harness only. No product code changes.
- ST2 (slow store) windows are also removed from speed budgets. Slowness
during a slow-store window is expected, so this is intended.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code
execution in Claude Code.

## 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-10 02:39:09 -06:00
DottaandClaude a14ae9b5c9 fix(workspaces): name a failed workspace setup command on the task page (#20478)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Before an agent starts, Paperclip prepares the task workspace and
runs the project's configured setup commands (for example the workspace
provision command)
> - When that command fails, the run fails as `setup_failed`, and the
task stops with a Retry
> - The task page shows only "Failed" and a generic recovery notice. The
real error (the command, its exit status and output) is visible only on
the run page
> - The user cannot see from the task which setting is broken or what to
change
> - This pull request records a bounded, named cause for a failed setup
command and shows it on the task page
> - The benefit is that the task names the broken setting and what to
change, and classification, retry and recovery stay the same

## Linked Issues or Issue Description

**What happened?**
The restart-chaos config fault wave CF1 sets a project
`workspaceStrategy.provisionCommand` to `bash
./scripts/provision-worktree.sh`, a script that does not exist. The run
fails as `setup_failed` with `Execution workspace provision command
"bash ./scripts/provision-worktree.sh" failed: bash:
./scripts/provision-worktree.sh: No such file or directory`. The task
page shows "Failed" and "Paperclip automatically retried continuation …
still has no live execution path". The grader fails the task as
`CF1/provision_command:message_only_in_run_detail`.

**Expected behavior**
The task page names the failed setup step, the configured command, its
exit status and a short tail of its output, and says what to change.
Retry behavior stays the same.

**Steps to reproduce**
`node cli/node_modules/tsx/dist/cli.mjs
tests/restart-chaos/config-wave.ts --faults CF1 --adapters codex --tasks
2 --concurrency 2 --restarts K1,K2 --seed 204` on `master`.

**Paperclip version or commit**
`master` at 5ab98b0 (wave `w-20261110T043929-s204-cf`, tasks t01 and
t03).

## What Changed

- `WorkspaceCommandExitError` now carries the setup step
(`workspace_provision`, `workspace_seed`,
`workspace_runtime_provision`), the configured command and the command
output.
- New `server/src/services/workspace-setup-command-failure.ts`: it
builds a bounded, redacted `setupCommandFailure` record (step, command
up to 200 characters, exit code, last 300 characters of output). It also
recognizes the older message format, so runs recorded before this change
still get a name.
- `run-completion.ts` adds `setupCommandFailure` to the setup failure
`resultJson`. The error code (`setup_failed`) and all retry and recovery
fields stay the same.
- `TaskInterruptionPresentation.setupFailure` (shared type) is set for
`setup_failed` runs. It goes through the same per-run secret redaction
and user-name redaction as `workspaceFailure`.
- `ExecutionBlockerNotice` shows "Couldn’t set up the task workspace.
<command> failed with exit code N: <output>. Fix the command in the
project’s workspace settings, then retry." A scheduled transient retry
keeps its countdown and only adds the name.
- The restart-chaos config wave reads `interruption.setupFailure` as a
task-level text for its actionable-message check.

## Verification

- Red before, green after (the tests fail with the source changes
reverted):
- `server/src/__tests__/workspace-runtime.test.ts` — "names a failed
project provision command with its exit status and output (CF1)" runs a
real git worktree with a missing provision script (exit 127).
- `server/src/services/task-interruption-presentation.test.ts` — "setup
command failure presentation (CF1)": names the command, keeps
`state`/`canRetry`/`userAction` identical, recognizes the older message,
keeps a scheduled retry, bounds output and strips credentials from the
command.
- `ui/src/components/ExecutionBlockerNotice.test.tsx` and
`tests/restart-chaos/config-faults.test.ts` cover the notice text and
the harness check.
- `pnpm --filter @paperclipai/shared typecheck`, `pnpm --filter
@paperclipai/server typecheck`, `pnpm --filter @paperclipai/ui
typecheck` pass.
- Live proof: config wave `w-20261110T083227-s204-cf` (CF1, codex, 2
tasks, K1 and K2, seed 204) on commit ae9a4b7: **2/2 pass**. Both tasks
show `The project's workspace provision command "bash
./scripts/provision-worktree.sh" failed with exit code 127: bash:
./scripts/provision-worktree.sh: No such file or directory` on the task,
rest again after the restart, and finish after the repair and Retry. On
`master` the same wave failed both tasks as `message_only_in_run_detail`
(`w-20261110T043929-s204-cf`).

## Risks

- Low risk. The change adds a presentation field. It does not change the
error code, retry, recovery or blocking behavior.
- The command and output are user-configured text. They are bounded,
passed through `redactSensitiveText`, the run's secret registry and
user-name redaction. If redaction fails, the field is omitted, like
`workspaceFailure`.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code with tool use (shell, file edits, test runs).

## 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 <noreply@anthropic.com>
2026-11-10 02:35:18 -06:00
DottaandClaude 1e13c58a4e fix(heartbeat): name the cause of generic adapter failures and keep SQL text off the board (#20483)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Heartbeat run settlement records why a run failed, and the board
shows that record.
> - Runs that fail without a named code settle as `adapter_failed` (or
`setup_failed`) with no cause.
> - A user, an operator, and the restart chaos harness cannot tell what
went wrong from such a run.
> - Some run errors also show raw SQL statement text to users.
> - This pull request records a bounded `failureCause` code for generic
failures, shows it on the board, and keeps SQL text out of run errors.
> - The benefit is that a generic failure now names its cause, for
example "adapter failed (40001)", without leaking messages, SQL, or
secrets.

## Linked Issues or Issue Description

Refs #20385 (the nested-cause extraction this change reuses). Related:
#20421 (restart chaos universal checks U1-U4).

**What happened?**

The restart chaos audit of 79 saved waves found 30 failed runs with
`errorCode: "adapter_failed"` and a null or generic error. Every U3
("diagnosable") warning was `adapter_failed`. Nobody could tell what
failed. Some board run errors also showed raw SQL statement text from
wrapped database errors (Drizzle `Failed query: ... params: ...`).

**Expected behavior**

A run that fails with a generic code names a short, safe cause. The
board shows the cause next to the stop reason. Run error text never
shows SQL statements or their parameters.

**Steps to reproduce**

Make an adapter throw a wrapped database error (an `Error` whose nested
`cause` has `code: "40001"` and whose message has a Drizzle `Failed
query:` statement). Before this change, the run settles as
`adapter_failed` with no cause, and `heartbeat_runs.error` contains the
SQL statement and parameters. The new tests in
`server/src/services/heartbeat/run-completion.test.ts` reproduce this.

## What Changed

- Runner: move the #20385 event-commit cause extraction into
`safeErrorCauseCode`
(`packages/paperclip-runner/src/control-plane/safe-error-cause.ts`) and
export it. The Runner's own diagnostics use the same function, unchanged
in behavior.
- Shared: add the `failureCause` contract
(`packages/shared/src/run-failure-cause.ts`): one `[A-Za-z0-9_.:-]`
token of at most 80 characters, `normalizeRunFailureCause`,
`readRunFailureCause`, and `withoutSqlStatementText`.
- Server: add `server/src/services/heartbeat/run-failure-cause.ts`.
Settlement records `resultJson.failureCause` only when the code is
generic (`adapter_failed`, `setup_failed`). `errorCode` stays the same.
- Thrown errors (`failRun`, `failRunSetup`): `provider_http_<status>`,
else `provider_<category>`, else the first safe code in the error and up
to three nested causes (`40001`, `ECONNRESET`), else the error class
name, else `unknown_error`.
- Adapter results without a code (`completeRun`): the error family,
`signal_<name>`, `process_cancellation_failed`, `exit_code_<n>`,
`adapter_reported_error`, or `unknown_error`.
- The failure log lines (`heartbeat execution failed`, `heartbeat
execution setup failed`, and a new `heartbeat run failed` warning for
generic adapter results) include `failureCause` and `errorCode`.
- Server: replace SQL statement text in run error text with "Database
query failed (<cause>)" at all three settlement paths, including the
cancelled branch. The execution blocker's `runError`, the attention
inbox (failed-run excerpt and metadata), and continuation summaries
apply the same replacement on read, so older rows also stop showing SQL
there.
- Server: Retry admission and failed-preparation replay accept only a
closed set of `resultJson` keys. `failureCause` is added to each of
those lists (`task-retry.ts`, `chat-channels/failed-run-retry.ts`,
`cancelled-native-startup.ts`, `fresh-workspace-continuation.ts`). It is
a diagnostic code with no execution or replay authority, so a
failed-preparation run stays retryable.
- Server: `runsForIssue` returns a validated top-level `failureCause`.
It is null for older rows, non-string JSON values, and malformed values.
- UI: `RunForIssue.failureCause`; `IssueRunLedger` shows "adapter failed
(40001)". Older runs keep "adapter failed". The Storybook run-ledger
fixture has a failed run with a cause.
- Restart chaos harness: run samples keep `failureCause`. U3 treats a
generic code with a specific `failureCause` as diagnosable. A bare
`Error`, `unknown_error`, or the no-code fallback
`adapter_reported_error` still counts as vague. U3 stays a warning.
- Docs: `doc/run-log-events.md` and the harness README describe the
field and the SQL replacement.

## Verification

- Red before the fix: the four new `run-completion.test.ts` cases failed
against `origin/master`'s `run-completion.ts` (no `failureCause`, SQL
text in `error`).
- Green:
- `server/src/services/heartbeat/run-completion.test.ts`: all tests
passed (real embedded PostgreSQL). New cases cover a nested `40001`
three levels deep for execution and setup failures, SQL and a secret
parameter absent from `error` and `resultJson`, the bound (an
81-character code and a SQL "code" fall back to the class name),
`exit_code_137` for a code-less adapter result, a cancelled result with
SQL text, no cause for a named code, and the log line. On this loaded
host the suite's `afterAll` database teardown hit its 30 s hook limit
after every test passed (the known local teardown limit, also seen in
#20385). CI runs the suite.
- `server/src/services/heartbeat/run-failure-cause.test.ts` (4),
`server/src/__tests__/activity-service.test.ts` (recorded, legacy,
malformed, overlong, boolean, and number cause projections),
`server/src/__tests__/issue-continuation-summary.test.ts` (legacy SQL
row).
- `packages/shared/src/run-failure-cause.test.ts` (6),
`ui/src/components/IssueRunLedger.test.tsx` (21), restart chaos
`universal.test.ts` and `observer.test.ts`.
-
`packages/paperclip-runner/src/control-plane/durable-prp-control-plane.test.ts`:
207 passed (the stall-diagnostic cases still read `40001` and
`ECONNRESET`).
- Typecheck: shared, runner, server (all layers), UI, restart chaos
harness. `pnpm check:token-gates`: all gates clean.
- `pnpm verify` returned exit 2 (global doc input changed); required CI
supplies broad coverage.
- CI on `6295f8d` found that the new key made failed-preparation runs
ineligible for Retry (`chat-channels.integration.test.ts` saved
board-message Retry got 409;
`workspace-recovery-preparation-dispatch.test.ts` dispatched 0 Retries).
Both reproduced locally and pass after the allowlist fix, with the other
459 Retry cases in `chat-channels.integration.test.ts` and all 25 in
`workspace-recovery-preparation-dispatch.test.ts`.
- No chaos waves were run. Storybook was not opened in a browser; the
fixture typechecks and the label is covered by the UI test.

## Risks

- Low risk. `errorCode` values do not change, so retry, recovery, and
comparison logic is unaffected.
- No schema change and no migration. `failureCause` is a new optional
`resultJson` key. Rows saved by the previous release have no key and
render as before; a new row read by an old server is ignored.
- The SQL replacement only acts on a Drizzle `Failed query:` marker, or
on `$n` placeholder text with a statement shape. Ordinary error text
does not change. When it acts, it also drops the text after the
statement.
- Code that checks `resultJson` against a closed key list must allow
`failureCause`. I found and updated the four lists on the Retry path; a
list I missed would refuse a Retry for a run with a generic failure.
- This is the run log / instance database path. It adds no Telemetry
event.
- The server failure log line still logs the raw error message
(unchanged behavior); only the board-facing text is changed.

## Model Used

- Claude Opus 5.5 (Anthropic), model ID `claude-opus-5-5`, used through
Claude Code with tool use (shell, file editing) and code execution for
tests.

## 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 <noreply@anthropic.com>
2026-11-10 02:14:53 -06:00
DottaandClaude 5c9641f141 test(restart-chaos): add repeated restarts (RR1-RR3): a restart inside the previous restart's recovery (#20480)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The restart chaos harness (`tests/restart-chaos/`) tests if agent
tasks survive control-plane restarts.
> - Every existing fault restarts the server once per task.
> - Real incidents on 2026-11-08 and 2026-11-09 were deploy loops. The
server restarted again 5–60 s after it came back, while recovery of
interrupted runs was still in progress.
> - No wave puts a second restart inside the first restart's recovery
window.
> - This pull request adds a fault family, repeated restarts (RR1–RR3).
It does not change product code.
> - The benefit is that waves now test recovery that is itself
interrupted.

## Linked Issues or Issue Description

**What is the problem?**
The chaos harness restarts the server only once per fault. Production
deploy loops restart it again while it recovers, and no wave tests that.

**What is the solution?**
Add three repeated-restart faults. Each one starts as a K1 and then
stops the replacement again after a seeded gap from its health.

**Why is it needed?**
Recovery of interrupted runs can itself be interrupted. A second restart
in that window can leave a lease, a claim or a wake half done.

Refs #20458 (the TM fault family this one follows).

## What Changed

- Add `RR1`, `RR2` and `RR3` to `--faults` (`repeated-restarts.ts`):
- **RR1 double crash**: K1, then one more `SIGKILL` 3–20 s after the
replacement reports healthy.
- **RR2 crash, then graceful**: K1, then one `SIGTERM` (as K2) 3–20 s
after health.
- **RR3 crash loop**: 3–4 `SIGKILL`s in all, 10–30 s after each health.
The last replacement stays up.
- Add `--rr-gap-seconds <n>` or `<min>-<max>` (1–300). It replaces every
RR gap. RR faults do not combine with `--storm-minutes`.
- The gaps are seeded per task (`restartSteps` in `plan.json`, also in
`--matrix` plans). They are drawn only for RR tasks, so the plans of
existing seeds do not change. `--replay` restarts again after the
recorded gaps.
- The supervisor runs the whole sequence as one fault receipt. The
grader therefore sees one fault window, from the first kill to the last
replacement's ready. Crossing, R1's outage allowance, R5, the T-oracle,
task deadlines and the recovery speed budget treat it as one fault. No
check is relaxed.
- Each stop and replacement is recorded with its signal, gap, PID, the
`/api/ready` status just before the stop, and its stopped, restarted and
healthy times. The record goes to
`<wave>/repeated-restarts/<faultId>.json` (also written when the
sequence fails part way), to the receipt (`repeatedRestart` in
`faults.json` and in each failure's `startup-phases.json`), and to the
timeline (`repeated_restart_stop`, `repeated_restart_healthy`).
- Small registry additions: `types.ts`, `config.ts`, `scheduler.ts`,
`journey-matrix.ts`, `main.ts`, `evidence.ts`, `supervisor.ts`, and the
README section next to TM.

## Verification

- `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts`:
33 files, 402 tests pass. The new `repeated-restarts.test.ts` covers
option parsing, seeded planning for each kind, unchanged plans for other
faults, steps in the task schedule, the matrix and replay, the
stop-and-replace loop on a fake clock (gaps measured from health,
`SIGTERM` for RR2, an exit during a gap, shutdown), and one crossing
window for the sequence.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos` passes.
- `pnpm verify` asks for broader coverage (the README is a global CI
input), so required CI supplies it.
- Live waves run from master after merge, as the merge rule for
test-only harness PRs allows. Their results go to a follow-up comment.

## Risks

- Harness-only change; no product code changes. Waves without RR faults
are unchanged: the plan draws, the fault receipt and the grader paths
are the same.
- During an RR sequence the server stays "not ready" for the harness, so
no other fault fires and the observer does not click Retry until the
last replacement is ready. This is the same rule as for K6.
- A replacement that exits on its own during a gap fails the fault as a
harness error, with the stops so far recorded. The wave then recovers
the server as after any failed fault.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- Anthropic Claude, model ID `claude-opus-5-5`, used through Claude Code
with tool use (shell, file edits) and extended thinking.

## 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 <noreply@anthropic.com>
2026-11-10 01:44:51 -06:00
DottaandClaude Opus 5.5 179694de7d test(chaos): provider message faults for Claude (ACPX) and across a K1 restart (#20454)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness (`tests/restart-chaos/`) proves that agent
work survives control-plane restarts and provider faults
> - PR #20432 added provider message faults (PV1 delay, PV2 drop, PV3
duplicate), but only for Codex, and only in waves without restarts
> - Claude runs through the ACPX sidecar, whose provider pack and
sidecar are digest-verified, so the Codex executable shim cannot reach
it
> - A late or missing terminal can also arrive across a control-plane
restart, which no wave could exercise
> - This pull request adds a Claude (ACPX) seam with no product change,
and a K1 restart that fires after a provider fault applies
> - The benefit is that governed waits, Stop and approvals are now
tested under provider message faults for both main adapters, also across
a kill -9

## Linked Issues or Issue Description

Refs #20432, #20330, #20336

**What is the problem?**
Provider message faults covered only Codex. They could not run with
restart faults. PV3, the `interrupt_ack` target, and the S1 and G1
workloads under PV1/PV2 never ran live.

**What is the expected outcome?**
The harness can delay, drop or duplicate one targeted provider message
for Claude and Codex. A PV1/PV2 fault can straddle a K1 restart, and the
grade states what a pass means.

**Why is this the right layer?**
This is test-harness work. The product is not changed. The seam replaces
only a file the runner already hashes and verifies at launch, inside a
per-wave stand-in package.

## What Changed

- `acpx-provider-faults.ts`: builds a per-wave stand-in runner package
under `<repo>/node_modules/.restart-chaos-<wave>/` (symlinks to the real
package, except a generated `dist/cli/acpx-runtime-sidecar.cjs`). It
also parses `--provider-fault-restart`.
- `provider-shim/acpx-sidecar-hook.mjs`: a server-only resolve hook that
loads `runnerd-codex-transport` (source `src/live/*.ts` or built
`dist/live/*.js`) from the stand-in package. Product code then hashes
the stand-in sidecar, and the runner verifies and launches it. The
module's own imports resolve to the real package.
- `provider-shim/acpx-fault-tap.cjs`: the stand-in installs this tap and
then runs the real sidecar bundle in the same process, as the runner's
verified CommonJS loader does. For Claude, `terminal` is the provider's
ACP `session/prompt` response after a `session/cancel`. `interrupt_ack`
is the sidecar's `turn.cancel` response, because ACP's cancel has no
acknowledgement. The provider process is still spawned and admitted by
product code.
- `--provider-fault-restart K1 [--provider-fault-restart-after-seconds
n]`: each PV1/PV2 task also kills -9 the control plane n seconds
(default 3) after its fault applied, through a new internal `provider`
trigger. PV3 and planned K cells stay rejected.
- `withProviderFaultCheck`: with a restart, the check passes only if the
fault applied once, the K1 started after it, and for PV1 no release came
first. The ordinary R1–R6 grade and the workload T-oracle still decide
whether the task recovered.
- `config.ts`: provider faults accept `--adapters codex,claude`. G1 with
a provider fault needs `--provider-fault-restart K1`, because its oracle
grades the board decision after a restart.
- README: the Claude seam, the target table per adapter, what a pass
means across a restart, and the Claude PV1 delay window.
- Unit tests: the ACP and runner-pipe taps, the generated stand-in with
a fixture sidecar, the hook against a fake runner package, planning,
config validation, and the restart check.

## Verification

- `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts`
(all pass after rebase on master)
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`
- Live waves (local, one at a time through the wave-slot protocol):

| Wave | Scenario | Result |
| --- | --- | --- |
| w-20261110T040202-s202 | Claude Q1, PV1 terminal 18 s | Task passed
its Q1 oracle; `provider_fault_applied` failed: the runner settled the
cancelled turn ~3.6 s after the cancel and closed the sidecar before the
release, so 18 s is a drop for Claude (documented) |
| w-20261110T041224-s203 | Claude Q1, PV1 terminal 1 s and 3 s | 2/2
pass (held and released) |
| w-20261110T041355-s204 | Claude Q1 and S1, PV2 terminal | 2/2 pass |
| w-20261110T043354-s205 | Codex Q1, PV3 terminal (duplicate) | 1/1 pass
|
| w-20261110T050407-s206 | Codex S1 and G1, PV1/PV2 terminal | 0/4: S1
terminal never sent (Codex exits after Stop, so `terminal` cannot apply;
documented, use `interrupt_ack`); G1 needs a restart by design (now
rejected without `--provider-fault-restart K1`). No product failure. |
| queued | Codex+Claude Q1/S1 PV1/PV2 `interrupt_ack`; Codex+Claude
Q1/G1 PV1/PV2 + K1 | Runs from master after merge (queue is ~90 min
deep) |


## Risks

- Low product risk: no product code changes.
- The seam depends on the runner computing the local sidecar path from
the transport module URL and on the ACPX provider bootstrap marker
string. If either changes, the tap logs nothing and every Claude PV task
fails `provider_fault_applied`. It does not pass silently.
- The stand-in package is written under the workspace `node_modules`
(gitignored) and removed at wave end.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- Anthropic Claude, model ID `claude-opus-5-5` (Claude Code agent with
tool use and code execution)

## 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
- [x] All Paperclip CI gates are green
- [x] 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-10 01:06:25 -06:00
DottaandClaude bf1524c5f8 test(restart-chaos): pin each wave's API URL to its own loopback server (#20462)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart-chaos harness starts an isolated Paperclip server for
each wave and runs real agents against it
> - Agents find the server through `PAPERCLIP_API_URL`, which the server
derives at startup
> - On a developer machine with a public URL for the main instance (for
example a tailnet origin), the wave server derived that public URL
instead of its own loopback origin
> - So `process` agents sent the run key for the temporary instance to a
different host, which returned 502
> - This pull request pins each wave server's `PAPERCLIP_API_URL` to its
own loopback origin and removes the Q3 script workaround
> - The benefit is that a wave's agent credentials only go to the wave's
own server

## Linked Issues or Issue Description

**What happened?**
In isolated restart-chaos wave instances, `process` agents received
`PAPERCLIP_API_URL` set to the machine's public tailnet origin. That
origin serves the developer's main instance, not the wave. The agent's
run key for the temporary instance was sent to that host, which returned
502. The Q3 agent script worked around this with a hard-coded loopback
base URL.

**Expected behavior**
A wave's agents call the wave's own server.

**Steps to reproduce**
1. Configure a public URL (for example a tailnet origin) for the
developer's main Paperclip instance on the machine.
2. Run a restart-chaos wave with a `process` agent that reads
`PAPERCLIP_API_URL`.
3. The agent's API calls go to the public origin, not to the wave's
`127.0.0.1` port.

**Paperclip version or commit**
master at 2c2186706e

**Additional context**
This is a harness isolation gap, not a product bug. The server
intentionally lets an explicit `PAPERCLIP_API_URL` win, and otherwise
prefers the configured public URL (`choosePrimaryRuntimeApiUrl`),
because an operator's public URL is the address agents must use in many
deployments. The isolated wave server is the component that must name
its own origin. `tests/runner-e2e/server.ts` already pins its public URL
to loopback in its public-MCP mode for the same reason.

## What Changed

- `tests/restart-chaos/supervisor.ts`: new
`waveServerUrlEnvironment(port)` sets `PAPERCLIP_API_URL` to
`http://127.0.0.1:<port>` in every wave server environment (also after
restarts and a K14 switch).
- `tests/restart-chaos/journey-driver.ts`: the Q3 agent script reads
`PAPERCLIP_API_URL` again. The hard-coded loopback workaround is
removed.
- `tests/restart-chaos/journeys.test.ts`: two tests.

## Verification

- Red before, green after (`npx vitest run -c
tests/restart-chaos/vitest.config.ts
tests/restart-chaos/journeys.test.ts`, 22 passed):
  - `pins the wave's loopback origin over an ambient public API URL`
- `sends the Q3 agent's run key only to the PAPERCLIP_API_URL its server
assigned` (runs the real Q3 script against a local HTTP server and
checks that every request goes there with the run key)
- `npx tsc --noEmit -p tests/restart-chaos/tsconfig.json` passes.

## Risks

- Low risk. Test harness only. No product code changes.
- The pin applies to `PAPERCLIP_API_URL` only. A public URL from the
developer environment can still set the wave's auth base URL. No current
workload depends on it.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code with tool use and code execution.

## 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 <noreply@anthropic.com>
2026-11-10 00:24:21 -06:00
DottaandClaude 400bb1f0f9 test(restart-chaos): one-row db pressure, bound accepted-Retry waits in U1, live L1 results (#20472)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The restart chaos harness grades real agent tasks on an isolated
control plane.
> - #20421 added the L1 contention workload, `--db-pressure`, and the
universal checks U1–U4.
> - The first live L1 waves showed two harness gaps and one missed
product shape.
> - The pressure writers locked several run rows per statement and
deadlocked with product transactions. A real stream does not do that.
> - U1 counted an accepted Retry that waits on the system as a live
owner forever. Ten tasks sat 8 minutes in that state with no run.
> - This pull request fixes both gaps, adds the owed direct test for
`collectContentionEvidence`, and records the live wave results.

## Linked Issues or Issue Description

Refs #20421, #20385.

**What happened?**

The live L1 waves (`w-20261110T040531-s20385` on the #20385 parent,
`w-20261110T044338-s20385` and `w-20261110T050818-s20386` on master)
produced harness deadlocks (40P01) from multi-row pressure statements.
U1 passed tasks whose accepted Retry waited on
`process_identity_missing` for 16 minutes without a run.

**Expected behavior**

Pressure writers behave like a run-log stream (one row per statement).
U1 flags an accepted Retry that waits on the system past the queue
bound.

**Steps to reproduce**

Run `node cli/node_modules/tsx/dist/cli.mjs
tests/restart-chaos/universal-audit.ts <results dirs>` over the saved
waves, or the L1 wave command in the README.

## What Changed

- `contention.ts`: each pressure statement updates one running run row.
The old statement updated every running row at once and deadlocked with
product transactions.
- `universal.ts` (U1): an accepted Retry that waits on the system
(`retryWait`) is an owner only for the queue bound (5 min of
server-ready time). While it waits, the Retry button, failed notice and
blocker do not count as actionable items, because the person already
acted. A scheduled retry is still a timer.
- `contention-db.test.ts`: a real PostgreSQL test for
`collectContentionEvidence` (receipt count, duplicate producer events,
terminal receipt, accounting fields, company scope).
- README: live wave results table, updated audit numbers and the U1
rule.

## Verification

- `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts`:
28 files, 345 tests pass.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`:
passes.
- Audit over 79 saved waves (1,620 tasks, 1,483 passing): U1 17 flags,
none on a passing task or an all-pass wave. The longest accepted-Retry
wait on a passing task was 278 s. U2 1, U3 30 (3 on passing tasks,
warning only), U4 0.
- Live waves (codex and claude, `--workloads L1 --faults K1
--concurrency 4 --tasks 4 --db-pressure 4`):
- `w-20261110T040531-s20385`, #20385 parent: 117 receipt-write
serialization failures (40001) in the server log; 1 of 4 tasks passed.
Three runs failed `adapter_failed` after 40001 exhausted in
`createRunUsageRecorder`, `reserveRunBudget` and native finalization.
- `w-20261110T044338-s20385`, master `8004ad069e` with the harness: 2
serialization failures; 1 of 4 passed. Failures included 40001 in
`createRunUsageRecorder` and deadlocks caused by the old multi-row
writers (fixed here).
- `w-20261110T050818-s20386`, same build with one-row writers: 0 of 4
passed. Every first run failed in `reserveRunBudget` before spawn, and
every accepted Retry then waited on `process_identity_missing` without
starting a run.
- No wave reached the runner's commit stall, so none showed
`native_event_replay_conflict`.

## Risks

- U1 can now fail a task whose accepted Retry waits more than 8 minutes.
No passing task in 79 saved waves came close (longest 278 s).
- One-row writers produce fewer statements per second than the multi-row
statement did; `contention-db.test.ts` still reproduces the #20385
receipt race with them.
- Test-only change; no product code.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, through Claude Code
with tool use (shell, file editing, GitHub CLI).

## 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 <noreply@anthropic.com>
2026-11-10 00:22:07 -06:00
DottaandClaude Opus 5.5 2444658c6f test(restart-chaos): add ST5 credential rejection and ST5R key rotation storage faults (#20455)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart-chaos harness runs real agent tasks while it injects
infrastructure faults
> - `--storage s3` already puts a fault proxy between the server and a
loopback MinIO (ST1 outage, ST2 latency, ST3 errors)
> - Cloud deployments use the default AWS credential chain, and keys can
expire or rotate during a run
> - The harness had no fault for a rejected or rotated key, so this
failure class was untested
> - This pull request adds ST5 (a window of 403 credential rejections
from the proxy) and ST5R (a real key rotation at MinIO)
> - The benefit is that waves can prove runs wait out credential
problems and pick up rotated keys

## Linked Issues or Issue Description

**What existing behavior does this improve?**
The restart-chaos storage faults cover outages, latency and 5xx errors
only.

**Subsystem affected**
tests/restart-chaos (storage chaos).

**Current behavior**
No wave can reject the server's S3 credentials or rotate the store's
key.

**Proposed behavior**
`--storage-faults ST5` answers every request in a window with 403
`ExpiredToken`, `InvalidAccessKeyId` or `SignatureDoesNotMatch`.
`--storage-faults ST5R` restarts MinIO with a new root key (the old key
stops working) and gives the server the new key through its credential
source only when the window ends.

**Reason and benefit**
The first local ST5 wave (w-20261110T041556-s501) shows the gap: a 41 s
`ExpiredToken` window failed 4 of 6 tasks with `adapter_failed`, and the
automatic retries failed within one second. The product fix is a
separate PR.

## What Changed

- `tests/restart-chaos/storage-credentials.ts` (new): ST5 and ST5R rule
planning, a wave-owned `AWS_CONFIG_FILE` profile whose
`credential_process` prints the current key, MinIO restart with a new
root key on the same loopback port, and a proxy wrapper that holds
traffic during the restart and publishes the new key when the window
ends.
- `storage-proxy.ts`: the `expired_credentials` fault takes an S3 error
code; a `credential_rotation` marker passes traffic through.
- `storage-chaos.ts`, `types.ts`, `config.ts`, `main.ts`: registry lines
for ST5/ST5R and the rotation wiring.
- README: ST5/ST5R rows and the credential-source description.

## Verification

- `PAPERCLIP_IN_WORKTREE=false npx vitest run --config
tests/restart-chaos/vitest.config.ts` (28 files, 328 tests pass). The
new `storage-credentials.test.ts` fails before this change (the kinds do
not exist) and passes after. It includes a real MinIO test: after a
rotation the old key gets 403 `InvalidAccessKeyId`, a client with a
cached key fails, and the same client succeeds after it re-resolves its
profile.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos` passes.
- Live wave: `--env local --workloads A1,C1 --faults K1,K2 --tasks 6
--storage s3 --storage-faults ST5,ST5R --storage-fault-seconds 30-90`
ran and injected 62 credential 403s (storage-proxy.json).

## Risks

- Test harness only. No product code changes.
- ST5R restarts MinIO. Requests are held for up to 5 s during the
restart, so a slow restart can also show as a short 502 blip.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code
execution in Claude Code.

## 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-10 00:16:28 -06:00
DottaandClaude Opus 5.5 95cb3c94c4 test(restart-chaos): cap store bandwidth during ST2 slow-store windows (#20452)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart-chaos harness tests how runs survive restarts and cloud
storage faults
> - A Paperclip-sized remote workspace (R1 on Daytona with `--storage
s3`) saves about 730 MiB per snapshot as a multipart upload and restores
it as one large download
> - The ST2 "slow store" fault only adds latency per request. On a
healthy loopback store the whole multipart upload takes about 2 s, so a
latency fault adds a fixed delay per part and never makes a large
transfer slow
> - A real degraded store is slow per byte too. We need that to test
large saves and restores that take minutes
> - This pull request gives each ST2 window a seeded bandwidth cap that
all requests in the window share
> - The benefit is that ST2 waves now slow large snapshot uploads and
downloads by minutes, and the harness can find save and restore timeouts
on big workspaces

## Linked Issues or Issue Description

Refs #20299, #20313 (storage outage handling on small workspaces),
#20442 (saved workspace Files over 512 MiB).

**What existing behavior does this improve?**
The ST2 slow-store fault in the restart-chaos storage proxy.

**Subsystem affected**
Test harness (`tests/restart-chaos`).

**Current behavior**
ST2 adds 1–10 s of latency before each request. The bytes then flow at
loopback speed. A 730 MiB snapshot upload or download is still fast.

**Proposed behavior**
ST2 also caps bandwidth at a seeded 2–16 MiB/s. One rate per direction
is shared by every request in the window, so parallel multipart parts
share the cap.

**Reason and benefit**
Large saves and restores now take minutes under ST2. This shows the save
and restore timing, the storage request counts, and any timeout on a
slow store.

## What Changed

- New `tests/restart-chaos/storage-bandwidth.ts`: a shared byte-rate
pacer (`ByteRate`), a pacing stream (`paced`), and the seeded ST2
bandwidth draw (2–16 MiB/s).
- `storage-proxy.ts`: the `latency` fault takes an optional
`bytesPerSecond`. The proxy paces the request body and the response body
through one rate per direction for each rule.
- `storage-chaos.ts`: ST2 windows set `bytesPerSecond`. Saved receipts
keep the rule, so a replay uses the same cap.
- README: the ST2 row describes the cap.

## Verification

- `PAPERCLIP_IN_WORKTREE=false npx vitest run --config
tests/restart-chaos/vitest.config.ts
tests/restart-chaos/storage-bandwidth.test.ts
tests/restart-chaos/storage-proxy.test.ts
tests/restart-chaos/storage-chaos.test.ts`: 14 pass.
- Red before: with the proxy and ST2 changes reverted, the proxy cap
test and the ST2 draw test fail. Green after.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`: pass.

## Risks

- Low risk. Test harness only. ST2 now draws one more seeded number, so
a seed gives different later windows than before. Replays read the saved
receipts and are not affected.

## Model Used

Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code
execution in Claude Code.

## 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-10 00:03:56 -06:00
DottaandClaude Opus 5.5 30d99f4e32 test(restart-chaos): add time faults (TM1-TM3): freeze, sleep with aged rows, aged outage (#20458)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The restart chaos harness (`tests/restart-chaos/`) tests if agent
tasks survive control-plane restarts.
> - Each wave runs for minutes on a fresh instance. Nothing time-bound
ever expires during a wave.
> - Production failures on 2026-11-09 were about time: an OAuth refresh
after more than 1 h made pending approvals invalid (#20340), and a
timeout used the wrong window (#20330).
> - This pull request adds a fourth fault family, time faults (TM1–TM3).
The faults make leases, deadlines and waits expire during a run or
across a restart. They do not change product code.
> - The benefit is that waves now find failures that occur only when
time passes. The first waves found one: a run whose turn completes after
a host sleep fails, and the task needs Retry.

## Linked Issues or Issue Description

Refs #20340
Refs #20330

**What is the problem?**
The chaos harness never moves the clock, so it cannot find failures
where a lease, a deadline or a pending wait expires.

**What is the solution?**
Add three time faults. Each one makes stored deadlines lapse in a way
that a real host sees: a process pause, a host sleep, or a long outage.

**Why is it needed?**
Two days of chaos testing did not find the failures of #20340 and
#20330, because no wave lasted long enough for a deadline to pass.

## What Changed

- Add `TM1`, `TM2` and `TM3` to `--faults` (`time-faults.ts`,
`time-faults-config.ts`):
- **TM1 freeze**: send `SIGSTOP` to the server process only, for 95–150
s, then send `SIGCONT`. The runner and Postgres continue. The pause is
longer than the controller-instance lease (90 s) and the
execution-control deadline (60 s).
- **TM2 sleep**: send `SIGSTOP` to the server and all of its
descendants. While they are stopped, move every database time back by
the age. Then send `SIGCONT`. This is a stand-in for a laptop sleep.
- **TM3 aged outage**: send `SIGKILL` to the server tree (as K1). While
it is down, move every database time back by the age. Then start a
replacement. With `--workloads G1 --triggers approval`, an approval
stays pending for a day across a restart.
- Add `--time-age-hours` (1–72, default 26). TM faults run only with
`--env local`, and not with storms, crash points or K14.
- Aging is one transaction. It moves every `timestamp` column, and every
time inside a `jsonb` column (ISO strings in their own format, and epoch
milliseconds in the wave's window), back by the same interval. Triggers
are off for the shift. Copies of a time in JSON (for example the comment
`updatedAt` in a continuation receipt) still match their columns. The
first wave showed that a shift of the columns only causes false
failures.
- The identity, state and time columns of the time-bound tables are
written before the shift to `<wave>/time-faults/<faultId>.json`.
Per-table counts go to the fault receipt (`timeFault`) and the timeline
(`time_fault_aged`).
- The observer and grader read through `TimeShiftedApi`. It adds the age
back to each time that a shift moved, so R4, R5, U1–U4 and the speed
budgets use real time.
- Make small additions to the registries: `types.ts` (fault kinds,
receipt field, option), `config.ts`, `scheduler.ts` (a seeded freeze
length only for TM tasks, so the plans of existing seeds do not change),
`supervisor.ts` (the freeze and aging hooks), `main.ts` (one line for
the API) and the README.

## Verification

- `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts`:
30 files, 368 tests pass. The new tests are:
- `time-faults.test.ts`: option parsing, freeze order, Postgres
exclusion, statement generation, and reading times back at their real
time.
- `time-faults-db.test.ts`: aging runs on real embedded Postgres. The
test checks that trigger rewrites are suppressed, that JSON copies match
their columns, that text columns do not change, that the evidence file
is written, and that an age the wave nearly outlived is refused.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos` passes.
- Live waves (local, codex), each run from a separate worktree and one
wave per slot:

| Wave | Scenario | Result |
| --- | --- | --- |
| `w-20261110T031929-s301` (A1,C1 × TM1–TM3) | First run, before JSON
aging | t01: TM1 missed, because A1 finished first. t02: pass. t03 (C1):
its turn completed after TM2 and failed with
`native_session_lease_lost`; a follow-up comment woke a new run. After
TM3, that run failed with `continuation_user_authorization_missing` and
needed Retry. That second failure was a harness artifact: the receipt
kept the comment's `updatedAt` in JSON, which the first version did not
shift. JSON aging fixes it. |
| `w-20261110T041910-s303` (B5 × TM1–TM3) | Freeze, sleep, aged outage |
t01 and t02 crossed TM1, then TM2. Both turns completed after the thaw
and failed with `native_session_lease_lost`, so both needed Retry (R3).
This is a **product failure**; see below. t03 (TM3, 26 h aged outage):
**pass**, with no Retry. |
| `w-20261110T050125-s302` (G1 × TM3, `--triggers approval`) | Aged
wait: an approval pending for 26 h across a restart | **Pass** (2/2).
Both approvals were still pending after the restart and the board
decided after the fault: t01 rejected, so the task is cancelled; t02
approved, so it is done with exactly one subtask. No Retry; U1–U4 pass.
|
| `w-20261110T051535-s304` (B5, TM2 with `--time-age-hours 1`, then TM1;
one task at a time) | Attribute the failure | t01 (TM2 sleep, only 1 h):
its turn completed after the thaw, then failed with
`native_session_lease_lost`, so the task needed Retry. This reproduces
the product failure. t02 (TM1 alone, 113 s server-only freeze):
**pass**. The failure follows the database clock jump, not the pause. |

**Product failure found (not fixed in this PR).** A local native run
whose host sleeps longer than the 20-minute execution lease loses its
completed work. After the wake, the provider finishes the turn and the
brief is saved. Then result acceptance (`native-session-executor.ts`,
the transaction at about line 12031) requires
`native_run_finalizations.lease_expires_at > clock_timestamp()`. The
database clock has moved on, so the run fails with
`native_session_lease_lost`, and the task shows "Couldn't finish this
attempt … Retry". No other boot claimed the lease: the owner, attempt
and controller identity are unchanged, and the server's monotonic
deadline did not expire. The lease is fenced by wall-clock expiry alone.
A safe fix changes lease fencing semantics, which is a
heartbeat-boundary decision, so I report it here rather than include a
fix in this harness PR. Reproduce with `--workloads B5 --faults TM2`
(`--time-age-hours 1` is enough; see `w-20261110T051535-s304`).

## Risks

- Harness-only change; no product code changes. Waves that do not use TM
faults are unchanged. `TimeShiftedApi` returns responses unchanged when
no shift was applied.
- `SIGSTOP` goes only to the wave server's own process tree. The tree is
found from the server PID, and Postgres is excluded. Every frozen
process gets `SIGCONT` in a `finally` block and in a process `exit`
hook.
- Aging is a model: the database becomes what it would be if everything
happened the age earlier. Times outside Postgres (in-memory timers,
runner state files), times inside `text` columns, and HMACs over a time
are not moved. The README lists these limits. Agent JWT expiry is not
covered: native runs mint a token for each tool call.

## Model Used

- Anthropic Claude, model ID `claude-opus-5-5`, used through Claude Code
with tool use (shell, file edits) and extended thinking.

## 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
- [x] All Paperclip CI gates are green
- [x] 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-09 23:44:52 -06:00
DottaandClaude 2c2186706e test(restart-chaos): human-in-the-loop journeys (Q2, Q3), journey triggers and a journey x fault matrix (#20448)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The restart chaos harness (`tests/restart-chaos/`) restarts the
control plane while real agents work, and grades each task.
> - Almost every harness workload is "the agent does a file task and
finishes". Few workloads have a person in the loop.
> - On 2026-11-09, production failures came from journeys with a person
in the loop: a saved answer that could not reach a dependency-blocked
task (#20363), and an app-action approval that a routine OAuth refresh
or a one-hour deadline made invalid (#20340).
> - No workload exercised these journeys, so no wave could catch them.
> - This pull request adds journey workloads with deterministic oracles
and journey-specific fault triggers. It also adds a journey × fault
matrix and a way to run a wave against an older build.
> - The benefit: one command crosses every journey with every fault
kind, and the same wave proves red before a fix and green after it.

## Linked Issues or Issue Description

Refs #20363, Refs #20340, Refs #20336, Refs #20432

## What Changed

- New workload **Q2** (`journeys.ts`, `journey-driver.ts`): the agent
saves a question with its native `request_human_input` tool. The board
adds an unfinished `backlog` prerequisite, marks the task `blocked` by
it, and answers through the task-thread respond route. The oracle
requires that the answer reaches the agent in bounded answer mode while
the task stays blocked, that the prerequisite relation stays, that no
run has `issue_dependencies_blocked`, and that no stale Retry notice
stays at rest. Then the board finishes the prerequisite and the agent
finishes the task.
- New workload **Q3**: a harness-owned OAuth 2.1 + MCP app on loopback
(`journey-oauth-app.ts`, discovery, dynamic client registration, PKCE,
rotating refresh tokens, one write tool). The board connects it through
the Apps routes and sets the tool to Ask first for a scripted `process`
agent. When the review is pending, the harness moves the review's saved
timestamps and the token expiry back 61 minutes in the wave's own
database, runs Check connection (which refreshes the token through the
app), and approves. The oracle requires one review, a refresh between
request and decision, an accepted approval, exactly one `tools/call`
with the reviewed arguments, an `executed` request, and an agent report
that carries a receipt only the provider result contains.
- New triggers `asked` and `answered` fire a restart fault 0–3 s after
the saved request is first seen and after the board's response. They
work for the existing **Q1** saved question (#20432) too. The board
answers before an `answered` fault fires and after every other planned
fault.
- New `--matrix` option: one task per workload × adapter × (a `K0`
no-fault baseline, each restart fault at each of the workload's own
triggers, each provider message fault). `K0` is a new task fault kind
with no fault.
- New `--server-from <checkout>` option: every server start and restart
of the wave runs another prepared checkout, for example the parent of a
fix, with the same harness code.
- Q3 setup allows unpriced usage on the wave's company budget policy,
because a scripted process run records usage with no price and the
default policy pauses the whole company.
- README documents the journeys, the triggers, the matrix and the
red-before-fix recipe. Unit tests cover the oracles (pass and the
pre-fix failure signatures), the driver's ordering, the matrix plan,
option validation and the OAuth app.

## Verification

- `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts`:
295 passed.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`:
passed.
- Live local waves, codex, one wave at a time (shared machine lock),
`--concurrency 2`, at most 3 tasks:

| Wave | Server build | Command | Result |
| --- | --- | --- | --- |
| `w-20261110T024308-s106` | this branch (master `5570debc5b` base) |
`--workloads Q3 --faults K1 --matrix --seed 106` | 3/3 pass (K0,
K1^asked, K1^answered); the app saw one refresh before each decision and
one `tools/call` |
| `w-20261110T024447-s106` | `--server-from` parent of #20340
(`0e325e8d52`) | same | 3/3 fail as expected: approve returns 409
`action_expired` ("Tool review has expired"), no `tools/call`, task
stays `in_review` |
| `w-20261110T024644-s107` | this branch | `--workloads Q2 --faults K1
--matrix --seed 107` | K1^asked and K1^answered pass. The K0 cell failed
only because the oracle then required a comment; the agent had reported
`ANSWER-…` in its bounded turn's run result 14 s after the answer, while
blocked. The oracle now accepts either. |
| `w-20261110T030341-s107` | `--server-from` parent of #20363
(`eebdfe71b1`) | `--workloads Q2 --faults K1 --matrix --seed 107` | 3/3
fail as expected: `answer_reached_agent_while_blocked`. No run starts
between the answer and the finished prerequisite (6 minutes of ready
server). |
| `w-20261110T033426-s108` | this branch, rebased onto #20432 |
`--workloads Q1,Q2 --faults K1 --triggers asked,answered --tasks 4
--seed 108` | Q1^asked and Q2^asked pass. Q1^answered and Q2^answered
fail on a product gap (below). |
| `w-20261110T020037-s102` | this branch, before the rebase |
`--workloads Q1,Q2,Q3 --faults K1 --triggers asked,answered --tasks 3
--seed 102` | Q1 with `asked` and `answered` K1 restarts: both done with
the answer. |

Waves `…-s107` (pre-fix) and `…-s106` (pre-fix) used the same harness
code as the green waves. Only the server build changed.

**New product failure (not fixed here).** A K1 restart 0.3–3 s after the
board's answer can lose the answer turn's native run:

- `w-20261110T033426-s108/t04` (Q1, K1 2.9 s after the answer): run
`a40cc8f7` resumes with `resume_dead_runner`, then fails
`runner_state_identity_mismatch: authority_indeterminate`
(`adapter_failed`). The task shows "Couldn't finish this attempt. Your
message is saved." and needs Retry. Retry works.
- `w-20261110T033426-s108/t03` (Q2, blocked task, K1 0.27 s after the
answer): run `5b5355de` is `process_lost`. Its bounded retry `92a57f50`
fails `native_session_recovery_failed` (`session.snapshot` failed). The
Retry click starts `0e6aa9d1`, which fails
`runner_state_identity_mismatch:
prior_owner_terminal_state_indeterminate;
retirement_refused=journal_commands_unsettled(turn.start.failed)`. The
task then waits with `retryWait.reason=process_identity_missing` and
stays blocked without the answer.
- Suspected path: native runner restart reconciliation of the runner
state identity (`resume_dead_runner`, journal command settlement) in
`server/src/services/native-runtime/` and `packages/paperclip-runner/`.
Host load was 8–14 during the wave.

## Risks

- Test harness only. No product code changes.
- Q2 depends on the model following its instructions. The oracle accepts
the answer in either a comment or the bounded turn's run result, because
both were seen.
- Q3 shifts persisted timestamps only in the wave's isolated database.
It does not shift unrelated tables, so it simulates elapsed time for the
review and the token, not for the whole instance.
- `K0` and the new triggers add values to shared registries (`types.ts`,
`scheduler.ts`). Existing plans keep their meaning.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code with tool use and code execution.

## 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 <noreply@anthropic.com>
2026-11-09 22:37:24 -06:00
DottaandClaude 5ab98b0063 test(restart-chaos): add database contention workload and universal checks U1-U4 (#20421)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The restart chaos harness (`tests/restart-chaos/`) runs real agent
tasks on an isolated control plane and grades them.
> - Two production failures on 2026-11-09 passed the harness. Its tasks
made little output and little accounting, so database write contention
never happened (#20385). Its grading was per workload, so a task stuck
for a reason no scenario targeted was not caught (#20361).
> - The harness needs a contention workload and checks that apply to
every task.
> - This pull request adds the L1 workload, a `--db-pressure` knob, and
the universal checks U1–U4.
> - The benefit is that the #20385 failure class and unexplained stuck
or misleading tasks now fail a wave.

## Linked Issues or Issue Description

Refs #20385, #20383, #20361.

**What happened?**

The harness ran real embedded PostgreSQL, but no task streamed enough
run-log output while usage receipts committed. The #20385 receipt race
(SQLSTATE 40001, retained outbox, `native_event_replay_conflict`) could
not occur. Also, no rule checked every task for an owner or an
actionable item, for retry loops, for diagnosable failures, or for stale
notices.

**Expected behavior**

A chaos wave can create run-row contention between run-log writes and
accounting writes and check that accounting stays exact. Every task of
every wave gets scenario-independent liveness and notice checks.

**Steps to reproduce**

Run an L1 wave with `--db-pressure 4` against the parent of the #20385
merge (red), then against master (green). Commands are in Verification.

## What Changed

- `contention.ts` (new): workload L1 (five 3,000-line shell outputs as
five tool calls, then a brief), its accounting oracle, and
`RunRowPressure`, the `--db-pressure <writers>` knob. The writers run
`update heartbeat_runs set updated_at = updated_at` on the wave
company's running runs, in the wave's own isolated database only.
- L1 oracle: no `native_event_replay_conflict` or
`runner_event_commit_stalled`; each producer event journaled once; a
terminal receipt for each succeeded native run; finished runs accounted;
per-run cost equals its cost events; the board's spend equals the
cost-event sum with no runs pending accounting; and proof that the task
produced contention.
- `universal.ts` (new): U1 liveness, U2 no silent retry loop, U3
diagnosable failures, U4 no stale notices. `grader.ts` grades them on
every task. U1, U2 and U4 fail a task. U3 is a warning.
- `invariants.ts`: `readUniversalDbEvidence` reads failed-run error text
and recovery-action causes from the wave database at wave end, for U3.
- `universal-audit.ts` (new): runs U1–U4 over saved wave evidence to
measure false positives and bound headroom.
- `contention-db.test.ts` (new): on real PostgreSQL, under the pressure
writers, the pre-#20385 repeatable-read receipt write exhausts its
retries with 40001 and the current writer commits.
- Small registry edits: `types.ts` (L1 id, `dbPressure`, optional
`contention` and `universal` fields), `workloads.ts` (L1 entry),
`config.ts` (`--db-pressure`, L1 not a default), `main.ts` (start and
stop the writers, read database evidence before teardown), `ledger.ts`
(universal verdicts and warnings in the ledger and `SUMMARY.md`).
- README: "Database contention" and "Universal checks" sections, with
the audit table and bound reasons.

## Verification

- `contention-db.test.ts` (real embedded PostgreSQL): with
`--db-pressure` writers (4), the pre-#20385 repeatable-read receipt
write exhausted its retries with 40001 in 8 of 8 attempts; the current
READ COMMITTED `persistUsageReceipt` committed 8 of 8. This is the
harness-level red/green for #20385.
- Live L1 waves (`--env local --adapters codex,claude --workloads L1
--faults K1 --concurrency 4 --tasks 4 --seed 20385 --db-pressure 4`),
red on the #20385 parent `bf818280f2` with this harness overlaid and
green on this branch: queued behind the shared one-wave-at-a-time lock
and not yet run. The first red attempt failed at startup (the fresh
checkout lacked a built plugin SDK; no tasks ran). Live results are not
claimed here.

- `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts`:
all harness unit tests pass, including the new `universal.test.ts`,
`contention.test.ts` and `contention-db.test.ts`.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`:
passes.
- Universal-check audit over 74 saved waves (1,592 tasks): U1 7 flags
(one C1 stall and six R1 runs queued 8 min behind a retry row, the
#20361 shape), U2 1, U3 22, U4 0. None of U1, U2 or U4 flags a passing
task or an all-pass wave. U3 flags 3 passing tasks (all
`adapter_failed`), so it ships as a warning.

## Risks

- U1, U2 and U4 can now fail a task that passed R1–R6 and T. The audit
found no such case in 70 saved waves. The bounds are
environment-configurable.
- `--db-pressure` is opt-in and writes no values. It writes only to the
wave's own temporary database.
- Other agents edit the same harness. This change keeps registry edits
small; `grader.ts` and `invariants.ts` changes are additive.
- No product code changes.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code with tool use (shell, file editing, GitHub CLI).

## 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 <noreply@anthropic.com>
2026-11-09 22:02:57 -06:00
DottaandClaude Opus 5.5 0c7a628fd1 test(chaos): read remote R1 files from the execution workspace and record why a read failed (#20440)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness grades Daytona R1 tasks by reading their
files through `GET /issues/:id/file-resources/content`
> - In replays w-20261110T010031-s9913 and w-20261110T012332-s9950 the
tasks recovered (R1, R3, R6 pass), but all 506 content reads returned
422, even after the closing save left the workspace clean (`snapshotId`
set, no writer, `location: null`)
> - The harness discarded each error, so the waves could not show which
422 the server returned
> - This pull request asks for the execution workspace explicitly and
records the last error per file in the task record and the timeline
> - The benefit is that the next wave names the exact cause instead of a
blind `module_present` failure

## Linked Issues or Issue Description

Follow-up to #20348.

**What happened?**
Every file read for a finished Daytona R1 task returned 422, and the
response was discarded.

**Expected behavior**
Failed reads leave their reason in the evidence.

**Actual behavior**
Only the outcome, a `null` file, was kept.

## What Changed

- File reads add `workspace=execution`, so the server reads the issue's
execution workspace and not the `auto` choice.
- `readRemoteWorkspaceFile` and `captureRemoteWorkspaceFiles` report
each failure through a callback. The observer keeps the last message per
file in `record.remoteFileErrors` and logs `remote_file_read_failed`
once per distinct message.
- The remote-workspace file reader stays in `remote-workspace-files.ts`;
`observer.ts` is at 993 lines.

## Verification

- `npx vitest run --config tests/restart-chaos/vitest.config.ts`: 263
passed. The test server now requires `workspace=execution`, so the
existing remote tests were red before, and a new test records the 422
`workspace_unavailable` message.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`: clean.

## Risks

- If a Daytona task's files only exist in a project workspace rather
than its execution workspace, an explicit `execution` selector reports
that clearly instead of falling back.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code
execution in Claude Code.

## 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-09 21:34:35 -06:00
DottaandClaude 1b52f359dd test(restart-chaos): add config fault waves (CF1-CF3) and the CF4 design (#20446)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness (`tests/restart-chaos/`) starts an
isolated control plane, runs real agent tasks and restarts the server
during them
> - Every wave starts from a new instance with valid default settings
and one valid provider
> - On 2026-11-09 production failed on state that a new instance never
has: a setup command that named a renamed script (#20269), and an
account default that did not exist (#20361)
> - This pull request adds a "config fault" family (CF1–CF3): the
harness breaks one saved reference on a task's own agent or project,
restarts the server while the task is parked, repairs the setting and
grades the result
> - The benefit is that the harness now catches "stale saved setting"
failures, including a run that retries forever or a task that stays
Queued with no explanation

## Linked Issues or Issue Description

Refs #20269
Refs #20361

**What existing behavior does this improve?**
The restart chaos harness only tests new instances with valid settings.
It cannot see failures that come from saved settings that point at a
missing object.

**Subsystem affected**
`tests/restart-chaos/` (test harness only). No product code changes.

**Current behavior**
No wave breaks a saved setting. The CF2 failure (#20361) and the CF1
failure (#20269) were found in production, not by the harness.

**Proposed behavior**
`config-wave.ts` runs CF1–CF3. The oracle is not "the task completes". A
task passes when all of these are true:

- it completes, or it rests on a message that names the broken setting
on the task page within a bound;
- no run retries unchanged configuration forever;
- no task stays Queued with no owner;
- the rest survives a K1 or K2 restart;
- after repair and the offered action (connection card, Retry, or agent
setup Retry), the task completes.

## What Changed

- `tests/restart-chaos/config-faults.ts` (new, pure): the catalog of
broken references, the seeded plan, `parkState`, the task-level message
check and `gradeConfigTask`. CF1 is a missing setup script. CF2 is a
missing personal OpenRouter default. CF3 is a seeded walk over a deleted
secret, a missing workspace path, a missing base ref, an unknown model,
plus CF1 and CF2. `CONFIG_REFERENCES_NOT_BROKEN` lists the settings CF3
cannot break, and why.
- `tests/restart-chaos/config-wave.ts` (new): the wave runner. Each task
gets its own agent, credential secret and project. The runner breaks the
setting through board routes, launches an A1 brief task, polls, fires K1
or K2 while the task is parked, repairs the setting, uses the offered
action and grades the task. It reuses the existing supervisor, seed and
A1 oracle. Waves are limited to 4 tasks and a concurrency of 2. The
instance is deleted at the end.
- `tests/restart-chaos/config-faults.test.ts` (new): unit tests for the
plan, the arguments, `parkState` and the grader. These include the CF2
bug signature and the CF1 "message only on the run page" signature.
- `tests/restart-chaos/README.md`: a new "Config faults" section and a
row in the pieces table.
- `doc/plans/2026-11-09-chaos-config-and-aged-state-faults.md`: the CF4
design (an instance aged by an older release, built on the existing K14
`--upgrade-from` machinery). CF4 is not built in this pull request.
- There are no edits to `main.ts`, `types.ts`, `workloads.ts`,
`invariants.ts` or `grader.ts`. Other agents work on those files in
parallel.

## Verification

- `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts`:
24 files, 291 tests pass (before the last unit test was added). The
config-fault suite has 13 tests, and all pass.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`:
passes.
- Live waves (local, isolated instance, one wave at a time):
  - **CF2 red before #20361** (base `8004ad069e`, before #20361 merged):
- Waves `w-20261110T000602-s201-cf` and `w-20261110T004818-s202-cf` fail
with
`CF2/ai_default:bounded_rest+actionable_message+lifecycle_retrying_unchanged_config+queued_without_live_owner`.
- The user sees `Queued`, a `resume` user action, and "Execution
configuration check failed. Paperclip could not complete this step. It
will retry automatically." There is no card.
- A debug run with logging added to the lifecycle failure log (not
committed) showed that the underlying error is
`ai_connection_default_missing`. Verification retried it every 60 s,
more than 20 times.
- After the harness connects an account that is installed for the agent,
the task completes 35 s later on its own.
- The CF2 run after #20361, and the CF1 and CF3 waves on current master,
are added below as they finish.

## Risks

- Low risk. The change is test harness code and docs only.
- A wave costs provider tokens only when the repaired task runs (one A1
brief).
- The CF2 repair creates a real OpenRouter account in the isolated
instance. The server validates the key live.
- The oracle reads the task page through the API (interruption,
execution status, cards, agent setup status and notices). It does not
render the board UI. A message that appears only in the run's own error
text is graded `message_only_in_run_detail`.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, through Claude Code
with tool use (shell, file edits, subagents). Standard context window.

## 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 <noreply@anthropic.com>
2026-11-09 21:10:42 -06:00
DottaandClaude 4158799d1b test(chaos): add provider message faults (PV1-PV3) and a saved-question scenario (#20432)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The restart chaos harness (`tests/restart-chaos`) tests that agent
work survives faults.
> - The harness has two fault families: control-plane restarts (K*) and
storage faults (ST*).
> - On 2026-11-09, two production failures came from a third family. The
provider was alive, but a protocol message was late or missing (#20330,
#20336).
> - The harness could not produce that family, so it did not catch these
failures.
> - This pull request adds provider message faults (PV1 delay, PV2 drop,
PV3 duplicate) on the real path between the runner and the Codex
app-server. It also adds a saved-question scenario (Q1) that uses the
faults.
> - The benefit: the harness can now find this class of failure, and it
fails against both pre-fix commits.

## Linked Issues or Issue Description

Refs #20330
Refs #20336

**What happened?**
A Codex interruption terminal that arrived after 18 s failed a governed
wait (#20330). A dropped terminal after a saved handoff failed shutdown
settlement with "Couldn't finish this attempt" (#20336). The chaos
harness could not inject either fault.

**Expected behavior**
The harness can delay, drop or duplicate a chosen provider protocol
message for one task. A scenario fails on the pre-fix product code and
passes on master.

**Steps to reproduce**
Run the wave commands in Verification. Run them on master and in a
worktree at the pre-fix commit with `tests/restart-chaos` copied from
this branch.

## What Changed

- `provider-faults.ts` and `provider-shim/`: the PV1–PV3 fault family.
- A resolve hook goes into the wave's server through `NODE_OPTIONS`. It
answers only the `@openai/codex/package.json` lookup that product code
uses to find the local Codex. It returns a stand-in package in the
instance temp root, and then removes itself from `NODE_OPTIONS`.
- The stand-in `codex` bin is a stdio shim. It starts the real Codex
with the same arguments and relays JSON lines unchanged. It changes only
the message that the task's rule names: the `turn/completed` after a
`turn/interrupt` (`terminal`) or the response to `turn/interrupt`
(`interrupt_ack`). Each rule applies once per task. The task nonce binds
the rule to the task.
- Every decision goes to `<wave>/provider-faults.jsonl`. A PV task fails
`provider_fault_applied` when its fault did not happen.
- There is no product change. Thus the same harness runs against older
product commits.
- `question-handoff.ts`: Q1 workload. The agent saves one question with
`request_human_input`. After the asking run settles, the board answers
through the respond route. The oracle checks that the question was asked
once, the asking run succeeded, no run failed, nothing ran before the
answer, and the brief has the answer.
- PV faults run only with `--env local --adapters codex`, only with
workloads that interrupt a turn (Q1, S1, G1), and not in the same wave
as restart faults. A PV1 delay passes only when the held message was
released.
- Registry edits: `--faults` accepts PV1–PV3. New options are
`--provider-fault-message` and `--provider-fault-delays` (a sweep,
cycled across tasks). The edits are in `types.ts`, `config.ts`,
`scheduler.ts`, `workloads.ts`, `observer.ts` (Q1 board step) and
`main.ts` (install the seam and add the check).
- README section with commands, the seam, and what is not covered.

## Verification

- Unit tests: `pnpm exec vitest run --config
tests/restart-chaos/vitest.config.ts`. 23 files and 279 tests pass on
the final head. The new tests drive the real shim against a stand-in
provider for each kind (delay, drop, duplicate, interrupt ack, once per
task, held message discarded on exit, a multibyte character split across
writes). They also run the resolve hook in a child Node process.
- Typecheck: `node scripts/typecheck-test-projects.mjs
tests/restart-chaos` passes.
- Live waves (real Runner, real Codex 0.160.0, gpt-6.1-sol, macOS
arm64):

Pre-fix runs use a worktree at the merge parent with
`tests/restart-chaos` copied from this branch. Common arguments: `--env
local --adapters codex --workloads Q1`.

  | Scenario | Product code | Wave | Result |
  | --- | --- | --- | --- |
| 1. PV1, terminal 18 s late (`--faults PV1 --provider-fault-delays 18
--tasks 1 --seed 101`) | master (`8004ad069e`) |
`w-20261110T000333-s101` | pass: terminal held 18 s and released, asking
run `succeeded`, one question, brief has the answer |
| 1. same | `34c1e6469c` (parent of #20330 merge) |
`w-20261110T014734-s101` | **fail**: Codex stopped at 14.9 s with the
terminal still held, asking run `failed
native_provider_terminal_failed`, banner "Couldn’t finish this attempt.
Your message is saved.", Retry needed (R3) |
| 1. sweep 5/14/25/29 s (`--faults PV1 --provider-fault-delays
5,14,25,29 --concurrency 2 --tasks 4 --seed 102`) | master |
`w-20261110T015426-s102` | 4/4 pass |
| 1. same sweep | `34c1e6469c` | `w-20261110T015708-s102` | 5 s and 14 s
pass; **25 s and 29 s fail** (`native_provider_terminal_failed`, Retry
needed): the old 15 s drain |
| 2. PV2, terminal dropped (`--faults PV2 --tasks 1 --seed 103`) |
master | `w-20261110T015239-s103` | pass: terminal dropped, asking run
`succeeded` through verified shutdown, one question, nothing ran before
the answer |
| 2. same | `a67b936885` (parent of #20336 merge) |
`w-20261110T015010-s103` | **fail**: asking run `failed
native_provider_terminal_failed`, banner "Couldn’t finish this attempt",
Retry needed (R3) |

The first master wave exited 1 for one cold `agent_boot` speed sample
(8.6 s against a 7 s p95 budget), before any fault, while a preflight
build ran in parallel. Its task passed. No new product failure was found
on master.

## Risks

- Test-only. No product code changes.
- The shim adds one Node process in front of Codex for PV waves only.
Other waves do not change.
- The nonce binds a rule to a Codex process. If one Codex process serves
two tasks, the second task's traffic can bind it too. Q1 tasks use
separate sessions.
- Claude is not covered. Claude runs through the ACPX sidecar against a
hash-verified provider pack, and its ACP protocol has no interrupt
acknowledgement. A Claude shim needs a different seam.

## Model Used

Anthropic Claude Opus 5.5 (`claude-opus-5-5`), through Claude Code, with
tool use, code edits and local execution.

## 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
- [x] 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 <noreply@anthropic.com>
2026-11-09 20:31:18 -06:00
DottaandClaude Opus 5.5 490d97bf1e test(perf): guard Mine against waiting on the company-wide workspace list (#20419)
> Follows #20388 (merged as 50df9a19b9). This PR adds only test and
fixture files. It changes no product code.

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The Mine inbox (`/:companyPrefix/issues?view=mine`) is the first
page an operator opens
> - On an instance with 8,758 execution workspaces, Mine took about 13 s
to show rows. It loaded every workspace in the company
(`execution-workspaces?summary=true`, 2.5 MB) and kept all rows hidden
until that read finished
> - #20388 fixes this. Mine now reads only the workspaces its rows
reference (`ids` filter), and waits for them only when the rows show
workspace data
> - The collections perf suite did not catch the regression. Its fixture
has almost no execution workspaces, and it has no check on the workspace
read
> - This pull request adds a workspace-scale fixture and a spec that
fails if Mine requests the company-wide list again, or if Mine rows wait
for the workspace read when no workspace data is shown
> - The benefit is a regression guard that fails on `master` before
#20388 and passes with it

## Linked Issues or Issue Description

Refs #20388

**What happened?**
The collections perf suite could not detect that Mine waited on a
company-wide execution-workspace list. The fixture had no workspace
scale, and no assertion covered the workspace request or the reveal
gate.

**What did you expect?**
A browser check that fails when Mine requests the unfiltered workspace
list, or when Mine rows wait on a workspace read that does not change
what the rows show.

**Steps to reproduce**
Run `tests/perf/collections` against `master` UI assets on a company
with thousands of execution workspaces. All existing checks pass,
although Mine requests the full list and holds rows for it.

## What Changed

- `tests/perf/collections/workspace-fixture.ts` (new): a retained
company with 6,000 execution workspaces. 4,000 of them belong to
finished tasks. 12 Mine tasks each run in their own isolated workspace.
Mine tasks come from the API. The API cannot create execution
workspaces, so the bulk rows go to the disposable instance database
directly (same pattern as
`tests/e2e/legacy-failure-continuation.spec.ts`). The database URL comes
from `PAPERCLIP_CONFIG` or `PAPERCLIP_COLLECTION_PERF_DATABASE_URL`,
loopback only. The fixture uses the existing retain/404 rule.
- `tests/perf/collections/mine-workspaces.spec.ts` (new), cold load of
`/:prefix/issues?view=mine`:
1. Mine requests only `ids`-filtered `execution-workspaces?summary=true`
reads, and only for the workspaces its rows reference. No unfiltered
read.
2. With the Workspace column off and no workspace grouping, all Mine
rows appear while every workspace read is held.
3. With the Workspace column on, each row shows its isolated workspace
name.
4. Records cold time from navigation start to the first Mine row (two
frames after mount, as in `measure-mine-context.ts`) over at least 5
runs. It attaches the samples and the median. It is not a budget.
- `tests/perf/collections/README.md`: documents the fixture, its
database prerequisite, and the checks.

## Verification

Local disposable instance (`onboard --yes --run`, `local_trusted`,
embedded PostgreSQL, `PAPERCLIP_UI_DEV_MIDDLEWARE=false`, production UI
builds). Both arms used the same server (this branch) and the same
database. Only the UI assets changed, so this is a UI comparison. The
`master` UI does not send `ids`.

```sh
PAPERCLIP_CONFIG=<instance>/config.json PAPERCLIP_COLLECTION_PERF_BASE_URL=http://127.0.0.1:3219 \
  pnpm exec playwright test --config tests/perf/collections/playwright.config.ts mine-workspaces.spec.ts
```

**Fails on `origin/master` UI at 5858cef8f9 (before #20388 merged):**

```
✘ 1 Mine reads only the execution workspaces its rows reference
    Error: Mine requested the company-wide list of 6000 workspaces
    +   "/api/companies/<id>/execution-workspaces?summary=true",
✘ 2 Mine rows appear while workspace reads are pending when no workspace data is shown
    Error: Mine rows waited for the workspace read
    expect(locator).toBeVisible() failed … timeout 15000ms
✓ 3 the Workspace column resolves each Mine row's isolated workspace name
✓ 4 records cold time to Mine rows
2 failed, 2 passed
```

**Passes on this branch (#20388 UI):**

```
✓ 1 Mine reads only the execution workspaces its rows reference
✓ 2 Mine rows appear while workspace reads are pending when no workspace data is shown
✓ 3 the Workspace column resolves each Mine row's isolated workspace name
✓ 4 records cold time to Mine rows
4 passed
```

**Cold time to first Mine row, 5 runs each, fresh browser contexts,
1440×900:**

| Scenario | `master` UI median (samples) | This branch median (samples)
|
| --- | --- | --- |
| Native loopback, no interception | 472 ms (461, 466, 472, 483, 523) |
425 ms (406, 417, 425, 429, 998) |
| Workspace reads delayed 3,000 ms (diagnostic, intercepted) | 3,523 ms
(3488–3539) | 546 ms (471–1754) |

On this fixture the unfiltered list is 1.55 MB but returns in about 40
ms on loopback. The filtered read is 3.2 KB in about 4 ms. So native
loopback timing does not separate the arms. The delayed scenario shows
the dependency: on `master`, rows wait for the full workspace delay. No
hard time budget is added. The suite has no CI timing convention, and
the structural assertions are the guard.

**CI status of this suite:** `tests/perf/collections` runs in no GitHub
workflow. It is a manual suite against a disposable local instance (see
its README). The new spec runs the same way as the existing specs: same
directory, same `playwright.config.ts`, no new config. The Vitest checks
in #20388 (`ui/src/pages/Inbox.test.tsx`) remain the CI guard for the
reveal rule.

**Full collections suite on this branch:** 19 passed, 5 failed. The 5
failures are existing cases (`startup-regressions`: two partial-source
cases and two 429 cases; `startup`: "Coordinator Inbox retains aggregate
Mine reads when Streamlined UI is disabled"). They fail the same way on
the `origin/master` UI with the new spec excluded. This PR does not
cause them.

`pnpm verify`: exit 2 (README is a global CI input), so required CI
supplies broad coverage. `tsc -p tests/tsconfig.playwright.json` passes
(the CI typecheck project that covers `tests/perf`).

## Risks

- Low risk. Test and fixture files only.
- The fixture writes rows straight to the database. It needs
`PAPERCLIP_CONFIG` or `PAPERCLIP_COLLECTION_PERF_DATABASE_URL`, and
refuses non-loopback databases. Without either variable the spec fails
with a clear message.
- The timing case changes the real `enableIsolatedWorkspaces` instance
setting and restores it in `finally`.
- Seeding 6,000 workspaces takes about 10 s on the first run. Later runs
reuse the retained fixture.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with
tool use (shell, file edits, Playwright runs). Extended reasoning on.

## 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-09 20:29:56 -06:00
Nicky LeachandClaude Opus 5.5 ff4ba7d319 perf(runner): run launch artifacts in place from a trusted read-only root (#20428)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The Rust Runner (`paperclip-runnerd`) starts provider sidecars and
proxies from qualified launch artifacts: a Node runtime and the scripts
it runs
> - Since #12699, Runner copies each artifact into a sealed memfd and
checks its SHA-256 on every verification. This defends against a path
swap or an in-place rewrite between the check and the exec
> - On a hosted agent computer (4 vCPU), the copy and digest of the 127
MB Node takes about 0.26–0.30 s warm and about 1.2 s right after a wake.
This is on the path from `session.open` to the sidecar spawn, and again
on every warm `run.attach`. Each Runner also holds about 125 MiB of
shmem for the copy
> - On that computer, the artifacts live on the Runner ROM: a read-only
erofs image that the deploy pins by digest. The threat that the memfd
defends against does not exist there. On local hosts the artifacts are
writable, so the memfd must stay
> - This pull request adds an explicit, path-scoped, default-off
`--trusted-launch-root <dir>`. Runner runs an artifact in place only
when its open descriptor proves that it is on an immutable mount under
that root. Everything else keeps the sealed, hashed snapshot
> - The benefit is a shorter `session.open` and `run.attach` on agent
computers, and about 125 MiB less shmem per Runner, with no change where
the flag is absent

## Linked Issues or Issue Description

**What existing behavior does this improve?**
Launch-artifact verification in `paperclip-runnerd`
(`qualified_launch.rs`). Its callers are the ACPX `verified_transport`
(every `session_config`, including the park rebind) and the OpenCode and
Cursor Cloud proxy launches.

**Subsystem affected**
Rust Runner (`packages/paperclip-runner/runner/crates/runner-core`).

**Current behavior**
Every verification opens the artifact, copies all bytes into a sealed
memfd, and hashes them. Node is 127 MB. The cost is described in the
Thinking Path.

**Proposed behavior**
When the host passes `--trusted-launch-root /opt/paperclip/runner` and
the root passes its checks, Runner holds each admitted artifact open in
place and executes it through `/proc/self/fd/N`. It does not copy or
re-hash it. Without the flag, nothing changes.

**Reason and benefit**
See the Thinking Path.

Refs #12699 (introduced the sealed memfd). Refs #20423 (overlap, see
below).

## What Changed

- `qualified_launch.rs`: new `TrustedLaunchRoot` and
`set_trusted_launch_root` (process-global, set once at startup, like
`--task-memory-cgroup`).
- **Root checks at startup.** The path must be absolute, already
canonical (no symlink or `..`), a directory, and on a read-only,
exec-permitted erofs or squashfs mount. If a check fails, runnerd logs a
warning and keeps today's behavior.
- **Per-artifact checks, on the open descriptor.** After today's checks
(regular file, not a symlink, not group- or world-writable, open-time
identity), the descriptor must be a regular file on the root's device.
Its kernel path (`readlink /proc/self/fd/N`) must lie under the root by
path components, so `/opt/paperclip/runner2` does not match. Its mount
must pass `fstatvfs` (`ST_RDONLY` set, `ST_NOEXEC` clear) and `fstatfs`
(erofs or squashfs magic). If any check fails, Runner logs the reason
and uses the sealed snapshot.
- **Admitted artifacts** become
`VerifiedProcessArtifact::in_place_on_trusted_root` (crate-private). The
held file goes through the existing `inherited_artifact` path, so the
exec is `/proc/self/fd/N` and `PAPERCLIP_VERIFIED_RUNTIME_EXECUTABLE`
still names a descriptor that nested launches can reopen.
- New `verify_assigned_executable_launch_artifact` never uses the root.
`stdio_mcp.rs` now calls it, so host-assigned stdio MCP binaries always
keep the sealed snapshot.
- `paperclip-runnerd.rs`: parse `--trusted-launch-root <dir>`. Older
runnerd builds ignore unknown flags, so a host can pass it before every
Runner understands it.
- The trusted path is Linux-only. On macOS and Windows,
`TrustedLaunchRoot::open` always refuses.
- `tests/runner-e2e/SECURITY.md`: documents how the Kimi and Pi
provider-death fixtures behave under a trusted root.

### Decision: the trusted path drops the digest

The trusted path does not hash the artifact, not even once per process:
- The ROM image is pinned by digest at deploy time.
- On the agent computer, the Launcher computes the expected
`--acpx-sidecar-command-sha256` and `--acpx-sidecar-script-sha256` from
the same ROM files immediately before it starts Runner. A re-hash of an
immutable file can only repeat that result.
- One hash per process would not help `session.open`, because each task
starts a new Runner process.
- The mount check is the guard: erofs and squashfs have no write path,
so the bytes cannot change while the descriptor is open.

### Fixtures that assert the sealed memfd

- Untrusted-mode assertions are unchanged. The fixtures do not pass the
flag.
- Kimi (`kimi-provider-fault.py` `checked_parent_descriptor`): with a
trusted root, the parent descriptor is the ROM's Node file (one link).
The existing linked-descriptor branch handles it: same inode as
`/proc/<pid>/exe`, and the complete bytes hashed against `nodeSha256`.
That branch never accepts the file as a memfd. The existing test
`test_linked_descriptor_keeps_regular_single_link_checks_without_memfd_claim`
covers it.
- Pi provider-death admission binds `/proc/self/fd/3|7` to the native
distribution snapshot's Node. The TypeScript ACPX closure copies that
file whether or not a trusted root is set, so the flag does not affect
it.

### Overlap with #20423

#20423 (draft) moves the same copy and hash to Runner boot and reuses
the result. Where this flag is set (Cloud agent computers), there is
nothing left to precompute, so #20423 is mostly redundant there. #20423
still helps hosts without a trusted root (local and sandbox Runners).
This PR does not change #20423.

## Verification

All runs are on Linux (aarch64, Rust 1.97.1, in a container):
- `cargo fmt --manifest-path runner/Cargo.toml --all -- --check`
- `cargo check --manifest-path runner/Cargo.toml --locked --workspace`
- `cargo test -p paperclip-runner-core --lib -- qualified_launch
trusted_root commonjs`: 12 passed. The new tests are:
-
`an_artifact_under_an_immutable_trusted_root_runs_in_place_without_a_digest`:
trusted-root hit, with no memfd, the same inode as the source, and no
re-hash.
- `an_artifact_outside_the_trusted_root_keeps_the_sealed_snapshot`: a
path outside the root is sealed, and a wrong digest still fails.
- `a_sibling_with_the_root_as_a_name_prefix_is_outside_the_root`:
`rom2/` does not match `rom`.
- `a_root_that_is_not_read_only_falls_back_to_the_sealed_snapshot`: the
real probe refuses a writable root at startup. A per-artifact mount
failure falls back, and a wrong digest still fails.
  - `the_real_mount_probe_rejects_a_writable_filesystem`
- `symlink_and_parent_escapes_from_the_root_fall_back`: a directory
symlink out of the root, a `..` path out of the root, and a
final-component symlink (still refused).
  - `a_root_must_be_absolute_canonical_and_a_directory`
- `without_a_trusted_root_launch_artifacts_are_sealed_and_hashed`: flag
absent, behavior unchanged.
  - `host_assigned_executables_ignore_the_trusted_root`
-
`trusted_root_artifacts_execute_the_held_file_and_reopen_it_for_nested_launches`:
Runner execs from the held file, and a child re-execs
`$PAPERCLIP_VERIFIED_RUNTIME_EXECUTABLE`. Both report the source file as
`/proc/self/exe`.
- `cargo test -p paperclip-runner-core --test stdio_mcp --test
process_supervisor`: 4 and 8 passed.
- Ad hoc, not committed: a privileged container with a real loop-mounted
squashfs. The real `fstatvfs`/`fstatfs` probe admitted a `ro` mount and
refused a `ro,noexec` mount. The unit tests replace only that mount
probe, because CI cannot mount filesystems.
- Not verified: a run on an agent computer with the real erofs ROM, and
the latency and memory savings there. The Cloud Launcher change that
passes the flag is a separate follow-up.

## Risks

- **This change needs review from a security owner before merge.** It
removes the sealed copy and the per-launch digest for admitted
artifacts. That relies on the host's assertion that the root is a
digest-pinned immutable image, plus the descriptor checks above.
- **Misconfiguration fails closed.** A wrong, writable, `noexec`,
non-canonical or missing root only loses the speedup, and runnerd logs a
warning. There is no global "trust everything" switch, and Runner never
selects a root from the filesystem it finds.
- **Residual trust.** On an admitted file, Runner no longer detects a
ROM image that differs from what the launch profile expected. On the
agent computer, the Launcher derives the expected digests from the same
ROM. Another host that passes the flag must pin its image the same way.
An attacker with root in the guest (who could write the backing block
device or remount) is out of scope; such an attacker can already control
runnerd.
- **Allowlist.** Only erofs and squashfs are accepted, because they
cannot be written through any mount. A read-only bind mount of a
writable filesystem is refused. A ROM that changes to another filesystem
type will silently fall back to the sealed path (slower, still safe).
- No migration. Behavior is unchanged when the flag is absent. Older
runnerd builds ignore the flag.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with
tool use (shell, file edits, container builds). Extended reasoning was
on.

## 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-09 17:46:33 -08:00
DottaandClaude Opus 5.5 b184e40108 perf(ui): serve every project list from one includeArchived read (#20395)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The board UI reads the company project list in many places: the
sidebar, starred projects, the command palette, the new-task dialog, and
archive-aware pages such as the Mine inbox
> - Active-only consumers read `GET /projects`, and archive-aware ones
read `GET /projects?includeArchived=true`, under two React Query keys
> - A Mine load therefore fetched the same large payload twice (580–590
KB each on our dev instance)
> - The two responses differ only by archived rows; the server's active
list is exactly the rows where `archivedAt` is null
> - This pull request makes every consumer share the
`includeArchived=true` query, and active-only consumers filter it with
`select`
> - The benefit is one project read per page instead of two

## Linked Issues or Issue Description

**What existing behavior does this improve?**
Duplicate project list reads on page load.

**Subsystem affected**
Board UI project queries (`ui/src/lib/company-projects-query.ts` and its
consumers).

**Current behavior**
A cold Mine load sends both `/companies/:id/projects` (sidebar) and
`/companies/:id/projects?includeArchived=true` (inbox).

**Proposed behavior**
One `includeArchived=true` read serves both. Active-only consumers apply
`selectActiveProjects`.

**Reason and benefit**
The project list is one of the largest shell payloads. Reading it once
saves a large response on every page that has both kinds of consumer.

## What Changed

- New `companyProjectsQuery(companyId, { includeArchived? })` in
`ui/src/lib/company-projects-query.ts`. It always uses the
`includeArchived: true` key and fetch. For active-only use it adds a
stable, module-level `select` that keeps rows without `archivedAt`.
- 22 files that read the active list now spread
`companyProjectsQuery(...)`, and so do the plugin SDK project picker and
16 archive-aware pages that wrote the shared key and fetcher inline.
Their `enabled` and other options are unchanged. `OnboardingWizard`'s
one imperative read is unchanged. `pages/Inbox.tsx` already reads the
same key and fetcher, so it shares the entry without an edit.
- Invalidation: project mutations and live events invalidate
`queryKeys.projects.all(companyId)`, which matches the shared key. The
onboarding wizard invalidated the old active-only key; it now
invalidates `projects.all`.
- Tests: new `company-projects-query.test.tsx` (one request for both
consumers, correct filtering, and one refetch after `projects.all`
invalidation). Three existing tests (`CommandPalette`, `CompanyExport`,
`GitHubBotManagement`) now seed or expect the shared key.
- New browser check
`tests/perf/collections/shared-project-list.spec.ts`: after the Mine
inbox read completes, the page has sent no active-only project request.

## Verification

- `pnpm verify` passes.
- `ui`: the consumer suites (`CoordinatorSidebar`, `SidebarProjects`,
`SidebarStarredProjects`, `CommandPalette`, `NewIssueDialog`,
`AgentWorkPanels`, `CoordinatorNeedsYou`, `IssueDetail`,
`ProjectDetail`, `Projects`, `CompanyExport`, `ChatExecutionDefaults`,
`GatewaysList`, `Search`, `IssueProperties`, `persisted-query-cache`)
pass.
- Browser check:
`PAPERCLIP_COLLECTION_PERF_BASE_URL=http://127.0.0.1:<port> pnpm exec
playwright test --config tests/perf/collections/playwright.config.ts
shared-project-list.spec.ts`. It passes on this branch and fails on
`master`.

## Risks

- A page that needs only active projects now downloads archived rows
too. For most companies that is a small difference; the duplicate read
it removes is a full copy.
- The server does not order either list, and consumers sort for display,
so filtering on the client keeps behavior.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with
tool use (shell, file edits, browser). 1M-token context window. Extended
thinking was off.

## 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-09 17:42:47 -06:00
DottaandClaude Opus 5.5 c7eb35809e perf(ui): read skills and routines only when an editor mounts (#20392)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The board UI wraps every page in an `EditorAutocompleteProvider`
that supplies slash commands to Markdown editors
> - The provider read the full company skills and routines lists as soon
as a company was selected, on every page
> - The Mine inbox has no editor, but it still paid for both reads:
259–284 KB for skills and 225–333 KB for routines on our dev and staging
instances, with skills taking 8.5–13.7 s on a saturated staging server
> - Those reads compete with the inbox rows for the same server
> - This pull request starts the two reads only when the first Markdown
editor mounts
> - The benefit is two fewer large reads on every page without an
editor, and no change for pages that have one

## Linked Issues or Issue Description

**What existing behavior does this improve?**
The requests that every board UI page sends on load.

**Subsystem affected**
Board UI context (`ui/src/context/EditorAutocompleteContext.tsx`).

**Current behavior**
A cold load of `/:prefix/issues?view=mine` sends `GET
/companies/:id/skills` and `GET /companies/:id/routines` although
nothing on the page renders them.

**Proposed behavior**
The provider keeps both queries disabled until a component calls
`useEditorAutocomplete()`. `MarkdownEditor` is the only caller, so the
reads start when an editor mounts: a task composer, the new-task dialog
when it opens, a comment box.

**Reason and benefit**
Each page without an editor saves two large reads. Pages with an editor
read the same data at mount, before a user can type `/`.

## What Changed

- `EditorAutocompleteProvider` holds a `requested` flag. Both queries
are `enabled` only after it is set.
- `useEditorAutocomplete()` sets the flag in an effect on first use. The
flag stays set, so the lists stay current through the existing
live-update invalidations for skills and routines.
- New unit test `EditorAutocompleteContext.test.tsx`: no reads without
an editor; one read of each list when two editors mount.
- New browser check `tests/perf/collections/shell-editor-reads.spec.ts`:
a Mine load sends no `skills` or `routines` request.

## Verification

- `pnpm verify` passes.
- `ui`: `npx vitest run src/context/EditorAutocompleteContext.test.tsx
src/components/task-chat/TaskChatComposer.test.tsx` passes.
- Browser check against a disposable local instance:
`PAPERCLIP_COLLECTION_PERF_BASE_URL=http://127.0.0.1:<port> pnpm exec
playwright test --config tests/perf/collections/playwright.config.ts
shell-editor-reads.spec.ts`. It passes on this branch and fails on
`master`.
- Manual: on a local instance the Mine load dropped from 34 to 32 API
requests. Opening a task shows the composer; `/` lists skills and
routines as before.

## Risks

- Low risk. On the first editor mount the slash menu is empty until the
two reads return. On `master` the same reads ran at app start, so a user
who typed `/` within the first moments of a page with an editor saw the
same gap.
- `attention/summary` is not changed. The navigation's "Needs you" count
renders it on the Mine page.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with
tool use (shell, file edits, browser). 1M-token context window. Extended
thinking was off.

## 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-09 17:42:39 -06:00
DottaandClaude Opus 5.5 7e93ccb4b3 perf: fetch recent task summaries in one request (#20389)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The board UI sidebar shows the person's recent tasks on every page,
including the Mine inbox
> - The sidebar refreshes those rows through `GET
/companies/:id/issues/summaries`, which accepted at most five IDs
> - `useRecentTasks` sent the chunks one after another, so a 25-task
history cost five sequential requests
> - On staging each request took 3–6 s, so the recent-task rows settled
about 20 s after load and every request competed with the inbox rows
> - This pull request raises the per-request cap to 50 and fetches any
remaining chunks in parallel
> - The benefit is one summaries request for a typical history instead
of a serial chain

## Linked Issues or Issue Description

**What existing behavior does this improve?**
The refresh of the sidebar's recent tasks on page load.

**Subsystem affected**
Board UI (`ui/src/hooks/useRecentTasks.ts`) and the `/issues/summaries`
route bound (`packages/shared`, `server/src/routes/issues.ts`).

**Current behavior**
A history of N recent tasks sends ceil(N/5) summaries requests in
series. Measured on staging on 2026-11-09 for a cold Mine load: five
requests of five IDs, started at 1.0 s, 4.3 s, 8.1 s, 12.3 s and 18.4 s,
each taking 3.3–6.1 s.

**Proposed behavior**
Up to 50 recent tasks are one request. Longer histories send their
chunks at the same time.

**Reason and benefit**
The staging server is saturated, so each extra serial round trip delays
the sidebar and the inbox rows. One request removes four round trips
from a typical load.

## What Changed

- `ISSUE_SUMMARIES_MAX_IDS` is now 50 (was 5). The shared validator, the
route's 400 message, the service guard and the OpenAPI description use
the new bound.
- `useRecentTasks` builds all chunks first and fetches them at most
three at a time (`RECENT_TASK_SUMMARY_CONCURRENCY`). The recent list has
no size cap, so this keeps a long history from becoming a burst. The
cache key, omission handling and pruning are unchanged. A failed chunk
still fails the batch, so nothing is pruned on a partial failure.
- Tests: the route test accepts exactly `MAX` IDs and rejects `MAX + 1`;
a hook test checks that at most three chunks are in flight; the API test
uses the constant; `useRecentTasks` tests cover a 25-task history in one
request and a 103-task history with three chunks in flight together,
with and without a failed chunk.
- New browser check
`tests/perf/collections/recent-task-summaries.spec.ts`: a Mine load with
a 12-task history sends summaries requests that each carry all 12 IDs,
and the server answers 200 with all 12 tasks.

## Verification

- `pnpm verify` (selected Vitest files) passes.
- `server`: `npx vitest run src/__tests__/issue-summaries-routes.test.ts
src/__tests__/issue-display-status.test.ts` passes (embedded
PostgreSQL).
- `ui`: `npx vitest run src/hooks/useRecentTasks.test.tsx
src/api/issue-summaries.test.ts
src/components/task-chat/TaskChatBlockerLinks.test.tsx
src/components/CoordinatorSidebar.test.tsx
src/components/SidebarRecentTasks.test.tsx` passes.
- Browser check against a disposable local instance:
`PAPERCLIP_COLLECTION_PERF_BASE_URL=http://127.0.0.1:<port> pnpm exec
playwright test --config tests/perf/collections/playwright.config.ts
recent-task-summaries.spec.ts`. It passes on this branch. With
`master`'s hook and constant it fails: three requests of 5, 5 and 2 IDs.

## Risks

- Low risk. The route still validates UUIDs, deduplicates, and applies
the same read authorization per row. Blocker attention is computed with
set-based queries, so 50 IDs is the same number of queries as 5.
- During a rolling deploy, a new UI against an old server would get a
400 for a history above five tasks. The server serves the UI bundle, so
both change together on a normal deploy. The sidebar keeps its stored
snapshot when the refresh fails.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with
tool use (shell, file edits, browser). 1M-token context window. Extended
thinking was off.

## 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-09 17:42:32 -06:00
DottaandPaperclip eebdfe71b1 fix(ui): show submitted screenshots and Markdown in steering messages (#20367)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - People send follow-up messages while an agent works.
> - The queue above the composer holds those messages until delivery.
> - The queue showed raw Markdown and had no screenshot previews.
> - Submitted screenshots stayed in the composer until the save response
arrived.
> - This pull request shows formatted text and previews in the queue at
submission.
> - It keeps upload receipts so failed saves can restore the draft and
its files.

## Linked Issues or Issue Description

**What happened?**

Send a message with Markdown and screenshots while an agent works. The
queue shows Markdown source. It has no screenshot previews. The composer
keeps the submitted screenshots until the save response arrives.

**Expected behavior**

Show formatted text and screenshot previews in the queue. Clear the
submitted previews with the text. Restore them if the save fails. Keep
files added for the next message.

**Steps to reproduce**

1. Open the Screenshot Submission Storybook case under Steering and
queued messages.
2. Add a screenshot and formatted text. Select Send.
3. Leave the simulated save pending. Inspect the queue and composer.
4. Reject the save. Confirm the draft and screenshots return. Retry and
complete the save.

Related work: #19321 bounds the queued-message list. This fix keeps that
scrolling behavior.

## What Changed

- Hide submitted attachment previews as soon as Send clears the text.
Keep their upload receipts until the request settles.
- Use the shared Markdown renderer in queued rows. Show up to three
compact screenshots with a count for more images. Strip standalone image
syntax from the text preview.
- Keep paragraph boundaries. Wrap thumbnails and controls within narrow
rows.
- Load queue previews only from local attachment and asset content
paths. Add an opt-in image omission mode to the shared Markdown renderer
for compact queue text.
- Correct the case lookup API summary so the existing search test passes
with the full catalog.
- Add regressions for successful, rejected, and uncertain saves. Check
files added to the next draft.
- Add a production-component Storybook case with controlled save
responses and a desktop/mobile browser test.
- Document the queue display and draft recovery behavior in DESIGN.md.

## Verification

- Red: the original components fail five new unit cases. The desktop and
mobile browser cases also fail on raw Markdown. Screenshots show the
previews still in the composer.
- Green: 256 composer, queue, and shared Markdown unit tests pass.
- Related coverage: 466 thread, draft, and attachment tests pass. One
existing test is skipped.
- Three browser cases pass. Desktop and mobile cases check pending save,
rejection, retry, successful save, and steering. A 320px case checks
separate paragraphs with no margins, three loaded thumbnails, the
additional-image count, and auto-send controls. It checks that all
controls and images fit within the queue.
- UI typecheck and design token gates pass after the review fixes. The
initial implementation also passed the UI production build, Storybook
catalog checks, and 30 map/catalog tests. The final PR workflow passed
typecheck, build, all required test suites, and browser E2E checks.
- Browser command: `pnpm exec playwright test
queued-message-screenshots.spec.ts
--config=tests/storybook-visual/playwright.config.ts --project=stories`.
Use `STORYBOOK_TEST_BASE_URL` to target an owned Storybook server.
- Review regressions first failed with two unit failures and an
inline-paragraph browser failure. They pass after the fixes.
- Initial CI failed the case lookup search test. It reproduced locally
and now passes all three cases after the API summary correction.
- Initial CI also failed process identity inspection during a server
shutdown test. All 22 focused shutdown tests pass locally without source
changes. The cause remains unconfirmed; the final CI Runner support
suite also passed without changes to process cleanup.
- The browser cases mount production components with simulated
upload/save and steer responses. They do not prove backend persistence
or live provider delivery.
- `pnpm verify` returns exit 2 because DESIGN.md is a global CI input.
The required PR workflow supplies the broad CI coverage.
- Native preflight cannot finish because Cargo is absent in this
environment. UI dependencies were installed. The missing plugin SDK
build was prepared before the successful UI checks.

- Final head: `2219de45721360d36341ea1ae46641b6bcb20360`. [CI
run](https://github.com/paperclipai/paperclip/actions/runs/42708839687)
passed. No checks are pending or failing. Greptile and Paperclip Review
each gave 5/5. All five prior review threads are resolved.

## Risks

- Narrow rows can grow to show two text lines and thumbnails. The
existing queue height limit still applies.
- Screenshot previews use the existing image-reference parser and a
local attachment path filter. Image omission is opt-in; thread and
decision media keep their existing modes.
- Pending uploads remain in draft state while their previews are hidden.
Rejection and uncertain-response tests check that the same receipts
return without losing later attachments.

## Model Used

- OpenAI Codex, based on GPT-6. The runtime does not expose a more
specific serving model ID or context limit. Used reasoning, tools, code
execution, and browser checks.

## 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
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-09 16:26:38 -06:00
DottaandClaude Opus 5.5 61c169753e test(chaos): fire planned faults at a fixed time after launch with --fault-after-seconds (#20345)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness fires each task's fault at a seeded time
inside the workload's window (R1: 20–90 s after launch)
> - With a Paperclip-sized remote workspace on Daytona, preparation
(sandbox clone and the initial snapshot save) takes longer than 90 s
> - Every restart in a Daytona R1 wave therefore lands before the run
resolves its runtime mode, so only the pre-resolution recovery path is
ever tested
> - This pull request adds `--fault-after-seconds`, which fires every
planned fault at a fixed time after launch while keeping the rest of the
seeded plan
> - The benefit is that one seed can test a restart both during
preparation and after native resolution

## Linked Issues or Issue Description

**What happened?**
Daytona R1 waves w-20261109T163514-s9913 and w-20261109T185009-s9950
restarted the controller at 43 s and 84 s. Both runs were still
preparing their remote workspace, so the native recovery path for a
real-size workspace was never exercised.

**Expected behavior**
An operator can choose when the fault fires relative to the task launch.

**Actual behavior**
The fault time could only come from the seeded workload window.

## What Changed

- `--fault-after-seconds <n>` (5–3600) sets `faultAfterMs`. `planTasks`
still draws the seeded fire time, so nonces, holds and triggers are
unchanged, but it uses `faultAfterMs` for every planned fault except
K13.
- The option is rejected with storms and crash-point waves, which have
no planned fault times.
- README: one line under Options.

## Verification

- `npx vitest run --config tests/restart-chaos/vitest.config.ts`: 259
passed. The new scheduler test fails before the change. It checks that
every planned fault fires at 150 s, that the rest of the plan matches
the same seed without the option, and that out-of-range or storm use is
rejected.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`: clean.

## Risks

- A fixed fault time can land after a fast task finishes, in which case
the fault crosses no work for that task. The trigger is recorded as
missed, as for any late fault.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code
execution in Claude Code.

## 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-09 14:13:07 -06:00
DottaandClaude Opus 5.5 97b89981d3 test(chaos): wait for the closing save before reading a remote R1 workspace's files (#20348)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness grades Daytona R1 tasks by reading their
files through `GET /issues/:id/file-resources/content`
> - The server serves a remote workspace's files either from its live
lease or, once the closing save finishes, from the saved snapshot
> - The closing save takes about a minute for a Paperclip-sized
workspace, and during it the workspace is dirty with a writer, so reads
return 422 `workspace_unavailable`
> - The harness read once at `done` and once at rest, both inside that
window, so every Daytona R1 task failed its file checks even when the
agent did the work
> - This pull request retries a 422 read for up to 4 minutes, until the
saved snapshot is readable, and returns a 404 at once
> - The benefit is that Daytona R1 grades the agent's actual files

## Linked Issues or Issue Description

Follow-up to #20288.

**What happened?**
Daytona R1 wave w-20261109T191837-s9950 (K1 at +180 s, native runs)
recovered both tasks without a Retry (R1, R3 and R6 pass), but both
failed `T:module_present`. The server log shows every content read
returning 422, from the moment the task was `done` (19:23:04) through
rest (19:23:10). Track 3's R1 waves lost 10 of 16 tasks the same way.

**Expected behavior**
The oracle reads the files once the workspace's closing save makes them
readable.

**Actual behavior**
It gave up on the first 422.

## What Changed

- `remoteWorkspaceFile` retries a 422 every 2 s for up to 4 minutes
(`REMOTE_FILE_WAIT_MS`). Any other failure, including 404 for a file the
agent never wrote, returns `null` at once.
- README: notes the wait.

## Verification

- `npx vitest run --config tests/restart-chaos/vitest.config.ts`: 260
passed. New tests: a server that answers 422 twice and then the files
(red before), and a missing file that returns within 10 s.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`: clean.

## Risks

- A remote workspace whose closing save never completes now costs up to
4 minutes of grading per file read before failing, which is visible in
the wave's duration.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code
execution in Claude Code.

## 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-09 13:54:15 -06:00
DottaandClaude Opus 5.5 908d2aae7a test(chaos): run waves against S3-compatible storage with a fault proxy, and click Retry in the real board UI (#20288)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Cloud deployments keep run logs, traces, attachments, artifacts and
saved workspace files in S3-compatible object storage
> - The restart chaos harness ran only with local disk storage, so
storage failures and slowdowns were never tested
> - It also clicked Retry through the API, so a missing or broken Retry
button in the board UI went unnoticed (staging PAP-1683: "the
saved-files recovery Retry does nothing")
> - This pull request runs waves against a loopback MinIO behind a fault
proxy, adds store-outage, latency and throttling faults, and clicks
every Retry in the real board UI
> - The benefit is that storage outages and broken Retry paths now
surface in chaos waves. The first ST1 wave found that a run starting
during a store outage fails immediately and exhausts recovery

## Linked Issues or Issue Description

**What happened?**
Staging saves to object storage and has failures there that local chaos
waves cannot reproduce, because the harness used `local_disk`. The
harness also clicked Retry through the API, which skips the board's
actual button.

**Expected behavior**
Chaos waves can run the server in cloud storage mode, inject storage
faults, and press Retry the way a user does.

**Actual behavior**
They could not.

## What Changed

- `--storage s3`:
- starts a loopback MinIO (`RELEASE.2025-04-22T22-12-26Z`, from
`~/.cache/paperclip/tools/minio/minio` or `RESTART_CHAOS_MINIO_BIN`)
with its data under the instance temp root and random root credentials;
  - creates the bucket;
  - puts a fault proxy in front;
- points the server's `PAPERCLIP_STORAGE_*` settings at the proxy, with
path-style addressing and a per-wave prefix. The supervisor declares
these as explicit workspace storage for the isolation check.
- `storage-proxy.ts`: a transparent, non-retrying S3 fault proxy that
keeps the signed Host header. Rules match by method, path and remaining
count. Faults are outage (503 `ServiceUnavailable`), latency with
jitter, a 5xx or `SlowDown` error rate, connection reset, and 403
`ExpiredToken`. It counts requests by method and status, written to
`storage-proxy.json`.
- `--storage-faults ST1,ST2,ST3` (`storage-chaos.ts`): seeded 60–180 s
windows of outage, 1–10 s latency, or 30% 503 / `SlowDown`, applied
while the wave runs. They're logged in the timeline and in
`storage-faults.json`.
- `board-ui.ts`: every task and subtask Retry the grader needs is now
clicked in the board UI. The harness uses headless Chromium through
Playwright, opens `/issues/<identifier>`, waits for the visible Retry
button and captures the `POST /issues/:id/retry` the click sends.
- R3 fails a missing button as `ui_retry_button_missing (user saw: …)`
and a click that sends nothing as `ui_retry_no_request`.
  - Crash-point waves, which serve no UI, keep the API path.
- README: a cloud storage section.

## Verification

- `npx vitest run --config tests/restart-chaos/vitest.config.ts`: 244
passed. New tests:
- the proxy against a fake upstream: passthrough, bounded outage,
`SlowDown`, `ExpiredToken`, latency and reset;
- live MinIO through the proxy: `If-None-Match` returns 412, and an
injected 503 reaches the client;
- Playwright against local pages: a working button, a missing button,
and a dead button;
- the observer clicking through the board, and R3 signatures for UI
failures;
  - window planning, apply and clear;
  - `--storage` and `--storage-faults` parsing.

  The observer and grader tests failed before the change.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`: clean.
- Live waves on master 610b54f551 with this branch:
- Baseline `--storage s3`, A1/C1, K1/K2 (w-20261109T144738-s9901): 5/8.
There were 2,319 S3 requests through the proxy and 18 conditional-write
412 fences. All 3 Retries were clicked in the board UI, returned 202
Accepted, and the new run succeeded. The 3 failures are a Claude session
issue (#20285), not storage.
- ST1 (w-20261109T145628-s9902): 1/8. A run that starts during a store
outage fails within about 1 s as `adapter_failed`, automatic recovery
exhausts within the outage, and UI Retries are accepted but do not
recover. This has been reported for a product fix.

## Risks

- `--storage s3` needs a locally built MinIO. MinIO no longer publishes
community binaries. Without the binary the wave fails at start with the
path to provide, and the live store test is skipped.
- The UI Retry needs the dev UI to load. A slow first Vite compile can
delay the click, up to the 60 s wait.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code
execution in Claude Code.

## 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-09 13:15:41 -06:00
DottaandClaude Opus 5.5 f7c85bca18 test(runner-e2e): delete each runner E2E instance when it ends and expire kept instances after 6 hours (#20246)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness boots a full temporary Paperclip instance
per wave: embedded Postgres, workspaces and caches, often 5–13 GB
> - At wave end it deleted that instance only when the wave passed with
no harness error and no `--keep`
> - Almost every chaos wave has at least one failing task, so almost
every instance stayed on disk
> - Over Nov 6–9, 359 `paperclip-runner-e2e-*` directories filled the
machine's disk (ENOSPC), which stopped a running storm wave
> - This pull request deletes the instance at the end of every wave,
smoke and baseline run unless it is explicitly kept or its processes
could not be stopped, and expires kept instances after 6 hours
> - The benefit is that chaos waves can run back to back without filling
the disk

## Linked Issues or Issue Description

**What happened?**
`main.ts` kept the temporary root whenever `keep || anyFailure ||
harnessError`. A failing task is the normal output of a chaos wave, so
instances accumulated until the disk was full. The codex K13 storm
w-20261109T113925-s9403 died after about a minute with every task cut
off.

**Steps to reproduce**
1. Run any wave in which at least one task fails.
2. The `paperclip-runner-e2e-*` directory under the OS temp dir stays
behind, with no expiry.

**Expected behavior**
The instance is removed when the wave ends. Evidence is already in
`results/`. Only an explicit `--keep`, or a stop failure that may leave
processes running, keeps it, and kept instances expire.

**Actual behavior**
Every failing wave leaks its instance permanently.

## What Changed

- New shared `tests/runner-e2e/instance-retention.ts`:
- `releaseInstance` deletes the root unless `--keep` was passed or the
processes could not be stopped. In those cases it writes a
`.paperclip-runner-e2e-kept.json` marker (owner, reason, time) and sets
the directory to 0700. An optional `retain` list keeps small evidence
files and removes everything else.
- `sweepExpiredKeptInstances` removes `paperclip-runner-e2e-*`
directories that carry the marker, are older than 6 hours, and have no
live `postmaster.pid` anywhere inside them, and that no live process
command line references. Each root is made removable first, and one
failed delete does not stop the sweep. Unmarked directories are never
touched, so existing directories from older runs stay until someone
decides on them. `recovery_state` markers never expire.
- `createRunnerE2ETemporaryRoot` runs the sweep, so every caller gets
it.
- Callers:
- A wave whose Daytona cleanup is still pending (environment DELETE
blocked, sandboxes still running after cleanup, or cleanup errors) keeps
its instance as `recovery_state`, because its database owns that
cleanup.
- The wave releases its instance before writing the ledger and summary,
so a failed report write cannot skip cleanup.
- Restart chaos `main.ts` (crash-point suite included) releases the
instance in its final cleanup, which also runs on failure and
interrupts. The crash-point task-count check moved above supervisor
creation so that error no longer leaks a fresh root.
- `ttft-baseline.ts` releases after stopping the supervisor, including
on SIGINT and SIGTERM.
- The rolling-deployment and controller-replacement smokes now delete
their roots, keeping only the evidence report, logs and screenshots. If
cleanup is unconfirmed, the root is kept with a marker.
- `launch.ts` marks its unconfirmed-remote-cleanup root as
`recovery_state`, which never expires, and its
`PAPERCLIP_RUNNER_E2E_KEEP_FAILED_PRIVATE` root as `keep`. Its existing
deletion path is unchanged, and the file gets one line shorter.
- README: the instance lifecycle and `--keep`.

## Verification

- `npx vitest run --config tests/runner-e2e/vitest.config.ts`: 328
files, 6915 passed. The new retention tests cover:
  - deleting a finished instance, and retaining only listed evidence;
  - keeping it with a marker and mode 0700;
- sweeping only marked, expired instances whose Postgres isn't running,
while sparing fresh, live, recovery-state, unmarked and unrelated
directories;
  - a stale `postmaster.pid`;
  - sweeping on `createRunnerE2ETemporaryRoot`.

The create-sweep test failed before `createRunnerE2ETemporaryRoot` ran
the sweep.
- `npx vitest run --config tests/restart-chaos/vitest.config.ts`: 221
passed.
- `node scripts/typecheck-test-projects.mjs tests/runner-e2e` and
`tests/restart-chaos`: clean.
- No wave or smoke was run, because the disk is still nearly full.

## Risks

- A failed wave's instance is no longer available for post-mortem DB
inspection unless the wave ran with `--keep`. Evidence bundles in
`results/` are unchanged.
- The sweep relies on `postmaster.pid`. An instance whose Postgres was
started outside the harness, without that file, could be removed after 6
hours.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code
execution in Claude Code.

## 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-09 09:20:09 -06:00
DottaandClaude Opus 5.5 ac0d0374f2 perf(issue-detail): measure task opens on real instances (#20276)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - People open a task from their issues list to see what an agent is
doing and to reply
> - On a busy staging instance, the composer and the latest messages
take 1.1–2.0 s to appear after a click. For one task with a slow run
log, they took 13.9 s
> - Local fixtures do not reproduce this. They do not have real network
round trips, real server contention, or real object-store latency
> - This pull request adds the measurement we need: a browser harness
that times a task open on a real instance, and a Server-Timing entry on
every API response that separates server time from network time
> - It also records the staging baseline and the improvement plan
> - The benefit is that each later performance change can be measured on
real data with the same tool, before and after deploy

## Linked Issues or Issue Description

**Problem or motivation**
Opening a task from the issues list is slow on instances with real data.
The composer and the latest chat messages can take seconds to appear. We
could not measure this on staging in a repeatable way. We also could not
tell server time from network and queueing time for most API requests.

**Proposed solution**
Add a reusable browser harness for task opens, and add a `paperclip_api`
Server-Timing entry to every API response.

**Alternatives considered**
Local Playwright fixtures only. They do not reproduce real network round
trips, server contention or object-store latency, so they did not show
the delays that people see. Per-endpoint Server-Timing only. It does not
cover the many other requests that run during a task open.

**Roadmap alignment**
This is measurement for existing task-loading behaviour. It does not add
product features. The staging baseline and the measured causes are in
`doc/plans/2026-11-09-task-loading-performance.md`. Follow-up pull
requests change the loading behaviour itself, starting with #20277.

## What Changed

- Added `server/src/middleware/api-server-timing.ts`. Every `/api`
response now has a `paperclip_api;dur=<ms>` Server-Timing entry. The
value is the server time from request arrival until headers are written,
including admission waits. The entry is appended, so the existing
`paperclip_issue` phases stay first.
- Registered the middleware first in `server/src/app.ts`.
- Added `tests/perf/issue-detail/staging-click-harness.js`. It clicks a
task row and records, frame by frame, when the composer and the latest
message become visible, layout shifts, movement of the latest message,
and the requests that run before the page is ready. The summary labels
each sample instead of using the task reference. It replaces query
values and every path segment that contains a digit. A fetch that is
still running when the sample ends is listed as pending.
- Documented the harness in `tests/perf/issue-detail/README.md`.
- Added the dated plan
`doc/plans/2026-11-09-task-loading-performance.md` with the staging
baseline, the causes, and the prioritized slices.

## Verification

- `cd server && npx vitest run src/__tests__/api-server-timing.test.ts`.
3 tests pass. They cover the JSON responses, the time spent in earlier
middleware, preserving route phases, and explicit `writeHead` calls.
- `tests/perf/issue-detail/issue-detail.perf.spec.ts` reads the first
`dur=` value. That value is still `paperclip_issue`.
- The harness was used on staging to capture the baseline in the plan
document.

## Risks

- Low risk. The header has only a duration, with no request, user or
company data.
- The middleware wraps `res.writeHead` once for each request. This is
the same pattern that `on-headers` uses.
- Server-Timing is visible to same-origin scripts only. No
`Timing-Allow-Origin` header is added.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used in Claude Code
with tool use (shell, browser automation, file editing).

## 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
- [ ] 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-09 09:06:37 -06:00
DottaandClaude Opus 5.5 610b54f551 test(chaos): close review-thread gaps in speed grading, S1 stops, readiness and stall reads (#20244)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness grades how real tasks survive
control-plane restarts, and its grades decide which product fixes come
next
> - Reviewers left 46 unresolved threads on ten merged harness PRs
(#20071, #20094, #20106, #20111, #20119, #20147, #20158, #20161, #20178,
#20194)
> - Several are real harness defects: false speed breaches from
one-sample rows, a codex-only wave that cannot start, event paging that
can loop forever, an S1 task failed for a Stop the harness never sent,
and a readiness wait that aborts on a transient lifecycle error
> - Wave S1 t05 (w-20261109T091247-s9806) hit the S1 defect: the run
finished before the stop window, so neither the Stop nor the continue
comment was sent, and the task was graded `done_too_late Infinity`
> - This pull request fixes the valid findings and adds the missing
tests
> - The benefit is that a failed task points at the product, not at the
harness

## Linked Issues or Issue Description

Refs #20071, #20094, #20111, #20147, #20158, #20161, #20178, #20194
(review threads). The security finding from #20111 was fixed separately
in #20228.

**What happened?**
Harness defects found in review and in waves:
- Aggregate p50/p95 rows breached at n = 1–3, which caused most marginal
speed failures.
- `--adapters codex` with the default workloads failed at parse because
M1 needs two adapters.
- `runEvents` paged forever if a full page had no `seq`.
- A timed-out task whose last sample was `done` was graded
`done_too_late Infinity`, and could pass when a fault had no ready time.
- S1 waited forever for a running run to stop; it also clicked Stop
while another task's fault had the server down or recovering.
- The readiness wait aborted on any `lifecycleError`, including the
transient one lifecycle `defer` sets before a 60 s retry, and hid read
errors as "not ready". Daytona used the 240 s local timeout.
- One failed parent read could hide an M1 stall for the whole 10 s check
window.

## What Changed

- Speed: aggregate rows breach only from 5 samples (`insufficient sample
(n < 5)` otherwise). Restart recoveries over the control-plane p95
budget now count in the per-run check, so a single slow recovery still
fails.
- Config: the default workloads skip M1 when fewer than two adapters are
given; an explicit `--workloads M1` with one adapter still fails.
- `runEvents` throws when a full page has no later `seq`; the run's
speed read fails instead of hanging.
- `gradeR1` fails a timed-out task as `no_rest_within_budget` before
checking its status.
- S1: the Stop waits for a ready server with no fault in progress. If
the run already finished when the window opens, the harness records a
missed Stop, skips the stop-triggered fault, still posts the continue
comment, and the oracle checks only the continue.
- Readiness: only a terminal lifecycle state (rejected, terminating,
cleaning up, terminated) aborts at once. Any other lifecycle error is
logged when it changes (`seed_agent_lifecycle_error`) and the wait
continues until ready or the timeout, which names each agent's last
lifecycle or read error. The agent API exposes neither
`lifecycleOperation.retryRequired` nor a retry-claim timestamp, so a
deferred retry cannot be told apart from a parked failure earlier.
Daytona waits up to 10 min.
- M1: a failed parent read retries on the next poll.
- Child speed collection moved into `childSpeedTasksInTurn` so its
one-at-a-time order is tested.
- Tests for each change, plus the gaps reviewers named: the stop
trigger's arming, stall-wake parsing, and `seedChaosLab`'s readiness
wiring.
- README: S1 missed Stop, R1 timeout rule, speed sample minimum.

## Verification

- `npx vitest run --config tests/restart-chaos/vitest.config.ts`: 230
passed. Each new test failed before its fix, such as the paging test
running out of memory in the loop and the stop-trigger test firing a
fault after a missed Stop.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`: clean.
- Wave evidence for the defects: S1 t05 in w-20261109T091247-s9806
(missed Stop); codex warm agent_boot p50 breach at n = 3 in
w-20261109T104527-s9901.

## Risks

- Aggregate rows with fewer than 5 samples no longer fail a wave.
Per-run p95 checks still catch individual slow runs and recoveries, but
a consistently slightly slow p50 in a small wave is now only reported.
- A lifecycle error that persists without `retryRequired` now waits for
the readiness timeout (4 min local, 10 min Daytona) instead of failing
at once. The timeout message names the error.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code
execution in Claude Code.

## 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-09 08:25:57 -06:00
DottaandClaude Opus 5.5 347d21fcbd test(runner-e2e): scope each agent's sandbox to its own run's saved state (#20253)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The runner E2E chat evals run real agents against a local Paperclip
instance on a CI host
> - PR #20044 hides the eval harness from local native agents with a
Linux Landlock domain around each runnerd launch
> - That domain is the same for every run in an attempt, so an eval
agent can still read another run's saved state
> - Recent chat-eval runs show agents that read other runs' OpenCode
session databases, another run's run log, the instance `pg_wal`,
`control-plane-state.json`, and `/proc/<pid>/cmdline` of harness
processes
> - This pull request scopes each runnerd launch to its own run and
moves the selected case id off the command line
> - The benefit is that an eval agent cannot learn about the case or
other runs from files outside its own run

## Linked Issues or Issue Description

Refs #20044

**What happened**

In local native OpenCode chat-eval cells, worker agents read files that
belong to other runs or to the harness:

- other runs' OpenCode sqlite session databases under
`runtime/paperclip-runner/durable-sessions/` (a status-cards run)
- `/proc/<pid>/cmdline` of harness processes, which shows harness paths
and the case id (`routines.pause-owned`)
- another run's run-log ndjson, and a listing of the instance
`db/pg_wal` (`routines.pause-owned`)
- a listing of `control-plane-state.json`
(`runs-adapters.list-model-options`)

The Landlock sandbox from #20044 hides only the harness (`tests/`, the
Git store, private attempt directories). All runnerd launches share one
hidden set, so the instance's per-run directories stay readable.

**Expected behavior**

An agent process sees only its own run's durable session, OpenCode
session and provider trace. Other runs' sessions, run logs and traces,
the instance database files, secrets and the shared control-plane state
in the attempt instance are denied. The case id is not on any harness
command line.

**Steps to reproduce**

1. Dispatch a local native OpenCode chat-features cell that has two
agents (for example a status-cards case).
2. In the worker's shell, run `ls
<instance>/runtime/paperclip-runner/durable-sessions/*/` and `ls
<instance>/db/pg_wal`.
3. Run `cat /proc/<launcher pid>/cmdline`.
4. Before this change, all three succeed and show other runs' files and
`--id chat-features...`.

## What Changed

- `agent-filesystem-sandbox.py`: new `--expose PATH` option. It grants a
path beneath a hidden directory. Landlock does not check traversal, so
the run uses its own entry while the parent stays unlistable. The helper
refuses a missing exposure, an exposure outside every hidden path, or
one that would reveal a hidden path. It fails the launch unless every
exposed path is readable.
- `agent-filesystem-sandbox.ts`: the launcher passes the instance root
in the sandbox configuration. `runnerdLaunchSandbox` adds per-launch
hidden paths: `durable-sessions/`, `runner/`, `control-plane/`,
`opencode/` under `runtime/paperclip-runner/`, and `data/run-logs/`,
`data/provider-traces/`, `data/backups/`, `db/`, `secrets/`. It exposes
only the launch's own durable-session directory (from `--state-dir`),
Codex runner state directory, OpenCode session directory (from
`--session-id`, named as the OpenCode driver names it) and provider
trace file.
- The wrapper creates the exposed entries, and the shared instance
directories that the product creates on first use, before runnerd
starts. It logs the run-scoped hidden and exposed paths for each launch.
- New `probeRunScopedSandbox`: at server start, a sandboxed shell runs
against a fixture instance. Another run's session, OpenCode database,
runner state, run log, trace, `pg_wal`, secrets, control-plane state and
the server's `/proc/<pid>/environ` must be denied. The run's own
session, OpenCode directory, ACPX directory and trace must be writable.
In CI any failure stops the attempt. The result goes to `server.log`.
- `selectors.ts` and `launch.ts`: the launcher reads the selected cell
from `PAPERCLIP_RUNNER_E2E_SELECTED_ID` and removes it from its
environment.
- `runner-full-stack-e2e.yml` (Run paid cell) and
`chat-catalog-testops-case.yml`: pass the cell in that variable instead
of `--id`. A target branch that does not read the variable still gets
`--id`.
- `SECURITY.md`: documents the run scope, the `/proc` limit and what
stays readable.

## Verification

- `pnpm exec vitest run --config tests/runner-e2e/vitest.config.ts
tests/runner-e2e/agent-filesystem-sandbox.test.ts
tests/runner-e2e/workflow-security.test.ts
tests/runner-e2e/manual-private-workflow.test.ts
tests/runner-e2e/private-trust-workflow-security.test.ts` passes on
macOS. The Landlock host tests (including the new fixture-instance probe
test) run only on Linux CI.
- `npx tsc --noEmit -p tests/runner-e2e` shows no errors in the changed
files.
- Paid run
[42611358428](https://github.com/paperclipai/paperclip/actions/runs/42611358428):
`chat-features.runner-opencode.local.agents.change-manager` on the same
commit applied to `chat-eval/master-candidate` (fca4bb7ce3). Result:
passed. In `server.log` the setup probe reports `failures: []` and
`run:otherDurableSession`, `run:durableSessionListing`,
`run:otherRunnerState`, `run:controlPlaneState`,
`run:otherOpencodeSession`, `run:otherRunLog`, `run:databaseWal`,
`run:secrets`, `run:otherProviderTrace` and `run:harnessEnviron` all
`denied`; `run:ownDurableSession`, `run:ownOpencodeSession`,
`run:acpxRuntime` and `run:ownProviderTrace` `allowed`;
`run:harnessCmdline` `allowed` (recorded, Landlock limit). The runnerd
launch line lists the nine run-scoped hidden instance paths and exposes
only its own durable-session and OpenCode session directories. The agent
finished the case inside that sandbox.

## Risks

- A sandboxed runnerd cannot create new direct entries in the instance
root, `runtime/`, `runtime/paperclip-runner/` or `data/`. The wrapper
creates the known ones first (`acpx`, `companies`, `projects`,
`workspaces`, `runtime-context-assets`, `skills`,
`runtime/provider-packs`). A provider that creates a different new
directory there fails in a sandboxed local cell. The paid run covers
OpenCode only. ACPX cells keep their runtime directory readable.
- A run still reads its own session's control-plane state, and other
agents' homes and task workspaces in the same instance.
- Landlock cannot hide `/proc/<pid>/cmdline`. Harness paths stay visible
there. Only the case id moves to the environment. Scheduled multi-cell
campaigns still pass `--id`.
- Harness-only change. No product code changes.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), extended thinking, tool
use (shell, file edits, GitHub CLI).

## 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-09 07:28:12 -06:00
DottaandClaude Opus 5.5 0d80b74c21 test(chaos): treat a crash-restart EPERM as a warning once the server pid is dead (#20232)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness crashes the local Paperclip server with
SIGKILL (K1, K13 storms) and grades how tasks survive
> - `crashForRestart` sends SIGKILL to every process group the server
owns
> - On macOS, `killpg` can report EPERM for a group whose members are
still exiting
> - When that happens the fault is recorded as failed, and the wave
exits 2, even though the server process itself is dead
> - This pull request tolerates EPERM in the crash path once the server
pid is confirmed dead, and logs it as a warning
> - The benefit is that a wave grades the product, not a cleanup race in
the harness

## Linked Issues or Issue Description

**What happened?**
A local Codex K13 storm at concurrency 6 (wave w-20261109T084023-s9403,
112 tasks) exited 2 because faults f04 and f09 failed with `kill EPERM`.
The server pids of both faults had died, and the tasks recovered
normally.

**Steps to reproduce**
1. Run a long K13 storm on macOS with high concurrency.
2. Occasionally the SIGKILL of the server's process groups races with
members that are exiting.

**Expected behavior**
If the server process is dead, the crash completes and the remaining
cleanup continues; the EPERM is recorded as a warning.

**Actual behavior**
The fault fails with `kill EPERM` and the wave reports an error.

## What Changed

- `crashForRestart` catches EPERM from the group SIGKILL. It then sends
SIGKILL to the server pid directly if needed and waits for it to exit
within the forced timeout.
- If the server exited, it re-observes the tree and sends SIGKILL to
each remaining group on its own, so one group's EPERM does not leave the
others to drain gracefully. It logs a warning naming the server pid and
any group that still refused SIGKILL, then continues with the normal
post-kill cleanup.
- If the server is still alive, or the error is not EPERM, the error is
rethrown as before.
- Four tests: EPERM with a dead server finishes with a warning; a
detached descendant is still killed without a graceful drain when the
first group kill reports EPERM; EPERM with a surviving server fails; a
non-EPERM error fails. The failure tests stop their owners in `finally`.

## Verification

- `npx vitest run --config tests/runner-e2e/vitest.config.ts
tests/runner-e2e/server-stop.test.ts`: the dead-server test fails before
the change (`kill EPERM`), the descendant test fails against the first
revision (the descendant drains on SIGTERM), and all 22 pass after it.
- `node scripts/typecheck-test-projects.mjs tests/runner-e2e`: clean.

## Risks

- A group that is still running and returns EPERM is no longer reported
in the crash step itself. The cleanup that follows still checks every
observed group, and it fails if any group survives.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code
execution in Claude Code.

## 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-09 05:39:40 -06:00
DottaandClaude Opus 5.5 9715b3d210 test(restart-chaos): do not grade a recovery that a back-to-back fault interrupted (#20243)
## Thinking Path

- #20183 split restart recovery so the speed budget judges Paperclip's
own share (`restart_recovery_control_plane`).
- In Phase 3's local mixed wave (`w-20261109T104527-s9901`, master
3963d38560), Claude's control-plane recovery was p50 1.4 s but p95 24.2
s (n=19). That suggested an occasional 20 s wait on Paperclip's side.
- The kept database shows otherwise. The slow samples all come from
back-to-back faults on one run (`566c07b4`):
- f07 was ready at 10:56:45.39; the run requested its runner at 45.70;
f08 (K6) killed the server at 45.78.
  - f09 started 0.69 s after f08 was ready.
- So each "sample" spans the next fault's outage plus a repeated
recovery. Outage time is removed, but the repeated claim, prepare and
launch work and the partial attempt are still charged. Paperclip has no
wait to fix; the grader mismeasures overlapping faults.

## Linked Issues or Issue Description

Refs #20183

**What happened?**
A recovery sample that a later fault interrupted before its next
provider event was graded as if it were one long recovery. That inflated
the control-plane p95 (Claude, 24.2 s) in fault storms.

**Steps to reproduce**
1. Run a chaos wave with back-to-back faults (for example the mixed wave
seed 9901: f07 K4 → f08 K6 0.4 s after ready → f09 K6 0.7 s after
ready).
2. Look at `restart_recovery_control_plane` for runs in flight across
those faults.

**Expected behavior**
Only uninterrupted recoveries are graded. Interrupted ones are counted
and reported.

## What Changed

- `tests/restart-chaos/ttft-baseline-stats.ts`:
  - `BaselineFault` gains `startedAt`.
- When a later fault starts inside a sample's window (after ready,
before the next provider event), `recoverySamples` marks the sample
`interruptedByFault` and nulls its latencies. The later fault's own
sample measures that run.
- `tests/restart-chaos/wave-speed.ts`:
  - `faultRuns` passes `startedAt`.
- `WaveSpeed` reports `recoveryInterruptedByFault`, which no longer
counts as "no provider event".
  - The speed summary prints it.
- Tests:
- A new back-to-back case: the interrupted sample is skipped and the
later fault's 1.5 s recovery is graded. It's red without the change and
green with it.
  - The `faultRuns` expectation includes `startedAt`.

## Verification

- `npx vitest run --config tests/restart-chaos/vitest.config.ts
tests/restart-chaos/wave-speed.test.ts
tests/restart-chaos/ttft-baseline-stats.test.ts`: 32 passed.
- Against the wave: the two samples behind the tail (f07 → f08, f08 →
f09) are exactly the interrupted ones. The next slowest Claude sample
(f10, 15.6 s) includes a 9.4 s `provider.session.load`, so its
control-plane share is about 6 s.

## Risks

- **Harness grading only; no product change.** Fault storms produce
fewer graded samples, and the count is reported, so a wave that is
mostly interrupted is visible.
- **A real Paperclip wait** that happened to straddle a later fault
would be hidden in that sample. It would show up in uninterrupted
samples.

## Model Used

Claude Opus 5.5 (`claude-opus-5-5`, Anthropic), 1M context window, tool
use and code execution, via Claude Code.

## 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-09 05:32:59 -06:00
DottaandClaude Opus 5.5 3963d38560 test(chaos): drop the R1 clone's remote so agents cannot push to the operator's checkout (#20228)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness runs real agents against a local Paperclip
and grades how they survive restarts
> - The R1 workload clones `--workspace-repo` (usually the operator's
own Paperclip checkout) and has agents change code in that clone
> - The clone kept `origin` pointing at the operator's checkout over
`file://`
> - An agent in an R1 workspace could therefore `git push` branches or
commits into the operator's real repository
> - This pull request removes the clone's remote right after the pinned
checkout
> - The benefit is that agent code in an R1 wave can no longer write to
the operator's checkout

## Linked Issues or Issue Description

Refs #20111 (review thread on the R1 workload)

**What happened?**
A review of #20111 noted that the cloned R1 workspace still has `origin
= file://<operator checkout>`. Project worktrees created from that clone
inherit the remote, so an agent can push into the operator's repository.

**Steps to reproduce**
1. Run a wave with `--workloads R1 --workspace-repo <checkout>`.
2. In a task worktree, run `git remote get-url origin`. It prints
`file://<checkout>`.

**Expected behavior**
The workspace has no path back to the operator's checkout.

**Actual behavior**
`git push origin HEAD:refs/heads/x` from the workspace would create a
branch in the operator's checkout.

## What Changed

- `cloneWorkspaceRepository` runs `git remote remove origin` after the
clone and any pinned-commit fetch.
- The clone test asserts that both the plain and the pinned clone have
no remote, that `git push origin` fails, and that no branch appears in
the source.

## Verification

- `npx vitest run --config tests/restart-chaos/vitest.config.ts
tests/restart-chaos/seed.test.ts`: the new assertions fail before the
change (`expected 'origin' to be ''`) and pass after it (8/8).
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`: clean.
- Audit of the three R1 waves that already ran (w-20261108T215822-s9805,
w-20261109T030542-s9805, w-20261109T081456-s9901): no reflog entry of
type push on any local ref in the operator checkout during the wave
windows, no `CHA-*` or R1 branch there, unchanged hooks, and no push in
the kept clones' logs. Nothing was pushed.
- A short local R1 wave on this branch confirms workspace provisioning
works without the remote (result in a comment below).

## Risks

- If Paperclip workspace provisioning needed `origin` in the project
repository, R1 tasks would fail to provision. The confirming R1 wave
covers this.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code
execution in Claude Code.

## 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-09 04:42:05 -06:00
DottaandClaude Opus 5.5 6fd46c0025 perf(budgets): judge Paperclip's share of restart recovery; report the provider's session load (#20183)
## Thinking Path

- Speed budgets protect the time to first visible agent activity, and
restart recovery is one of the judged metrics
(`native-speed-budgets.ts`, wave grader).
- In the B1 concurrency-6 chaos wave (`w-20261109T045320-s9807`), Codex
`restart_recovery` was p50 8.6 s and p95 11.2 s, against a budget of 6.5
s and 8.0 s. At concurrency 4 (K13) it was 3.8 s and 5.0 s.
- The preparation spans on a copy of the kept database break it down:
- Paperclip's share (ready → claim → prepare → runtime stage → runner
launch) is a steady 1.2–2.0 s.
- `provider.session.load`, the Codex app-server boot plus
`thread/resume` of long saved sessions, takes 3.0–6.3 s, 65–75% of each
recovery.
- Resumes run concurrently, and the worktree lock and the startup
reconciler are not on the path.
- Judging the full latency therefore mixes our code with provider and
session-size effects. Budget the part we control, so a regression in our
code shows up regardless of session size or concurrency, and report the
rest.

## Linked Issues or Issue Description

**What happened?**
`restart_recovery` breached its budget whenever many long provider
sessions resumed at once, although Paperclip's own recovery work stayed
within about 2 s. The breach did not point at anything Paperclip could
fix, and a real regression in our code could hide inside the
provider-load variance.

**Steps to reproduce**
1. Run a local chaos wave at concurrency 6 with long-running Codex tasks
and K1/K2/K4 faults (for example B1, seed 9807).
2. Compare `restart_recovery` in `speed.json` with
`provider.session.load` in the recovered runs' run logs.

**Expected behavior**
The budget judges Paperclip's share of a recovery. The provider's
session load and the full latency are reported but not judged.

## What Changed

- `server/src/services/native-runtime/native-speed-budgets.ts`:
- New budgeted metric `restart_recovery_control_plane`: from `ready` to
the start of `provider.session.load`, or to the next provider event when
nothing reloads (for example a reattached runner).
- Its budget is 2.5 s / 4.0 s for claude, codex and opencode. Measured:
1.2–2.0 s per run at concurrency 6.
  - The full `restart_recovery` stays in the table for reporting.
  - Breach payloads carry `restartTotalMs` and `providerLoadMs`.
- `server/src/services/native-runtime/native-run-trace.ts`:
- The per-run check records when `provider.session.load` starts, keeps
the recovery measurement, and judges it when the run finishes.
- It judges after the deferred diagnostics drain, so a provider span
committed after the provider event still counts.
  - It emits `restart_recovery_control_plane` breaches only.
- `tests/restart-chaos/ttft-baseline-stats.ts`: each recovery sample
gains `controlPlaneMs` and `providerLoadMs`. The provider-load start is
the span event's write time minus its duration.
- `tests/restart-chaos/wave-speed.ts`:
- Three restart rows per adapter: `restart_recovery` (report-only),
`restart_recovery_control_plane` (budgeted) and `restart_provider_load`
(report-only).
  - All three have outage time removed.
- `tests/restart-chaos/ttft-baseline.ts`: the recovery table adds a
"Paperclip share" column.
- Docs: `doc/run-log-events.md` (metric and payload) and the
speed-budget table in
`doc/plans/2026-11-07-durable-ownership-and-reconciliation.md`.

## Verification

- `server`: `npx vitest run
src/services/native-runtime/native-speed-budgets.test.ts`, 18 passed.
Two new tests:
  - a recovery dominated by provider load does not breach;
- a slow Paperclip share breaches with `restartTotalMs` and
`providerLoadMs`, even when the provider span is committed after the
provider event.

The existing recovery tests moved to the new metric. All six were red
before the trace change.
- `tests/restart-chaos`: `npx vitest run --config
tests/restart-chaos/vitest.config.ts wave-speed.test.ts
ttft-baseline-stats.test.ts`, 29 passed. New tests cover the sample
split and the grader rows (a judged share, a report-only provider load,
zero breaches).
- Server typecheck: no errors in the touched files.

## Risks

- **Lower sensitivity to provider-side slowdowns.** A provider
regression in session load no longer breaches. It stays visible in the
report-only rows and in breach payloads.
- **Runs with no `provider.session.load` span** (reattach, or an adapter
that doesn't emit it) are judged on the full latency against the
control-plane budget. That's conservative.
- **Run-log consumers that filter on `metric: "restart_recovery"`** see
`restart_recovery_control_plane` from now on. The doc notes the change
date.

## Model Used

Claude Opus 5.5 (`claude-opus-5-5`, Anthropic), 1M context window, tool
use and code execution, via Claude Code.

## 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-09 01:22:28 -06:00
DottaandClaude Opus 5.5 bf67e2fbd9 test(chaos): honor 429 retry delays on harness reads (#20194)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness collects each wave's speed from the board
API after the tasks rest, including the M1 subtasks' runs
> - The server rate-limits concurrent issue-list requests per client and
answers 429 with `retryAfterSeconds`
> - After a 95-task storm, the harness requested every M1 parent's
subtask list at once. The server returned 429, speed collection failed,
and the wave exited 1 with no speed grade
> - This pull request makes harness GET requests honor a 429's retry
delay with a bounded number of attempts, and collects subtask speed one
parent at a time
> - The benefit is that a busy wave keeps its speed grade, and the
harness behaves like a polite API client

## Linked Issues or Issue Description

Refs #20071, #20147

**What happened?**
Wave `w-20261109T055902-s9402` (K13 storm, A1/C1/M1, 95 tasks) finished
its tasks. Speed collection then failed with: `GET
/api/companies/…/issues?view=index&parentIds=…&limit=20 returned 429:
{"error":"Too many concurrent issue-list requests for this
actor/client","retryAfterSeconds":1}`. `main.ts` ran `childSpeedTasks()`
for every observer in parallel.

**Expected behavior**
- The harness waits and retries a rate-limited read.
- It does not flood the issue-list endpoint while collecting speed.
- A busy wave still gets graded.

## What Changed

- `api.ts`: `ChaosApi` retries a GET that returns 429.
- It waits for the delay from `Retry-After` or the body's
`retryAfterSeconds` (default 1 s, capped at 10 s), scaled by the attempt
number.
  - It makes at most 5 attempts by default.
- Writes (POST/PUT/PATCH/DELETE) are never retried, so a user action is
never repeated.
- `rateLimitDelayMs` is exported for tests.
- `main.ts`: subtask speed is collected one observer at a time instead
of with `Promise.all`.
- New `api.test.ts` against a real local HTTP server:
  - a GET that gets 429 twice, then 200, succeeds;
  - a POST that gets 429 is sent once;
  - retries stop at the bound;
  - delay parsing (header, body, cap, default).

## Verification

- `npx vitest run` in `tests/restart-chaos`: 216 passed.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`: clean.

## Risks

- A persistently rate-limited read now takes up to about 15 s before it
fails, instead of failing at once.
- Collecting subtask speed one parent at a time adds a few seconds to
the end of large M1 waves.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code
execution in Claude Code.

## 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-09 01:06:41 -06:00
DottaandClaude Opus 5.5 0ddc9f1ef1 test(chaos): count an M1 subtask stall only when its recovery needs the board (#20178)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The M1 chaos oracle checks that a parent gets exactly one
`issue_blocker_stalled` wake per stalled subtask (#20147)
> - The product wakes a parent only for a stall that needs the board.
The harness recorded a stall for any blocked subtask, whatever its
latest recovery action
> - In the Daytona M1 wave, a subtask blocked twice on agent- and
system-owned recovery that resolved by itself. The harness counted two
stalls and failed the parent for getting no wake, a false failure
> - This pull request records a stall only when the subtask's latest
recovery action needs the board, matching the product's definition
> - The benefit is that `parent_woken_once_per_stall` fails only for a
wake the product owed

## Linked Issues or Issue Description

Refs #20147, #20098

**What happened?**
In wave `w-20261109T041248-s9501`, t10's subtask 195b5fd7 was blocked
twice. Action 56d036e0 was agent-owned (`native_session_interrupted`)
and was cancelled. Action 53eb91b0 was system-owned
(`native_continuation_requires_reconciliation`) and resolved with
outcome cancelled. Neither needed the board, so the product correctly
sent no stall wake. The harness still failed
`parent_woken_once_per_stall` with 2 stalls and 0 wakes.

**Expected behavior**
A stall counts when the subtask's latest recovery action is:
- open (active or escalated) and board-owned, or escalated; or
- board-owned and resolved with outcome blocked, with the subtask still
blocked.

That is the definition the product's blocker-stall wake uses.

## What Changed

- `observeChildStalls` reads the parent's `stalledDependencies` from
`GET /api/issues/:id`. That is the product's own stalled-dependency view
(`listStalledDependencies`, built with the same `readStalls` the
blocker-stall wake uses).
- Each `relation: "child"` entry is recorded as a stall
(`stalledIssueId`, `recoveryAction.id`). The harness no longer infers
stalls from `/recovery-actions`. That endpoint lists only open actions
and selected holds, and can omit a board action resolved with outcome
blocked.
- Clicking the board Retry on a blocked subtask is unchanged: it still
happens whenever the product offers Retry, and it still counts as a
needed Retry.
- Tests:
  - a stall the product lists is recorded;
- for t10's two non-board recovery states, the product lists nothing, so
no stall is recorded, and the subtask's Retry is still clicked.

## Verification

- `npx vitest run` in `tests/restart-chaos`: 212 passed.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`: clean.

## Risks

- The harness checks `stalledDependencies` every 10 s. A stall that
appears and clears inside one interval is not recorded, so its wake goes
unchecked. It is never reported as a false failure.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code
execution in Claude Code.

## 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-08 23:36:40 -06:00
DottaandClaude Opus 5.5 61fa13c6d8 test(chaos): add S1, a board Stop across a restart then a continue comment (#20161)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness drives real user actions across
control-plane restarts: comments, Retry, approvals and budget resumes
> - The board's Stop button on a running task was not exercised. The
cancel route stamps the stop as operator-initiated so that recovery
stands down instead of re-waking the agent
> - A restart right after a Stop is where recovery could wrongly restart
work the operator stopped, or lose the operator's later "continue"
direction
> - This pull request adds S1: stop the running run, fire a fault right
after, then comment to continue
> - The benefit is a graded check that a Stop holds across a restart and
that a later continue comment finishes the task

## Linked Issues or Issue Description

Refs #20071

**What is missing?**
The campaign's scenario list includes mid-run stops. C1 covers a mid-run
comment (a steer), but no workload clicks Stop. Without one, a
regression where recovery restarts operator-stopped work, or drops the
continue comment, would go unseen.

**Expected behavior**
- After the board stops a run and a restart follows, the stopped run
stays cancelled.
- Nothing runs until the operator comments.
- The continue comment then finishes the task.

## What Changed

- S1 workload ("Stop while running, then continue"): a brief task. At
its follow-up time, while a run is running, the observer posts `POST
/api/heartbeat-runs/:id/cancel` (the board's Stop) and records `stop`.
- New `stop` trigger, for S1 only: the planned fault fires 0–2 s after
the stop.
- The continue comment is posted only after the stop has settled: at
least 20 s after the stop, with no run running or queued and no fault in
progress. It asks the agent to continue and add a `CONTINUE-<nonce>`
line.
- The oracle requires:
  - status done;
  - the brief with the chaos and continue markers;
  - `stop_took_effect`: the stopped run ended `cancelled`;
- `nothing_ran_after_stop`: no run was created between the stop and the
continue comment.
- S1 is not a default workload. There is a README section.

## Verification

- `npx vitest run` in `tests/restart-chaos`: 205 passed. New tests
cover:
- the S1 grade (pass, a run restarted after the stop, the stop not
taking effect, no stop);
  - the trigger-to-workload pairing;
- an observer test that clicks Stop once and posts the continue comment
only after the 20 s settle.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`: clean.
- A live S1 wave runs after the current chaos wave. Its result will be
posted here before merge.

## Risks

- If the product moves the stopped run to a status other than
`cancelled`, `stop_took_effect` fails. That check is intentional.
- The continue comment waits for the stop to settle, so a run stuck
`queued` delays it. R5 and the wave deadline still bound the wait.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code
execution in Claude Code.

## 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-08 22:52:48 -06:00
DottaandClaude Opus 5.5 f316562045 test(chaos): wait for new agents to be ready before the first assignment (#20158)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness grades each run's time to first activity
against the speed budgets
> - The performance session traced an 8–12 s "first claim" delay on
every wave's first cold runs. A brand-new agent spends about 8 s in
`lifecycleState: verifying` while its harness runs a real hello probe,
and queued runs start only when it becomes ready
> - The chaos waves create agents and assign the first tasks in the same
second, so those runs carry the one-time verification. Their cold
first-activity and boot looked over budget even though dispatch after
ready takes 40–60 ms
> - This pull request makes the waves wait for each agent to be ready
before the first assignment, records how long verification took, and
fails the wave if an agent never verifies
> - The benefit is that cold-start grades measure task startup, while
agent verification stays visible as its own number

## Linked Issues or Issue Description

Refs #20119

**What happened?**
In waves `w-20261109T021322-s9801` (G1) and the K4/K6 wave, every new
agent's first cold runs waited 8–12 s in `heartbeat.queue`. The
performance session reproduced it locally: the agent is in
`lifecycleState: verifying` while a hello probe runs, and its queued
runs start on `onReady`. `ttft-baseline.ts` already waits for `ready`
before launching tasks, so it never showed the delay.

**Expected behavior**
The chaos waves start their first task after the agent is ready, as the
baseline does, and they report the verification time.

## What Changed

- `seed.ts`: new `waitForAgentsReady` polls each new agent until
`lifecycleState` is `ready`, with a 4-minute cap.
- It records `seed_agent_ready` with `readyAfterMs` per agent on the
timeline.
- It throws, failing the wave, if an agent reports a `lifecycleError` or
never becomes ready.
- `seedChaosLab` runs it when `waitForReadyAgents` is set. `main.ts`
sets it for every wave. `ttft-baseline.ts` keeps its own readiness
logic, which skips unready adapters.
- The seed test's fake API answers agent reads as ready.

## Verification

- `npx vitest run` in `tests/restart-chaos`: 205 passed. New tests cover
the wait, the verification-error failure and the timeout failure.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`: clean.

## Risks

- Waves start about 8 s later, once per wave.
- A product regression that keeps agents in `verifying` now fails the
wave at seeding with a clear error, instead of showing as slow first
tasks.
- The one-time verification delay a user sees on a new agent's first
task is no longer in the cold grade. It stays in the timeline
(`seed_agent_ready`) for anyone tracking it.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code
execution in Claude Code.

## 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-08 21:32:11 -06:00
DottaandClaude Opus 5.5 0502cc9fac test(chaos): retry a blocked M1 subtask and check the parent's stall wake (#20147)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness grades the M1 scenario: a parent task
delegates two subtasks across restarts
> - When a subtask's run failed recovery, the harness only clicked Retry
on the parent. The parent's Retry is correctly held behind the blocked
subtask (#20109), so the task waited until its deadline
> - A real board user would also click the subtask's Retry. #20098 also
added a wake for a parent whose subtask stalls, and the grade did not
check it
> - This pull request clicks the board Retry on a blocked subtask
(graded as a needed Retry) and checks that the parent got exactly one
stall wake per stall
> - The benefit is that M1 grades the full user path and proves the
stall wake from the product's own activity record

## Linked Issues or Issue Description

Refs #20098, #20109, #20094

**What happened?**
In wave `w-20261109T003126-s9701`, the M1 parent t05 rested blocked
behind a blocked codex subtask. The harness clicked Retry on the parent.
The product held that Retry ("waiting on a blocked task") as designed.
Nothing retried the subtask, and the task waited 38 minutes until the
wave deadline. The wave also could not show whether the parent got the
new `issue_blocker_stalled` wake.

**Expected behavior**
The harness acts like the board: it clicks Retry on a blocked subtask
that offers one, and it counts that as a needed Retry. The M1 oracle
checks the parent's activity for one `issue.blocker_stalled_wake` per
stalled subtask.

## What Changed

- `observer.ts`:
- For M1, the observer checks the parent's subtasks every 10 s while the
parent is open, independent of the parent's rest window.
- Each blocked subtask is recorded as a stall: the subtask plus its
latest recovery action, from `GET /api/issues/:id/recovery-actions`.
- When a blocked subtask's interruption offers Retry, the harness posts
the board Retry. It clicks once per subtask; a click that was not
accepted can be tried again on a later check.
- The parent's `issue.blocker_stalled_wake` activity entries are
collected as `stallWakes` through `GET /api/issues/:id/activity`.
- `grader.ts`: R3 fails with `retry_needed_on_subtask_<n> (<ids>)` when
a subtask needed a board Retry, unless the parent's own Retry already
failed R3.
- `workloads.ts`: the new M1 check `parent_woken_once_per_stall` is
added when a subtask stalled. Each (subtask, recovery action) stall must
have exactly one matching wake. A stall with an unknown action matches
any wake for that subtask.

## Verification

- `npx vitest run` in `tests/restart-chaos`: 203 passed. New tests
cover:
- an observer test: a blocked subtask gets one Retry click, and the
parent is still watched;
  - the R3 subtask-retry grade;
- the stall-wake check: woken once, not woken, woken twice, two stalls
of one subtask with only one woken, and no stall.
- G1, which also has subtasks, is unaffected: the existing governance
tests pass.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`: clean.

## Risks

- The harness now acts on subtasks, which it did not before. It clicks
once per subtask and only when the product offers Retry.
- The stall-wake check reads activity through the board API. If the API
omits these entries for a board user, the check fails visibly instead of
passing.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code
execution in Claude Code.

## 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-08 20:12:03 -06:00
DottaandClaude Opus 5.5 218eb4ec8b test(runner-e2e): hide the eval harness from local native agents (#20044)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Runner E2E evals run real agents against a real Paperclip server,
then grade the result
> - A local cell runs the product from the same checkout as the eval, as
the same OS user
> - In run 42472276601 an OpenCode agent read the eval's own scenario
YAML and grader from that checkout
> - An agent that can read the oracle can steer toward it instead of the
user's request, so the eval stops measuring the product
> - This pull request hides the eval harness from every local native
agent process and removes eval names from fixtures
> - The benefit is that a passing chat eval again shows product
behavior, not oracle access

## Linked Issues or Issue Description

Refs #19614 (the chat catalog eval harness that these graders belong
to).

**What happened**
In run 42472276601, cell
`chat-features.runner-opencode.local.access.cli-authorization`
(OpenCode, DeepSeek), the agent printed its environment and saw the
checkout path in `PATH` and `PAPERCLIP_RUNTIME_NOTIFICATION_HELPER`.
OpenCode's `read` and `grep` tools were denied by its
`external_directory` policy. The agent then used its `bash` tool with
absolute paths, which that policy does not gate. It listed
`/home/runner/_work/paperclip/paperclip`, read `docs/cli/*.md` and
`cli/src/commands/client/*.ts`, ran `grep -rn` over `tests/runner-e2e`,
and found the query in `chat-full-catalog-testops-manifest.json`. It
then read `tests/runner-e2e/chat-scenarios/access.yaml`, the manifest
entry, and the case grader
`chat-catalog-domains/identity-cli-authorization.ts`. Its reasoning
said: "This is the exact test case!" Separately, fixture company names
contained the case id, for example `Runner E2E
chat-features.runner-opencode.local.access.private-resource …`, and an
agent noticed that it was in a test.

**Expected behavior**
The agent under test cannot read the eval's scenarios, fixtures, graders
or captured evidence. It keeps everything that it needs: its workspace,
its home, the product skills, the product packages and temporary files.
The names it can see do not contain case ids, "E2E" or "test".

**Steps to reproduce**
1. Dispatch `runner-full-stack-e2e.yml` for any
`chat-features.runner-opencode.local.*` cell on master.
2. In the agent run, use bash to execute `grep -rn expected_outcome
/home/runner/_work/paperclip/paperclip/tests/runner-e2e/chat-scenarios`.
3. The command returns the scenario expectations. With this change, the
command returns `Permission denied` and exit code 2.

## What Changed

- `tests/runner-e2e/agent-filesystem-sandbox.py` (new): applies a Linux
Landlock domain that grants filesystem access everywhere except the
hidden paths, sets `no_new_privs`, checks in-process that the hidden
paths are denied, then `execv`s the command with the same PID, argv and
environment.
- `tests/runner-e2e/agent-filesystem-sandbox.ts` (new): selects the
hidden paths (`tests/`, `.manual-private-trusted/`, the private evidence
directory, `RUNNER_TEMP`). It never hides the agent's temporary root,
the checkout root or the shared temporary directory. It runs a setup
probe, logs the result and fails closed in CI. Then it wraps each
`paperclip-runnerd` spawn of the E2E server.
- Follow-up: run 42445668869 showed that the hidden list missed two
places. A fixture worker listed Playwright's compiled copy of the
harness and graders with source maps
(`/tmp/playwright-transform-cache-1001/`). An agent in another run
listed the attempt root's `artifacts-private/`, `control/` and
`selected-private-inputs/` under `/tmp`. A sandboxed attempt now creates
its root in `/var/tmp`. The reason: Landlock hides a path by leaving its
ancestors ungranted, and agents must still create files in `/tmp`. The
launcher sets `PWTEST_CACHE_DIR` to a hidden child of that root and
removes the default cache. The hidden list covers the four private
children of the root, including children that are created after runnerd
starts. The probe also checks that the default Playwright cache is not
listable.
- `tests/runner-e2e/launch.ts`: passes the selection to the server
process. The selection applies only to Linux local native cells. It does
not apply to Codex cells (Codex's bubblewrap sandbox cannot mount under
Landlock), installed-release launches, legacy adapters or Daytona.
- `tests/runner-e2e/server-entry.ts`: installs the sandbox before the
product starts. The product server itself stays unrestricted because it
loads this entry from the harness.
- `tests/runner-e2e/fixture-names.ts` (new), `catalog.ts`,
`live-fixtures.ts`: fixture company, agent, environment, secret, AI
connection and project names and descriptions use a neutral word and an
8-character digest suffix. They do not contain case ids or "Runner E2E".
No grader or harness lookup reads these names.
- `tests/runner-e2e/agent-filesystem-sandbox.test.ts` (new): tests the
selection, the launch rewrite and the server-entry order. On Linux with
Landlock, it also tests that a sandboxed `grep` over a fake `tests/` dir
is denied while the product file stays readable.
- `tests/runner-e2e/SECURITY.md`: documents the boundary and its limits.
- The trusted files `manual-private-handoff.ts`,
`manual-private-retention.ts` and `manual-private-report-assets.ts` are
byte-identical to master.

## Verification

- `pnpm exec vitest run --config tests/runner-e2e/vitest.config.ts
tests/runner-e2e/agent-filesystem-sandbox.test.ts
tests/runner-e2e/live-fixtures.test.ts tests/runner-e2e/catalog.test.ts
tests/runner-e2e/codex-ci-sandbox.test.ts
tests/runner-e2e/server-entry.test.ts`: 151 passed, 1 skipped. The
Landlock test is skipped on macOS. `npx tsc --noEmit -p
tests/runner-e2e` shows no errors in the changed files.
- Paid run
[42480314919](https://github.com/paperclipai/paperclip/actions/runs/42480314919)
for `chat-features.runner-opencode.local.agents.change-manager` ran on
branch `chat-eval/eval-isolation-verify`, which is this commit
cherry-picked onto the chat catalog candidate. The cell passed. Its
`server.log` records:
- `probe …
"harnessFile":"denied","harnessDirectory":"denied","productFile":"readable","temporaryWrite":"allowed"`,
and `denied` for each of `…/paperclip/tests`,
`…/.manual-private-trusted`,
`…/_temp/manual-private-evidence-*/capture`,
`…/_temp/manual-private-evidence-*` and `…/_temp`. `"failures":[]`.
  - `runnerd launched inside the sandbox (pid 3548)`.
- The fixture company was `Company cc369a2e`. No captured snapshot
contains "Runner E2E".
- The probe is a regression check on every local native cell: a missing
or ineffective sandbox stops the attempt in CI before an agent starts.

## Risks

- Landlock does not hide that a path exists. `stat` of
`tests/runner-e2e` still succeeds (the probe logs `harnessDirectoryStat:
visible`). Reading and listing are denied.
- Directories above a hidden path (for example `/`, `/home/runner` and
the checkout root) cannot be listed. New top-level entries in those
directories are not accessible after launch. Agent state, `HOME` and
temporary files are in `/tmp`, which stays fully granted.
- Not covered: Codex cells, legacy adapters, installed-release launches
and macOS. An agent that can reach the Docker daemon can still mount the
checkout into a container. `ps` still shows the launcher's command line,
which includes the case id. The temporary root
(`paperclip-runner-e2e-*`) and instance id (`runner-e2e-*`) in the
agent's paths still say "e2e". The chat catalog candidate depends on
those prefixes, so this PR does not rename them.
- Python 3 must be present on Linux runners. Ubuntu runners have it, and
the harness already uses it.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), agentic coding with
tool use (shell, file edits, GitHub CLI).

## 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-08 20:10:21 -06:00
DottaandClaude Opus 5.5 89265f7312 test(chaos): add R1, a code change in a fresh clone of a real repository (#20111)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness proves that real agent tasks survive
control-plane restarts
> - Its coding workloads run in a tiny chaoscli fixture. Real work
happens in large repositories, where workspace setup, copy and restore
cost more and fail differently
> - The campaign asks for real coding tasks in a fresh Paperclip clone,
run under restarts
> - This pull request adds the smallest support for that: a
`--workspace-repo` option and an R1 workload
> - The benefit is restart coverage for code changes in a real
repository, graded by running the agent's own test and a hidden case

## Linked Issues or Issue Description

Refs #20071, #20089

**What is missing?**
The harness can only seed the generated chaoscli fixture as the project
workspace. There is no way to run restart chaos against a code change in
a real repository.

**Expected behavior**
An operator can name a git repository. The harness clones it fresh as
the workspace, and a workload asks for a real code change in it with an
oracle that runs the result.

## What Changed

- `--workspace-repo <git repository>`: `seedChaosLab` clones it
(shallow, from committed HEAD, `--no-hardlinks`) as the project
workspace instead of the chaoscli fixture. Local only.
- R1 workload: the agent adds
`packages/shared/src/chaos/slug-<nonce>.ts` with `chaosSlug` and a
`node:test` file, and runs it. The nonce names keep concurrent tasks
from colliding in one repository.
- The R1 oracle runs the agent's test with `node --test` and a hidden
case against the module through Node's type stripping.
- Collect commands can take an `argv` function of the nonce.
- Config rules: R1 and `--workspace-repo` go together. B1 and B5 (which
need the chaoscli fixture) do not combine with it. R1 is not a default
workload.
- README section "Code change in a real repository".

## Verification

- `npx vitest run` in `tests/restart-chaos`: 195 passed, including new
tests for the config pairing, R1 grading with the nonce argv, and a
shallow clone of a fixture repository.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`: clean.
- I checked the oracle commands by hand on Node 26: `node --test` on a
`.ts` test and `node --input-type=module -e 'import … from "./…ts"'`
both work.
- A live R1 wave against a Paperclip clone runs after the current chaos
wave frees the harness slot. Its result will be posted here before
merge.

## Risks

- A shallow clone of a large repository adds seconds to wave seeding.
That is one clone per wave.
- The oracle uses Node's built-in type stripping. A runtime without it
fails `node_test` and `hidden_case`, which shows as an oracle failure,
not a false pass.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code
execution in Claude Code.

## 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-08 16:55:02 -06:00
DottaandClaude Opus 5.5 42a5d71cb7 test(chaos): separate workspace provisioning from agent boot; grade outage-created runs as restart (#20119)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness grades speed against the native speed
budgets, and a breach fails the wave
> - In the real-repository R1 wave, the claude cold boot p95 was 31 s
against 20.5 s, and cold first activity p95 was 37 s against 30 s
> - The performance session traced both. The boot breach was the
repository's own provisioning script on the first worktree creation
(21–24 s), not Paperclip startup. The first-activity breach was a run
created during an outage, behind the next fault
> - This pull request reports provisioning on its own row, keeps it out
of agent boot, and grades runs created during an outage as restart
> - The benefit is that a speed breach points at Paperclip startup, not
at project setup scripts or injected downtime

## Linked Issues or Issue Description

Refs #20106, #20111

**What happened?**
Wave `w-20261108T215822-s9805` (R1 in a Paperclip clone) reported a
claude cold boot p95 of 31.0 s. The slow run spent 22.8 s in
`heartbeat.prepare.workspace_provision`, the repository's
`scripts/provision-worktree.sh`. Reuse takes 0.3 s. Its cold
first-activity p95 came from run 630088a4, created 4 s after a fault
killed its predecessor. That run queued until the next fault cleared,
and #20106 still graded it cold because no fault started inside its own
window.

**Expected behavior**
Project provisioning is reported, not charged to agent boot. A run
created while a fault's outage is in progress is graded as restart.

## What Changed

- `faultBeforeFirstActivity` takes outage windows and returns true when
any window, from fault start to ready, overlaps the run's span from
creation to its first activity. It used to take fault start instants
only.
- A run with `retryOfRunId` (the successor of an interrupted run) is
graded as restart.
- `collectWaveSpeed` reads each run's workspace operations. Provisioning
counts only when a `workspace_provision` operation has
`metadata.created: true`, meaning first-time creation of the workspace;
reuse (about 0.5 s) is left in boot.
- `bootWithoutProvisionMs` subtracts that first-time
`heartbeat.prepare.workspace_provision` span from agent boot for the
wave grade. Per-run boot breaches use it too.
- New report-only row `workspace_provision` per adapter and start, shown
only when the run created its workspace. It has no budget.
- `runMetrics` and the TTFT baseline tool are unchanged. `BaselineRun`
gains an optional `workspaceCreated`.

## Verification

- `npx vitest run` in `tests/restart-chaos`: 195 passed. New cases cover
a run created during an outage, a retry successor, provisioning counted
only for `created: true` through `collectWaveSpeed`, the provisioning
row and boot without provisioning.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`: clean.

## Risks

- A slow provisioning step no longer fails the boot budget. It stays
visible on its own row, with no budget, because it runs the project's
own script.
- A run created during any outage, and any retry successor (including a
user Retry), is graded as restart even when it started quickly. Restart
recovery still measures it.
- One extra API read per run (workspace operations) during speed
collection. A failed read counts as not created, so boot is never
under-charged.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code
execution in Claude Code.

## 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-08 16:54:58 -06:00
DottaandClaude Opus 5.5 83a229ae8c test(chaos): grade a fault-hit run as a restart, not a cold start (#20106)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness grades speed against the native speed
budgets, and a regression fails the wave
> - It grades a run's time to first activity as a cold or warm start,
and restart recovery separately
> - Fault receipts carry no run ids, so a run that a fault hit before
its first activity was graded as a cold start
> - In a K4/K6 wave, the two slowest codex cold samples were runs that a
K6 SIGKILL hit before their first activity, so the cold p95 breach was
not a cold start
> - This pull request grades such a run as a restart
> - The benefit is that cold and warm rows measure only starts with no
fault, and restart cost stays in restart recovery

## Linked Issues or Issue Description

Refs #20071, #20094

**What happened?**
Wave `w-20261108T200620-s9701` reported a codex cold
time-to-first-activity p95 of 29.9 s against a 17 s budget. The two
slowest samples were runs that a K6 restart hit before their first
activity.

**Expected behavior**
A run whose first-activity window contains a fault start is graded as a
restart, not as a cold or warm start.

**Actual behavior**
The run was graded as a cold start, and the restart time counted as
cold-start latency.

## What Changed

- `faultBeforeFirstActivity(run, faultStarts)`: true when a fault
started between the run's trace start and the end of its raw
first-activity span (or first-token span when no activity span exists).
- `collectWaveSpeed` sets such a run's phase to `restart` before it
removes outage time. Cold, warm and single-run grading already skip
restart runs. The run still counts in restart recovery through the
fault's run timing.
- A test covers the helper and the exclusion from the cold row.

## Verification

- `npx vitest run wave-speed` in `tests/restart-chaos`: 10 passed.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`: clean.

## Risks

- A run with no first-activity or first-token span counts as hit when
any fault starts after its trace start. Such a run has no cold sample to
grade anyway.
- A fault that starts just before a run's trace start does not count,
and outage removal handles it as before.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code
execution in Claude Code.

## 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-08 15:30:45 -06:00
DottaandClaude Opus 5.5 88835e624a test(chaos): fail M1 fast on a blocked subtask and grade interrupted waves (#20094)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness proves that real agent tasks survive
control-plane restarts
> - In a K4/K6 wave with the M1 multi-agent scenario, one subtask
failed. Its parent waited on it, and the harness kept watching for over
50 minutes
> - When the operator stopped the wave, the harness wrote no grade
files, so the 17 finished tasks had no grade
> - Separately, master's harness test for time to first activity failed
after #20077 changed how time is measured from run creation
> - This pull request makes M1 fail fast on a blocked subtask, grades
the finished tasks of an interrupted wave, and aligns the speed grade
with #20077
> - The benefit is shorter waves with a grade for every task, and a
green harness suite on master

## Linked Issues or Issue Description

Refs #20071, #20077, #20088

**What happened?**
Wave `w-20261108T181313-s9701` (local, codex and claude, A1/C1/M1, K4
and K6) ran 71 minutes against a planned 30. Task t05 (M1) delegated
CHA-8 and CHA-9. The CHA-8 run failed after a K4 restart (the defect
that #20088 fixes), and CHA-8 went to `blocked`. The observer kept
waiting for CHA-8 to finish. After SIGINT, the harness threw before
grading, so no task had a grade file. Also, `wave-speed.test.ts` fails
on master: #20077 changed `runMetrics` to measure from the start of
`heartbeat.queue`.

**Steps to reproduce**
1. Run an M1 task whose subtask run fails and leaves the subtask
`blocked`.
2. See the observer wait until the parent deadline.
3. Send SIGINT to the harness and see that no grade files are written.

**Expected behavior**
A blocked subtask ends the wait, and the parent is graded as a failure
with a `task_child_blocked` timeline entry. An interrupted wave grades
every finished task and fails the wave.

## What Changed

- `observer.ts`: `childrenPending` treats a `blocked` subtask as
settled, so the parent rests and is graded. The timeline records
`task_child_blocked` with the subtask ids.
- `observer.ts`: `markUnfinished` closes an open task at interrupt as
unfinished (`timedOut`, last seen status) and records
`task_unfinished_at_interrupt`.
- `main.ts`: an interrupt now sets the harness error and grades the
wave, instead of throwing before the grade. The wave still fails.
- `wave-speed.ts`: `firstActivityMs` uses
`runMetrics().firstActivityAfterRunCreatedMs` from #20077, so there is
one definition of time from run creation.
- `wave-speed.test.ts`: the fixture starts `heartbeat.queue` when the
comment wait ends, as in real runs.

## Verification

- `npx vitest run` in `tests/restart-chaos`: 172 passed. Before this
change, the wave-speed test failed on master.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`: clean.

## Risks

- A subtask that is `blocked` only for a short time and then resumes
would end the parent's watch after the 60-second grace. The grade then
shows a failure that a longer wait could avoid. M1 subtasks have no
blockers, so `blocked` means a stuck subtask.
- An interrupted wave grades tasks while its server is still up. If the
server is already down, artifact collection fails, and those tasks are
graded as failures.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code
execution in Claude Code.

## 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-08 14:20:57 -06:00
DottaandClaude Opus 5.5 c3c226d165 test(restart-chaos): governance (approval, budget hard stop) and upgrade mid-run scenarios (#20089)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness kills and restarts the control plane
during real agent tasks and grades every task
> - It covers run lifecycle, comments, delegation and long tools, but
not two areas that must also survive a restart: governance (approvals
and budget hard stops) and an upgrade in the middle of a run
> - An approval that disappears, runs twice or runs after a rejection,
or a budget pause that dispatches work, breaks governance. An upgrade
that cannot resume saved state breaks the upgrade-safe-saved-state rule
> - This pull request adds the harness support for both scenarios, using
only the paths a real board user and agent use
> - The benefit is that the next chaos waves can test these boundaries
and report failures with evidence

## Linked Issues or Issue Description

Refs #20084, #20067

**What happened?**
The restart chaos harness had no scenario for an approval-gated action
pending across a restart, for a budget hard stop crossed mid-run with a
restart, or for a server upgrade in the middle of a run. These
boundaries were untested.

**Expected behavior**
- **G1:** a pending approval survives K1, K2 or K4. Approving it after
the restart runs the gated action exactly once; rejecting it never runs
it.
- **H1:** a budget hard stop crossed mid-run pauses the company, nothing
dispatches while paused (also across a restart), and resuming continues
the work.
- **K14:** a wave started on an older build and upgraded mid-turn
resumes old-saved state on the new build with no Retry, no lost comments
and no stuck records.

**Steps to reproduce**
1. Run the harness commands in `tests/restart-chaos/README.md`
("Governance across restarts" and "Upgrade mid-run").
2. Each task is graded R1–R6 plus its oracle.

**Paperclip version or commit**
master at 4f7eee10c2.

## What Changed

- **G1 workload: approval across a restart.**
- The agent requests board approval through its `call_api` tool (`POST
/api/companies/{companyId}/approvals`, `request_board_approval`, linked
to its task), blocks, and ends its turn.
- A new `approval` trigger fires the fault once the observer sees the
approval pending.
- After the restart, with the server ready, the observer acts as the
board through `POST /api/approvals/:id/approve` or `/reject` (rejection
also cancels the task). The decision is fixed by the nonce.
- The oracle checks one request, still pending when decided after the
restart, the decision, and the gated subtask exactly once on approval
and never after rejection.
- R1 and R3 grade a rejected task against `cancelled`
(`expectedFinalStatus`).
- **H1 workload: budget hard stop across a restart.**
- Before launch the observer sets the company budget to observed spend
plus one cent through `PATCH /api/companies/:id/budgets`.
- A new `paused` trigger fires the fault once the company shows `paused`
/ `budget`.
- After the restart the pause must hold 30 s with the server ready. The
observer records runs that started while paused, then resolves the
incident with `raise_budget_and_resume`.
- The oracle checks the brief, the pause, no dispatch while paused, the
resume after the fault, and an active company at rest.
- H1 runs alone, at concurrency 1, and not with `--rc06-masked` (config
errors).
- **K14 fault: upgrade mid-run, with `--upgrade-from <older
checkout>`.**
  - The server starts from the older checkout.
- The first K14 stops it with SIGTERM and starts this checkout on the
same database, instance directory, port and secrets. The runner binary,
pinned Codex and source dependencies are prepared per checkout.
  - The receipt names both commits.
- K14 is local-only and needs `--upgrade-from` (and the reverse). It is
excluded from storm defaults. Preflight refuses an older checkout that
is not prepared.
- G1 and H1 are opt-in: they are not in the default workloads.
- README sections for both scenarios.

## Verification

- `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts`:
every new and changed test passes; 188 of 189 pass in total. The one
failure, `wave-speed.test.ts` "judges time to first agent activity…",
also fails on master: #20077 gave the local TTFT a budget after #20071's
test expected none. This PR does not touch `wave-speed` or the speed
budgets. The new `governance.test.ts` covers:
- the board decides only after the fault the pending approval armed,
with the server ready, for approve and reject;
  - a blocked task waiting on its approval is not counted as resting;
- G1 and H1 oracle pass and fail cases (duplicate request, action twice
or after rejection, decision or resume that crossed no restart, dispatch
while paused);
- H1 budget set, pause hold and incident resolve, against a fake board
API;
- trigger gating, opt-in workloads, H1 exclusivity, and K14 option
pairing.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`:
passed.
- Not run yet: a live wave of G1, H1 or K14. Phase 3 holds the harness
slot, and the smoke runs are requested there. Their results will be
added here.

## Risks

- Test harness only; no product code changes.
- G1 relies on the agent following the instructions (request, block, act
only when approved). A task where the agent does not follow them fails
the oracle visibly.
- K14 seeds the company with this checkout's harness code while the
older server serves. This works for recent builds; a much older build
could reject a seed call.
- Live behavior is not proven until the smoke runs.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), via Claude Code with tool use
(shell, file edits, test execution, a read-only research 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-08 13:40:39 -06:00
DottaandClaude Opus 5.5 fcdceaf5fb test(restart-chaos): grade a stopped retain-on-failure restore source as retained (#20086)
## Thinking Path

- The restart-chaos harness's Daytona sweep grades R6, sandbox leaks,
after every wave. It counts a sandbox as kept on purpose when its lease
says so: a retained lease, or a released reusable lease whose sandbox is
stopped for reuse.
- Paperclip also keeps a failed run's sandbox stopped as a
workspace-restore source (`retain_on_failure`, owned by restore
recovery). The sweep graded those as orphans, so the warm-reuse chaos
gate for #20048 counted intentional, recoverable retention as leaks.
- The coordinator's decision: grade it honestly. A stopped, owned,
recoverable restore source is retained; a running one, or one that
survives its environment's deletion (no lease left), is still a leak.

## Linked Issues or Issue Description

Refs #20048

**What happened?**
In Daytona warm wave w-20261108T170423-s9501, four "orphans" were
`retain_on_failure` leases: released, sandbox stopped, kept as failed
runs' workspace copies. The sweep counted them as R6 leaks, failing the
gate for intended retention.

**Steps to reproduce**
1. Have a released `retain_on_failure` lease whose Daytona sandbox is
stopped.
2. Run `reconcileSandboxes`: the sandbox is listed in `orphanSandboxes`.

**Expected behavior**
It is listed in `retainedLeases`. A running restore source, or a sandbox
with no lease, stays in `orphanSandboxes`.

## What Changed

- `tests/restart-chaos/daytona-sweep.ts`: `keptForReuse` also accepts a
released `retain_on_failure` lease whose sandbox is `stopped`; the doc
comment explains both kinds of kept sandbox.
- `tests/restart-chaos/daytona-sweep.test.ts`: a stopped restore source
is retained; a running one, or a sandbox with no lease, is still an
orphan.

## Verification

- `npx vitest run --root tests/restart-chaos daytona-sweep`: 14/14 pass.

## Risks

- **Hiding a leak.** Only stopped sandboxes behind a released restore
lease qualify; they are owned by restore recovery and, with #20085,
destroyed with their environment. Running sandboxes and lease-less
sandboxes still fail R6. This is harness-only, with no product change.

## Model Used

Claude Opus 5.5 (`claude-opus-5-5`, Anthropic), 1M context window, tool
use and code execution, via Claude Code.

## 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-08 12:32:45 -06:00