Commit Graph
650 Commits
Author SHA1 Message Date
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
DottaandClaude Opus 5.5 b32ef9f111 perf(budgets): judge time to first visible agent activity; keep time to first text as a reported metric (#20077)
## Thinking Path

- The native speed budgets (plan invariant 8) judge how fast an agent
responds. They judged `task.time_to_first_token`: time to the first
deliverable **text**.
- In the clean local restart storm (w-20261108T153612-s9401), Claude
breached its TTFT budget (cold p50/p95 17.4/34.7 s against 16.5/30). The
per-run spans showed this was not a control-plane wait: in all 49 cold
runs (and 76% of warm), Claude started a tool call before writing any
text. Its first visible activity came at 7.4 s p50 from run creation;
first text came at 19.6 s. Codex never calls a tool first.
- So first text charges model tool work as latency for tool-first
adapters. What the user experiences as "the agent responded" is the
first visible activity: a text delta, a tool-call start, or a thinking
indicator.
- The coordinator decided: budgets judge time to first visible agent
activity from run creation, with the budget numbers unchanged; first
text stays a reported metric with no budget for tool-first adapters.

## Linked Issues or Issue Description

**What happened?**
Local storm s9401: the Claude TTFT budget breached on every aggregate,
while the control-plane steps (queue 0.05 s, prepare 0.3 s, runner
startup ~4 s, first provider event 1.3–1.8 s) were well within budget.
The rest of the first-text wait was Claude streaming tool input (about
5.5 s per call) and running tools before it wrote text.

**Steps to reproduce**
1. Run a Claude task whose first action is a tool call (most real
tasks).
2. Read the run log: `provider.time_to_first_agent_event` ends at the
tool call; `task.time_to_first_token` ends many seconds later at the
first text.
3. The speed budget judges the later one and reports a breach.

**Expected behavior**
The budget judges the first visible agent activity from run creation
(`task.time_to_first_agent_activity`). Time to first text is still
recorded and reported.

## What Changed

- `server/src/services/native-runtime/native-session-executor.ts`: when
the first meaningful agent event commits (the existing
`firstMeaningfulAgentEventKind`: text delta, tool-call start, or
reasoning), record `task.time_to_first_agent_activity`. It uses the same
detached anchor resolution as TTFT (accepted comment, answered question,
durable wake request, or run creation), so it adds nothing to the turn
path.
- `server/src/services/native-runtime/native-speed-budgets.ts`: the
metric is now named `time_to_first_activity` (was `ttft`), with
identical budget numbers. The type comment documents the definition and
why first text is unbudgeted.
- `server/src/services/native-runtime/native-run-trace.ts`: the budget
check judges `task.time_to_first_agent_activity` from run creation,
keeping the existing input-wait handling. `task.time_to_first_token` is
no longer budgeted.
- `tests/restart-chaos/ttft-baseline-stats.ts` and `ttft-baseline.ts`:
report first activity from run creation (the budgeted value) next to
TTFT.
- Docs: `doc/observability.md` (new span) and
`doc/plans/2026-11-07-ttft-baseline.md` (metric table).
- Tests:
- A tool-first turn records first activity at the tool call (5 s) while
TTFT records first text (16 s).
- A tool-first trace with slow first text but fast first activity is
within budget.
  - The existing budget tests now judge the activity span.

Observability path (AGENTS.md section 5.7): the new run-log span uses
only the existing `eventKind` and `anchor` attributes. Its name survives
run-log redaction intact; checked with `redactEventPayload`. The
operator OTLP gate is unchanged. It is not Telemetry.

## Verification

- `cd server && npx vitest run
src/services/native-runtime/native-speed-budgets.test.ts
src/services/native-runtime/native-run-trace.test.ts`: 44/44 pass.
- `cd server && npx vitest run
src/services/native-runtime/native-session-executor.test.ts -t "first
visible activity|first assistant text once|first-token timing"`: 7/7
pass.
- `npx vitest run --root tests/restart-chaos`: 159/159 pass; `tsc` on
`tests/restart-chaos` is clean.

## Risks

- **A metric rename.** Consumers of `run.performance.budget_exceeded`
see `metric: "time_to_first_activity"` instead of `"ttft"`. In-repo
there are none besides the tests; Phase 3's chaos speed grade switches
to the new span.
- **A looser budget for slow first text.** Intentional: first text is
reported, not judged. A regression in control-plane latency still shows
up in first activity.

## 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:42 -06:00
DottaandClaude Opus 5.5 a65626a2a2 test(chaos): fail waves on a needed Retry and on speed regressions (outage removed); add M1 multi-agent workload (#20071)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness (tests/restart-chaos) kills and restarts
the control plane during real agent tasks, and grades every task on
reliability rules R1–R6 plus a task oracle
> - Dotta's bar for today's campaign: restarts must continue on their
own (Retry is only a fallback), the system must stay fast (speed budgets
in `native-speed-budgets.ts`), and realistic scenarios must be covered
> - The harness graded a task that needed a user Retry as a pass, graded
speed only in crash-point mode, and counted a K4 controller hold as slow
TTFT. It also had no multi-agent scenario
> - This pull request closes those four gaps
> - The benefit is that every chaos wave now fails on a needed Retry and
on a real speed regression, but not on the fault's own outage

## Linked Issues or Issue Description

Refs #19831 (durable ownership plan) and the restart chaos plan
(`doc/plans/2026-11-06-control-plane-restart-chaos.md`). Used by today's
campaign waves:
- w-20261108T134237-s9401 re-graded to 104/107 under the Retry rule;
- w-20261108T153612-s9401: 100/101, with a Speed section showing 6
breaches after outage removal.

## What Changed

- **R3 Retry rule** (`grader.ts`): a task that needed a Retry fails R3
as `retry_needed (<failed runs' error codes>)`, and a second click fails
as `retry_needed_<n>_clicks`. The harness still clicks Retry so the task
can finish and be graded.
- **Per-wave speed grade** (new `wave-speed.ts`, wired in `main.ts`):
- It reads every task run's run log through the API and reports TTFT
(from run created) and agent boot p50/p95 per adapter and start kind,
restart recovery (ready → the run's next provider event), and single-run
`run.performance.budget_exceeded` events, against the local budgets.
  - A breach fails the wave.
- Output goes to `speed.json` and a Speed section in `SUMMARY.md`.
Remote environments report numbers with no budget.
- **Outage removal**: each performance span is shortened by its overlap
with fault outage windows (fault start to replacement server ready)
before it is judged. The summary reports how many spans and how much
time were removed.
- **M1 workload** (multi-agent delegation):
- The parent task creates two subtasks, part 1 delegated to the other
Chaos Lab agent.
- Each subtask saves a marked document, and the parent writes a summary
of both before it completes.
- The observer collects the subtasks and their documents, and the oracle
checks the parent links, assignees, statuses and markers.

## Verification

- `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts`:
14 files, 163 tests pass. New tests cover the M1 grade, the R3 Retry
rule (one click and two clicks), the wave speed grade (budget judgement,
restart recovery, remote no-budget, fault→run attribution) and outage
removal.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos` passes.
- Real waves:
  - The R3 rule re-graded w-20261108T134237-s9401 to 104/107.
- w-20261108T153612-s9401 produced the Speed section: 923 spans
adjusted, 2,452 s of outage removed, 6 breaches.
- M1 has not yet run in a live wave. It's next in the campaign rotation.

## Risks

- Waves that passed before can now fail: on a needed Retry, or on a
speed breach. That is the intended bar.
- Speed is judged at wave concurrency on a shared host, which is noisier
than the idle baseline the budgets came from. Breaches are reported per
row, so a reviewer can see the margin.

## 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 11:27:39 -06:00
DottaandClaude Opus 5.5 f679cf81dc fix(runner): refuse a finish or block decided before earlier results reached the agent (#20069)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Restarts must be invisible to a task: an agent that was working
keeps working after the control plane comes back
> - Restart chaos t03 (Daytona warm wave, K4 holds) ended a task as
blocked even though its document was saved
> - The cause is in how a terminal decision (paperclip_block or
paperclip_finish) is handled after a long controller outage, not in how
the Runner journals tool calls
> - This pull request refuses a terminal call that the agent made before
it had seen results that already returned, so the agent continues with
those results
> - The benefit is that a controller outage no longer turns into a false
"blocked" task

## Linked Issues or Issue Description

Restart chaos t03, wave `w-20261108T142906-s9501` (Daytona, codex, A1,
three back-to-back K4 holds of 136 s, 252 s and 152 s). Refs #19831
(durable ownership plan).

**What happened**

The kept instance's run log shows the order of events:
1. The turn is accepted at 14:29:57. The runner then delivers nothing to
the controller until 14:39:27. The holds keep the controller down, and
the Runner keeps Codex's `item/tool/call` requests until it is
reattached.
2. During that outage Codex keeps running. Its `write_document` call
stays pending. The model is sampled again about once a minute and says
"The save request is also waiting on Paperclip". It then calls
`paperclip_block`, with the reason `tool_unavailable`.
3. After the reattach the Runner delivers the backlog in order.
`write_document` runs and returns (the document is created: sourceSeq
152). Then the stale block arrives (sourceSeq 247) before Codex has
acknowledged the write result: there is no `tool.execution.completed`
for it before the block. The task ends blocked.

**Codex behavior, checked directly** against the pinned Codex 0.160
app-server. A local probe sent one dynamic tool call and left it
unanswered:
- Codex does not time out. It re-samples the model at about 60 s and 127
s ("The save call is still waiting to return").
- A reply sent 130 s later is accepted (`durationMs` 130003), and the
model finishes ("DONE").

So the reply is not lost. The agent's terminal decision was made without
it.

**Expected behavior**
- A terminal call decided while earlier Paperclip results were still
unseen is refused with a retryable error.
- The agent continues with those results. In t03 it would see that the
document was saved, and finish.

## What Changed

- `packages/paperclip-runner/src/live/terminal-call-freshness.ts`:
`unseenReturnedToolCalls` reads the turn's committed run events up to
the terminal call. It returns each dynamic call that has a returned
`tool_result` but no provider `tool.execution.completed`.
`staleTerminalCallResult` builds the retryable
`terminal_call_before_results` error, which names those calls.
- `runnerd-codex-transport.ts` `#handleSemanticToolInput`: when such
calls exist, `paperclip_finish` and `paperclip_block` get that error
instead of being dispatched. The recovery path after a restart uses the
same handler. A long-running call with no result yet does not count.
- Crash point `runner.semantic_tool.after_input_before_dispatch`, just
before the controller dispatches a dynamic tool call. A crash-point hold
(`--hold-ms`, `--crash-point-hold-ms`; a K4-style receipt) keeps the
controller down after the self-kill. Together they reproduce t03 on
Daytona.

## Verification

- Red/green: `cd packages/paperclip-runner && npx vitest run
src/live/runnerd-codex-transport.test.ts -t "refuses a terminal call
decided"`.
- The case `seen: false` failed on master: the handler was called and
the stale block was dispatched. It passes with the fix.
- The case `seen: true` (the agent saw the result first) dispatches
normally, both before and after.
- `npx vitest run src/live/runnerd-codex-transport.test.ts`: 289 passed.
`src/live/terminal-call-freshness.test.ts`: 5 passed.
- `npx tsc --noEmit -p packages/paperclip-runner` passes, and `node
scripts/typecheck-test-projects.mjs tests/restart-chaos` passes.
- Shared registry: 15 passed. Call-site test: 3 passed. Chaos runner
tests: 12 passed.
- Codex probe (local, about 2.5 min per run): reproduced the "still
waiting" re-sampling and the accepted late reply described above.
- Not yet run: the Daytona crash-point repro with a hold. It needs the
shared harness slot, and I'll add the result here. The fix is controller
TypeScript, which is not an input of the Daytona image, so the current
image still matches.

## Risks

- A refused terminal call costs the agent one more turn step. It applies
only when a result returned that the agent had not seen, which in
practice happens only after a controller outage.
- The check reads committed run events. If the store evicted the earlier
call's events, it finds nothing and keeps today's behavior.
- Upgrade-safe run state: no persisted shape changes. The check reads
events the Runner already commits. A run saved by the previous release
resumes with the same history and the same answer.
- ACPX and other providers emit no dynamic `tool.execution.*` events, so
the check is a no-op for them.

## Model Used

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

## 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 10:43:21 -06:00
DottaandClaude Opus 5.5 5de2842620 fix(restart-chaos): bound each skopeo image copy attempt (#20056)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart-chaos harness downloads the signed Daytona runner image
with skopeo before each Daytona wave
> - #20021 made a failed copy retry from an empty directory, but the
retry only runs when skopeo exits
> - A hung blob read never exits, and a Daytona pull stalled for 80
minutes
> - This pull request bounds each copy attempt, so a stall is killed and
retried
> - The benefit is that one network stall costs at most one bounded
attempt, not the whole run

## Linked Issues or Issue Description

Follow-up to #20021. No duplicate PR was found.

**What happened?**

A Daytona chaos run's image pull stalled inside `skopeo copy` for 80
minutes, even after #20021. `copyImageToDir` awaited `execFile` with no
timeout, so a read that hung forever blocked the run forever.

**Expected behavior**

Each copy attempt has a time bound. A stalled attempt is killed and the
next attempt starts from an empty directory.

**Steps to reproduce**

1. Run a Daytona wave or crash point with an uncached image on a network
where a registry blob read hangs.
2. Before this PR, `skopeo copy` never returns and the run waits
indefinitely.

**Paperclip version or commit**

`master` before this PR.

## What Changed

- `copyImageToDir` passes `timeout` (default 20 minutes,
`RESTART_CHAOS_SKOPEO_ATTEMPT_TIMEOUT_MS`) and `killSignal: "SIGKILL"`
to each `skopeo copy`. A timed-out attempt counts as a failed attempt
and retries from an empty directory.
- README updated.

## Verification

- New test: a fake skopeo that writes a partial blob and then hangs is
killed after the 500 ms test bound, and the second attempt succeeds,
within 10 s.
- `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts
tests/restart-chaos/daytona-image.test.ts`: 18/18. `tsc -p
tests/restart-chaos/tsconfig.json` is clean.

## Risks

- Test harness only. A very slow but progressing pull longer than 20
minutes would now be retried; the bound is configurable.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), via Claude Code with
tool use.

## 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 09:07:36 -06:00
DottaandClaude Opus 5.5 96badeaa42 fix(lifecycle): never repeat a passed harness verification; settle a dead verifier's lease at once (#20030)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A new or reconfigured agent runs a harness verification (a hello
probe in a test sandbox) before it is ready, and its runs wait until it
passes
> - In the final Daytona restart chaos wave, a Codex agent's runs waited
about 417 s for their first sandbox, while boot took only about 32 s
> - Each control-plane restart killed the verifier. The next process
waited out the 120 s lifecycle lease and then booted a fresh sandbox.
Nothing recorded a configuration that had already passed.
> - This pull request records a passed verification by what makes it
valid and reuses it, and settles a dead verifier's lease on the shared
ownership path at once
> - The benefit is that a restart never repeats a passed verification,
and never leaves runs waiting out a dead process's lease

## Linked Issues or Issue Description

**What happened?**
In the final Daytona chaos wave, the Codex agent was created at 09:55:46
UTC and its three runs were queued. Verification ran four times, in four
sandboxes, because faults K1, K4 and K2 each killed the verifying
server. After K2 the server was ready at 10:00:20, but verification
restarted only at 10:02:13: it waited out the 120 s lifecycle lease. The
first run sandboxes were created at 10:02:43, 417 s after launch.

**Steps to reproduce**
1. Create a Codex agent and assign it a task at once, so the task waits
for harness verification.
2. Kill the control plane (SIGKILL) while the agent is verifying.
3. The restarted server waits out the dead verifier's 120 s lifecycle
lease, then verifies again in a new test target. The task's first token
arrives about 130 s after the fault.

**Expected behavior**
- A passed verification of the same effective configuration in the same
runner image is never repeated.
- A verification is never restarted from scratch while its holder
process is alive. When the holder is proven gone, its lease is settled
at once.
- Runs gate only on a configuration that has never passed.

**Root cause**
`verify()` (`server/src/services/agent-harness-verification.ts`) leased
a fresh sandbox on every attempt and recorded nothing on a pass. The
lifecycle lease (`modules/agent-lifecycle/adapters/postgres.ts`) named
no holder process, so a restart could only wait for it to expire.

## What Changed

- Restart chaos TTFT driver: a `verify` phase, reported in its own rows,
and a `--verify-fault K1|K2|K6` option. The fault fires 1 s after
`verifying` is observed, so it interrupts a held lease. The seed keeps
each agent's creation body, so the driver can hire an identical agent.

- **Persisted pass:** new columns `agents.harness_verified_digest` and
`harness_verified_at` (migration 0448). The digest is an HMAC, keyed
with the instance's decision-signing secret, over:
- the fully resolved effective configuration: every value the test used,
including `cwd`, resolved credentials, and agent-over-environment
precedence;
- the selected AI account and its credential generation (connection and
grant ids and `updatedAt`, never the credential);
  - the environment and the runner image.

The key means the stored value cannot confirm a guessed credential
offline. For a sandbox, the runner image is an immutable image digest or
a qualified Daytona snapshot ID (`sandboxBinding`, now exported). For
local runs it is the server version. A mutable image or snapshot name,
or a local environment redirected to the managed sandbox, never reuses a
pass.
- **Server-only:** `normalizeAgentBaseRow` and
`redactAgentRowForResponse` strip the digest and its timestamp from
agent API responses.
- `verify()` still runs every account, policy and secret check, then
returns complete on a matching digest without booting a test target. It
records the digest on a pass before the lifecycle step completes, so a
restart in between reuses it.
- **Lease holder:** the lifecycle `claim()` stamps the claiming
controller process on the lease (`lifecycleOperation.leaseHolder`,
supplied through the lifecycle effects port) and clears the stamp with
the lease. The compare-and-swap release lives in the lifecycle module
(`releaseAgentLifecycleLeaseOfHolderInTransaction`).
- **Shared ownership path:** a new `agent_lifecycle_lease` kind with
`settleAgentLifecycleLease`.
- When the reconciler proves the holder gone, settle clears exactly that
lease (compare-and-swap on the holder), notifies lifecycle work, and
logs `agent.lifecycle_lease_reconciled`.
- A live holder is never taken over. The lifecycle state, operation and
progress are untouched.

## Verification

- `restart-verification.test.ts` (embedded Postgres, real lifecycle
store and worker, real reconciler pass):
- A verification held by a dead process is claimed and completed at once
after the startup pass, with the agent ready in under 10 s instead of
waiting out the 120 s lease. It fails without the new kind.
  - A live holder is never taken over.
  - The claim stamps the holder, and `defer` clears it.
- `agent-harness-verification-preflight.test.ts`:
- A second `verify()` of the same configuration allocates no target. A
revoked account still fails before reuse. A changed model, `cwd` or
literal credential tests again. It fails without the reuse.
  - A failed verification records nothing.
- Ownership report, all lifecycle module tests, remote, provider-API and
unit harness verification tests, and crash-point call sites: 13 files,
220 tests pass.
- Targeted local run: the TTFT driver's new `verify` phase (`--phases
verify --verify-fault K1`, Codex, 2 samples) hires a new agent, assigns
it a task at once, kills the control plane (K1) while it verifies, and
times fault to first agent token:
- Before (master server code): 129.8 s and 129.8 s. The restarted server
waited out the dead verifier's 120 s lease. Run-log TTFT from run
creation was only 4.1 s.
- After (this branch): 15.8 s and 17.2 s. The startup reconciler pass
settled `agent_lifecycle_lease` and verification re-ran at once.

## Risks

- **A stale pass:** the digest covers every resolved value, the selected
account generation, the environment and an immutable runner image, so
any change tests again. Rotating the instance signing secret invalidates
every digest, so each agent verifies once more.
- **Migration:** two nullable columns, no backfill. Existing agents
verify once more, then reuse.
- **Settle:** it runs only on a `gone` proof, compare-and-swap on the
exact holder. It never changes the lifecycle state.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with
tool use (shell, file editing, test 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 Opus 5.5 <noreply@anthropic.com>
2026-11-08 08:10:40 -06:00
DottaandClaude Opus 5.5 bfa9002a3c test(restart-chaos): reach the stop-and-retain crash point with --daytona-reuse-lease (#20049)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart-chaos crash-point suite kills the server at named
points, then checks that recovery restores every invariant
> - On Daytona, `environment.stop_retain.after_claim_before_stop` was
never reached, because no workload produced a stop-and-retain release
> - A per-turn run on a reusable sandbox lease ends every turn with
`stop_and_reuse`, which releases through exactly that stop-only claim
> - This pull request adds `--daytona-reuse-lease`, so the chaos Daytona
environment keeps reusable sandboxes and the point is reachable
> - The benefit is live coverage of the stop-and-retain crash window

## Linked Issues or Issue Description

This is a follow-up from the Phase 2 durable-ownership data
(`doc/plans/2026-11-07-phase2-reconciler-data.md`, "Not covered").
Related: #19937 (crash-point suite) and #20042 (stop-only claim fence
renewal). No duplicate PR was found.

**What happened?**

The Daytona crash-point run `cp-20261108T104102` reported
`environment.stop_retain.after_claim_before_stop` as `not_reached`. The
point needs a stop-and-retain intent, and the chaos Daytona environment
is created with ephemeral leases.

**Expected behavior**

A flag lets a Daytona wave or crash-point run use reusable leases. A
per-turn run then stops and keeps its sandbox, and the point is reached
and graded.

**Steps to reproduce**

1. Run `node scripts/crash-points.mjs --env daytona --daytona-image
<image> --points environment.stop_retain.after_claim_before_stop
--adapters codex --workloads A1`.
2. Before this PR the point is `not_reached`.
3. With `--daytona-reuse-lease --lifecycle per_turn` it is reached.

**Paperclip version or commit**

`master` at `8475826d2d` (the signed image `c24059f1…` revision).

## What Changed

- `--daytona-reuse-lease` (wave and crash-point suite, Daytona only)
creates the chaos environment with `reuseLease: true`. The crash-point
suite passes the flag through.
- The R6 sandbox sweep counts a **stopped** sandbox behind a **released
`reuse_by_environment`** lease as kept for reuse, not a leak. A later
run on the same workspace resumes it, because reuse accepts released and
retained leases. A running sandbox, or a stopped one behind an ephemeral
lease, is still a leak.
- The crash-point registry prerequisite and the README name the flag.

## Verification

- Live, Daytona, codex A1, from the image revision `8475826d2d`:
- `cp-20261108T132149`: the point was **reached** and settled with no
invariant violations. R6 flagged the stopped reusable sandbox as a leak,
which led to the sweep fix.
- `cp-20261108T133351`, with the sweep fix: **pass**. 0 orphaned
sandboxes, 0 held leases, the environment deleted, and nothing left
running.
- `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts`
passes `daytona-sweep`, `scheduler` and `crash-points` (52/52). `tsc -p
tests/restart-chaos/tsconfig.json` is clean.

## Risks

- Test harness only; the registry change is prerequisite text. Without
the flag, waves behave as before.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), via Claude Code with
tool use.

## 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 08:07:19 -06:00
DottaandClaude Opus 5.5 c966334ec3 fix(recovery): settle a run-less lease once its stamped holder is proven gone (#20026)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The control plane recovers in-progress records after a server
restart through a shared ownership path
> - Run-less sandbox leases (login, probe, harness verification) were
released only by a five-minute idle sweep
> - That sweep releases by age: it leaves a dead holder's sandbox for
minutes and takes a live login session's sandbox
> - This pull request settles a run-less lease only when its stamped
holder is proven gone
> - The benefit is no stuck sandbox after a restart, and no live session
loses its sandbox

## Linked Issues or Issue Description

Related: #19965 (named the owner of login/probe leases), #19964 (the
run-lease settle this mirrors). No duplicate PR was found.

**What happened?**

The final Daytona chaos wave `w-20261108T083036-s9401` recorded I2
`environment_login_probe_lease` (`pid_dead`) on 2 records. These were
the codex agent's harness-verification leases. A restart killed their
holder. The leases stayed `active` until the five-minute idle sweep
released them. The same sweep releases any run-less lease after five
idle minutes, also when its holder is alive.

**Expected behavior**

A run-less lease is released as soon as its stamped holder is proven
gone. A lease whose holder is alive is never released, however long it
is idle.

**Steps to reproduce**

1. Start a Daytona wave: `node cli/node_modules/tsx/dist/cli.mjs
tests/restart-chaos/main.ts --env daytona --daytona-image <image>`
(K1/K4 faults).
2. Let a fault kill the server while an agent's harness verification
holds its sandbox lease.
3. See the reconciler report I2 `environment_login_probe_lease` until
the idle sweep releases it.

**Paperclip version or commit**

`master` at `8475826d2d`.

## What Changed

- New `settleEnvironmentLoginProbeLease`, registered on
`environment_login_probe_lease`. It runs only for a stamped holder that
is proven gone, and compares and swaps on that exact stamp.
- It moves the lease to `pending_cleanup` with the recovered-orphan
reason, so the cleanup sweep destroys the sandbox in its next tick.
- It leaves the lease alone when a legacy shutdown fence applies, or
when another live lease holds the same provider resource.
- The idle sweep for run-less leases now covers only legacy rows saved
before holders were stamped (no `leaseController` key). Stamped rows are
never released by age.
- `holderUnchanged` is now exported from the run-lease settle so both
settles share it.
- The controller stamp (`leaseController`, and every other
`ControllerProcess` stamp) now records the platform deployment when the
platform names one (Railway: project, environment, service, deployment,
volume). `controllerStamp` passes it to `proveOwnerGone`. A container
redeploy changes the PID space, and the old stamp would read
`different_process_space` (unprovable) forever. Now the existing
`deployment_replaced` proof shows the old holder gone. This also covers
`environment_run_lease_active` and the other kinds built on
`controllerStamp`.
- `currentDeploymentIdentity` in `owner-proof.ts` now delegates to
`currentControllerDeployment`, so there is one parser.
- Data doc: added the final Daytona wave, the six Daytona-only crash
points (4 pass, 2 not reached, no violations), and the cause of the
roughly 450 s codex TTFT. Runs waited behind the agent's harness
verification, which restarted after each fault; boot itself took about
32 s.
- Restart-chaos README: corrected which Daytona crash points a plain
task reaches.

## Verification

- Red, before the change:
`environment-login-probe-lease-ownership.test.ts` failed 3 of 6. No
settle happened, and the idle sweep released 3 live holders' leases.
- Green: `pnpm exec vitest run` passes for
`environment-login-probe-lease-ownership`,
`environment-run-lease-ownership` and
`heartbeat-orphaned-active-lease-sweep` (45/45), and for
`environment-lease-owner-stamps`,
`heartbeat-observed-workspace-preservation`,
`ownership-reconciliation-report`, `heartbeat/recovery` and
`workspace-recovery-warm-handoff`.
- Redeploy: a stamp from a replaced Railway deployment settles with
`deployment_replaced`. A stamp from the same deployment in another PID
space stays `active`. This was red when `controllerStamp` dropped the
deployment.
- Live-owner counter-cases: the current process, another live PID,
another PID space, and a lease re-stamped after the proof all stay
`active`. A read-only pass never settles.
- Not re-run live: the Daytona wave and crash points ran before this
fix. The next Daytona wave should show no
`environment_login_probe_lease` I2.

## Risks

- A stamped run-less lease whose live holder never releases it, because
of a bug in that process, is no longer released after five idle minutes.
It is released once that process exits. This is the intended trade:
never take a live holder's sandbox.
- On an ephemeral server without a platform deployment identity (not
Railway), a stamp from a replaced container is still unprovable. Such a
run-less lease stays `active` until the provider expires the sandbox,
and the reconciler reports it as unprovable. Before this PR, the
five-minute sweep released it by age. This is the same open gap as
unstamped legacy leases on an ephemeral server: there is no operator
action yet. It is listed as a Phase 2 follow-up.
- Legacy unstamped run-less leases keep the age fallback. These are rows
written before #19965, and no current code writes them.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), via Claude Code with
tool use.

## 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 06:43:45 -06:00
DottaandClaude Opus 5.5 022c73e475 fix(restart-chaos): retry a failed image copy from an empty directory; share the image cache (#20021)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart-chaos harness tests recovery on Daytona sandboxes
> - The harness extracts the runner from the Daytona image with skopeo
before a wave starts
> - A skopeo blob read that timed out failed the whole run before any
test started
> - This pull request makes the image extraction retry cleanly and share
one cache
> - The benefit is that one network hiccup no longer costs a 20+ minute
run

## Linked Issues or Issue Description

Related: #19544 (Daytona chaos waves without Docker). No duplicate PR
was found.

**What happened?**

The Daytona wave `w-20261108T082021` and the crash-point run
`cp-20261108T101304` stopped before any test ran. skopeo reported `read:
operation timed out` while it copied a blob. The retry then reported
`not a containers image directory, don't want to overwrite`, because the
destination already had partial files. The crash-point suite also used
its own image cache, so it downloaded the image again on each run.

**Expected behavior**

A failed copy retries into an empty directory. Every wave and
crash-point run reuses an image that another run already extracted.

**Steps to reproduce**

1. Run `node scripts/crash-points.mjs --env daytona --daytona-image
<image>` on a slow or unstable network.
2. Let skopeo time out on one blob read.
3. See the harness stop with exit 2 before any point runs.

**Paperclip version or commit**

`master` at `8475826d2d`.

## What Changed

- With a shared cache, two runs can extract the same image at once. The
final cache directory is created only by an atomic rename, and no run
deletes a cache entry. A run that loses the rename adopts the valid
entry. An invalid leftover stays in place, and the run publishes its
extraction to a unique sibling directory.

- `copyImageToDir` runs up to 3 `skopeo copy` attempts. Each attempt
starts from an empty destination directory.
- `defaultDaytonaImageCacheRoot()` is now the one cache for both the
wave harness and the CLI: `tests/restart-chaos/results/daytona-images`
by default, or `RESTART_CHAOS_IMAGE_CACHE` if set. Crash-point runs
reuse an image that a wave already extracted.
- README updated.
- Three more tests: two runs against one cache through a barrier, a
corrupt leftover, and two runs that both meet a corrupt leftover.
- Unit tests use a fake skopeo that behaves like the real one: it
refuses a non-empty destination and leaves partial blobs behind when it
fails.

## Verification

- `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts
tests/restart-chaos/daytona-image.test.ts`: 17/17 passed.
- `tsc -p tests/restart-chaos/tsconfig.json`: no errors in
`tests/restart-chaos`.
- Live run: the six Daytona crash points were rerun with this patch,
reusing the cached image.

## Risks

- Low. Test harness only; no product code changes.
- A shared cache means all runs depend on one receipt per image digest.
Receipts are already written atomically by renaming the staging dir, and
each one is keyed by the immutable digest.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), via Claude Code with
tool use.

## 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 06:00:48 -06:00
DottaandClaude Opus 5.5 8475826d2d fix(wake-queue): deliver user comments whose wake a restart lost or a stale continuation dropped (#19992)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A user's follow-up comment on a task must reach the agent exactly
once, through restarts (invariant 4)
> - The final unmasked local chaos storm (96 tasks, 31 restarts) passed
94 tasks. The 2 failures were both lost user comments
> - In t69, a restart killed the server between saving the comment and
writing its wake. In t72, a comment's own run was cancelled as stale,
because the earlier run completed the task first without reading the
comment
> - This pull request adds a reconciler rule that gives a lost comment
its wake, and saves the comment before a stale cancellation
> - The benefit is that a user's message is never silently dropped when
a restart or a completion race hits at the wrong moment

## Linked Issues or Issue Description

Refs #19956 (work-intent outbox), #19944, #19831 (plan, invariant 4).

**What happened?**
In chaos wave `w-20261108T055444-s9401` (local, unmasked, codex and
claude, 30-min K13 storm, master `46bf913fb9`), 94 of 96 tasks passed.
Two failed:
- **t69, R1 `rested_not_done`.** The user's comment was saved at
06:14:59.358 and reopened the task to `todo`. A K1 SIGKILL fired 28 ms
later, before the wake was written. No wake row exists. Recovery skips a
`todo` task whose last run succeeded, so the task sat with no run until
the wave ended.
- **t72, R4 `concurrent_runs`.** A comment posted during a run queued
its own run. That run was claimed at 06:16:04.874. The first run's
completion marked the task `done` 2 ms later. The comment's run was then
cancelled with `continuation_task_ownership_changed` before dispatch,
and the comment was lost.

**Expected behavior**
Each accepted user comment is delivered once, unless the task is
cancelled or reassigned, or a person closes it.

**Steps to reproduce**
`node cli/node_modules/tsx/dist/cli.mjs tests/restart-chaos/main.ts
--replay w-20261108T055444-s9401/t69` (or `.../t72`).

## What Changed

- `recovery/ownership-invariant-report.ts`: a new reconciler-owned I4
rule, `user_comment_without_wake`. It finds a user comment that meets
all of these conditions:
  - it is newer than every run of the task;
- the task is assigned, is not a conversation, has no live run or lock,
and has at least one earlier run;
  - no wake request carries the comment.

A comment younger than 30 s is not reported yet, because it may still
get its normal wake. Its end-of-grace time is returned as `nextDueAt`.
- `native-runtime/native-finalization-reconciler.ts`: the backstop step
is now exported as `applyOwnershipReportBackstops`. For each such
comment it saves the missing outbox receipt (`executionWait: wake_lost`)
and promotes it.
- `work-intent-outbox.ts`: new `saveUndeliveredUserComments`, which the
reconciler rule and the stale-run path both use. It saves only live user
comments that no completed run received. It writes nothing for a task
that is reassigned, is a conversation, or has a status the caller does
not allow.
- `heartbeat/queue.ts`: the saved-receipt pass that already sends
pause-saved messages also sends `wake_lost` receipts. It uses the same
per-comment consumption check and the same chat-input exclusion.
- `heartbeat/stale-comment-continuation.ts` and `heartbeat.ts`: before a
stale continuation is cancelled, its undelivered user comments are saved
as a deferred receipt. The release drain then reopens the `done` task
and delivers them once. This applies only when an agent run that started
before the comment completed the task after it. A task that a person
marks done is left closed.
- `tests/restart-chaos/grader.ts`: R4 ignores runs cancelled before
dispatch (`continuation_task_ownership_changed`) when it looks for
overlapping execution. Those runs never executed. Comment consumption
still counts them.

## Verification

Each focused file ran alone in one process (`--maxWorkers=1`):
- `heartbeat-held-comment-wake.test.ts` (22 pass). This includes two new
cases:
- t69 shape: the reconciler rule fires, the backstop delivers the
comment once, and a second pass does nothing;
- t72 shape: the stale run is cancelled, the task reopens, and the
comment is delivered once.

  Both fail with the rule or the save disabled, and pass with them.
- `ownership-reconciliation-report.test.ts` (23 pass). It includes the
rule's boundaries: a carried comment, a too-young comment (deadline
only) and an already-read comment.
- `tests/restart-chaos/grader.test.ts` (17 pass). The new R4 case fails
with the old grader. A cancelled run that is not a pre-dispatch
cancellation still fails R4.
- Unchanged and passing: comment wake batching (27), refused assignment
wake (3), wake-queue postgres adapter (103), stale queue invalidation
(68), deferred receipt work (45). The first version also reopened a task
that a person had marked done, which broke one deferred-receipt test.
That case is now excluded.
- The server package typecheck and the restart-chaos harness typecheck
pass.
- Same-session chaos context on master: K6 crash loop
`w-20261108T063326-s9402`: 6 of 6 pass. The K4 long-hold wave is still
running. After merge, I'll replay only the targeted fault shapes (t69
and t72).

## Risks

- The reconciler rule runs on each report pass. It is bounded by the
1-hour lookback and the per-kind row cap, and it excludes conversation
tasks, so chat input keeps its own authorization.
- A comment younger than 30 s waits up to the end of its grace period
before the backstop acts.
- The stale-run save reopens a `done` task only for a comment that the
completing run could not have read.

## 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 02:14:45 -06:00
DottaandClaude Opus 5.5 68d5fb5a6e feat(chaos): check I6 and I8 per crash point; ready the Daytona points (#19984)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The on-demand crash-point suite (#19946) kills the control plane at
named durable write boundaries and checks the plan's invariants
> - Its first full run measured I1–I4 and I7, but I5, I6 and I8 were
reported as not measured
> - #19960 (generation stamping) and #19863 (speed budgets) now record
what I6 and I8 need in the run log
> - The six Daytona-only points have not run yet, and three of them
cannot be reached by a plain task
> - This pull request checks I6 and I8 per point, names the I5 gap, and
readies the Daytona points for the coming Daytona wave
> - The benefit is that every invariant is either asserted or a named
gap, and a Daytona run explains any point it cannot reach

## Linked Issues or Issue Description

Refs #19831 (plan, Phase 5). Follows #19946 (the suite). Uses #19960
(event generations) and #19863 (speed budgets).

**Problem**
- The suite reported I5, I6 and I8 as "not measured".
- A Daytona run would report three points as `not_reached` with no
reason.
- A Daytona run without its key or image failed only after a 3-minute
boot.

**Expected behavior**
- I6 and I8 are checked at each point.
- I5 is a named gap with its reason.
- Daytona points say what they need, and a Daytona run checks its inputs
before it boots.

## What Changed

- `tests/restart-chaos/invariants.ts`: `runLogViolations` checks two
invariants from the run log, scoped to the wave's company:
- I6: a run that failed with an error that names a stale or
earlier-generation event;
- I8: each `run.performance.budget_exceeded` event, with the budget
evidence (metric, measured, p95, adapter, start kind).
Both are keyed by run or event id, so the per-point baseline diff
charges each one to the point that produced it. I5 is reported as
`notMeasured` with a reason (`I5_GAP`).
- `packages/shared/src/crash-point-registry.ts`: an optional
`prerequisite` on the three Daytona points a plain task cannot reach.
These are the teardown claim (needs a failed release), the reusable
destroy (needs a `reuse_by_environment` lease), and stop-and-retain.
- `tests/restart-chaos/crash-points.ts`:
- `--list` prints each point's `needs:`, and a `not_reached` result
names it too;
- `--env daytona` refuses to start without `DAYTONA_API_KEY` and an
image.
- Docs: the README and the plan describe the I5/I6/I8 coverage and the
Daytona points.

## Verification

- `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts
tests/restart-chaos/crash-points.test.ts`: 12 passed.
- `cd packages/shared && npx vitest run src/crash-points.test.ts`: 15
passed.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos` and
`npx tsc --noEmit -p packages/shared` both pass.
- I ran `runLogViolations` against a throwaway Postgres 18 with seeded
rows, then stopped and deleted it. It returned:
  - I8 for the budget breach, with its evidence;
- I6 for the run of this company that failed on
`stale_generation_event`.
It excluded the other company's stale run, the `process_lost` run and a
non-budget event.
- `node scripts/crash-points.mjs --list --env daytona` lists 19 points,
with `needs:` on the three. `--env daytona` with no key or image fails
at once with the missing names.
- No harness run: the harness slot is held for other work. The next
suite run exercises the new checks.

## Risks

- Test harness only; no product code.
- I6 is a proxy: it matches the run's error text and code. A failure
that names a generation for another reason would be reported, with its
evidence, for a person to judge.
- I8 can flag breaches on a loaded host. Each breach carries its
measured and budget values, so a load-caused breach is visible as one.

## Model Used

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

## 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 00:06:01 -06:00
DottaandClaude Opus 5.5 051834bdc8 feat(chaos): add the on-demand crash-point suite and its manual CI workflow (#19946)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A control-plane restart between two durable writes leaves an
in-progress record owned by a dead process
> - Random restart-chaos waves found these 20 ms–1 s windows only by
luck
> - #19937 added `crashPoint(name)`, and #19938 put it at 20 durable
write boundaries
> - Nothing yet runs a task, kills the server at a chosen point,
restarts it, and checks the plan's invariants
> - This pull request adds that on-demand suite, as a local command and
as a manual CI workflow (not a required check, per Decision 2)
> - The benefit is a repeatable work list: which boundary breaks which
invariant, for Phases 2–4

## Linked Issues or Issue Description

Refs #19831 (plan, Phase 5 and Decision 2). Refs #19861 (report-only
reconciler used for I1–I7). Stacked on #19938 (call sites), which is
stacked on #19937 (helper).

**Problem**
The named crash points cannot be exercised end to end. No command arms
one point, runs real agent tasks across it, restarts, and grades the
result.

**Expected behavior**
`node scripts/crash-points.mjs --points <glob> --kinds <id> --adapters
<list>` runs one wave per selected point and reports each point as pass,
fail (with failing R rules and violated invariants), not reached, or
harness error. A `workflow_dispatch` workflow runs the same thing on
demand.

## What Changed

- **Runtime arming.** `PAPERCLIP_CRASH_POINT_CONTROL=<file>`, in the
shared helper and the runner mirror. Each hit reads `<token>
<name>[:<nth>]`, and a new token resets the count. It is test mode only:
`NODE_ENV=production` ignores it, and the disarmed path is still one
comparison.
- **`tests/restart-chaos/main.ts --crash-point
<a>:<nth>,<b>:<nth>,...`** (config, types, ledger):
  - one booted server and no Vite UI;
- per point: take an invariant baseline, launch the point's batch, arm
the point, then on rest disarm it and check invariants as violations
that are new since the baseline;
  - every task is graded R1–R6 and T;
  - `crash-point.json` (v2) holds one entry per point.
- **`tests/restart-chaos/supervisor.ts`:**
  - every spawn gets the control file;
- a SIGKILL whose receipt names the child's pid is restarted like K1:
the control file is emptied first, the owned tree is killed, and a
replacement starts. A failed tree cleanup fails the fault;
- the isolated instance pins `PAPERCLIP_IN_WORKTREE=false` (a
`.paperclip/.env` above the checkout otherwise suppressed all
scheduling);
  - `RESTART_CHAOS_HEALTH_TIMEOUT_MS`.
- **`tests/restart-chaos/invariants.ts`:**
- the Phase 1 report-only pass with `record: false`: two real-clock
passes for I1 and I2, and one pass 15 minutes ahead for I3, I4 and I7;
  - spool kinds are read from the wave's instance;
  - an unfinished pass is a harness error;
  - each violation carries its wake and run state as evidence.
- **`tests/restart-chaos/crash-points.ts` and
`scripts/crash-points.mjs`:**
- select points by glob, kind and environment, and run them in one wave;
  - a per-point outcome: pass, fail, not_reached or harness_error;
  - `SUMMARY.md` with a violation evidence column.
- **`.github/workflows/crash-points.yml`**, manual only, under the
paid-workflow contract:
  - a credential-free `authorize` job;
  - a `suite` job in `runner-e2e-paid` whose first step reauthorizes;
  - a frozen install with `--ignore-scripts` and no shared cache;
  - only the selected adapters' keys;
  - the workflow is added to both security-test allowlists.
- **Docs:** "Crash-point suite" in `tests/restart-chaos/README.md`, and
"Built (Phase 5)" plus the first work list in the plan.

## Verification

- Focused tests:
- `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts
tests/restart-chaos/crash-points.test.ts`;
- `cd packages/shared && npx vitest run src/crash-points.test.ts`: 15
tests, including runtime arming in a real child process;
- `node --test scripts/__tests__/release-verify-workflow.test.mjs`: 72
tests;
  - `tests/runner-e2e/workflow-security.test.ts`: 19 tests.
- Typecheck: `node scripts/typecheck-test-projects.mjs
tests/restart-chaos` passes.
- Loop line: question "how long is one crash-point loop?" | loop: `node
scripts/crash-points.mjs --points <one>` (codex A1) | elapsed 2.7 min
end to end: boot 9 s, crash 9 s after arming, replacement ready in 6 s,
rest about 2 min | result: one point alone takes about 3 min. Before
this change it took about 15 min (one cold boot per point, Vite UI, host
at load 77).
- Full local run, 14 points, codex A1, 11.8 min in all:
- every point was reached (the server killed itself at each one, and the
replacement came back);
  - 11 pass;
- `workspace.finalize.after_start_before_copyback` and
`workspace.finalize.after_copyback_before_done` fail I2 and R6: a
`workspace_finalize` operation stays running after the run succeeded;
- `wake.claim.after_run_running_before_wakeup_claimed` reports I4. Its
evidence shows the wake failed with "…retrying once", and the retry
delivered the work, so this is the known refusal-regex false positive.
- The 6 Daytona-only points were not run.

## Risks

- Test harness and an on-demand workflow only. No product code changes.
- `.github/**` needs a CODEOWNER review. The workflow uses provider
keys, so it refuses non-default branches and actors that are not on the
allowlist, like `runner-full-stack-e2e.yml`.
- `PAPERCLIP_IN_WORKTREE=false` is pinned in every chaos wave, not only
crash-point waves. A chaos instance is never a worktree dev instance, so
scheduling must not be suppressed.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 23:41:15 -06:00
DottaandClaude Opus 5.5 4aed3056db feat(ui): steer a queued follow-up into the live turn after a 5 s edit window (#19970)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - When a person comments on a task while the agent works, the comment
waits in a queue until the current run ends, and only the next run reads
it
> - Measured locally, a comment posted during a Codex tool call waited
54–68 s for a reply (median 64 s)
> - Codex and Pi can take a message into the live turn ("Steer"), but
today the person must click Steer on each queued message
> - Restart recovery now delivers a steered message that a restart
interrupted (#19962), so steering is safe to do automatically
> - This pull request steers a queued follow-up into the live turn on
its own after a 5-second window in which the person can edit or cancel
it
> - The benefit is a faster reply to mid-turn comments on Codex and Pi,
with no new decision for the person

## Linked Issues or Issue Description

**Feature**
Send a queued follow-up into the running turn automatically when the
runner supports steering.

**Problem**
A follow-up comment posted during a running turn waits for the whole
turn to end and for a new run to start. Steering is available for Codex
and Pi, but only as a manual button on each queued message.

**Proposed behavior**
After a 5-second edit window, the queued message is steered into the
live turn. During the window the row shows "Sending in Xs" with Edit and
Cancel. If steering is unavailable, the message stays queued as before.
Claude, OpenCode and Cursor keep the queue. Steer and Interrupt stay
available as buttons.

Refs #19962

## What Changed

- `queued-message-auto-steer.ts`: one constant,
`QUEUED_MESSAGE_AUTO_STEER_DELAY_MS = 5_000`, a deadline helper and an
eligibility check. A message steers automatically only when all of these
are true:
- steering is available for the queue, and it is a `paperclip_runner_v1`
queue with a queue id and a target run;
  - the person can edit the message;
- it is not an interaction response or a response that requires a fresh
session;
- it is not optimistic, cancelled, already attempted, or open in the
editor.
- `TaskChatQueuedMessages`: countdown row "Sending in Xs" with Edit and
Cancel in place of Steer and the "…" menu. The deadline is `max(message
changed, first eligible) + delay`, so an old queued message does not
send the moment its turn becomes steerable, and an edit restarts the
window. When the deadline passes, the row uses the existing Steer
action, so the server path, idempotency and fallback stay the same.
Cancel returns the row to the normal Steer button. The countdown is
announced once to screen readers, not on every tick.
- `TaskChatThread` turns it on with the constant and passes the message
being edited, so an open edit never sends. The prop stays off by default
for other callers.
- Storybook: `AutoSteerCountdown` (with a play test for Cancel) and
`AutoSteerCountdownMobile` in "Composer / Steering and queued messages".
- Restart chaos TTFT driver: a `midturn` phase measures comment-to-reply
for a comment posted during a 40-second tool call, with `--midturn-mode
queue|steer` and `--midturn-fault K1|K2|K6`. It records the reply count,
so exactly-once delivery is checked.

## Verification

- `pnpm --filter @paperclipai/ui vitest run
src/components/task-chat/TaskChatQueuedMessages.test.tsx
src/components/TaskChatThread.test.tsx`: 434 passed. Seven new
auto-steer tests use fake timers: countdown and send, Cancel, Edit
restarts the window, an open edit never sends, ineligible queues stay
manual, no instant send for an old message, one attempt per revision.
They fail with the trigger disabled.
- `node scripts/check-token-gates.mjs`: clean.
- Storybook in the browser, desktop and mobile: the first row counts
down and steers ("Message steered into the active turn."). The cancelled
row shows the normal Steer button. On mobile the text truncates and the
row fits.
- Mid-turn latency, local, Codex, comment during a 40-second tool call:
queue 54–68 s (median 64 s), steer 47–61 s (median 50 s). The steer was
acknowledged in 0.8–0.9 s. The remaining time is the tool call and the
model, not the hand-off.
- Restart chaos, Codex, steer mode, faults K1, K2 and K6, 2 samples
each, on #19962 plus this branch: 6 of 6 messages acknowledged, still
unread at the fault, recovered, and answered exactly once (before
#19962: 0 of 6).

## Risks

- A message now leaves the queue after 5 seconds unless the person edits
or cancels it. The window and the countdown make this visible, and
steering is only automatic where the provider supports it.
- Pi: a steer the model already read before a restart can arrive again
after recovery, because Pi reports no read evidence (see #19962).
Automatic steering makes this a little more likely.
- If steering fails, the message stays queued and is delivered by the
next run, as today.
- UI only and a test harness. No schema, API or server change.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with
tool use (shell, file editing, browser checks, test 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 Opus 5.5 <noreply@anthropic.com>
2026-11-07 22:59:44 -06:00
DottaandClaude Opus 5.5 f35eb1e5ef fix(observability): judge the TTFT budget from run creation; report the wait behind a live turn (#19941)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Invariant 8 of the durable-ownership plan says turns must start
fast. #19863 added per-run speed budgets that write
`run.performance.budget_exceeded` to the run log.
> - The Phase 2 chaos data (#19927) showed follow-up TTFT about 3x over
the claude budget.
> - The cause is measurement scope. A follow-up comment posted during a
live turn waits for that turn to end, and TTFT is anchored at the
comment. The budgets came from idle follow-ups.
> - This pull request judges the TTFT budget from run creation and
reports the wait separately.
> - The benefit is that budget breaches point at real start-up slowness,
not at time spent behind a turn the user is already watching.

## Linked Issues or Issue Description

Refs #19863 (speed budgets), #19927 (Phase 2 data), #19852 (startup
sub-spans), #19888 (warm default).

**What happened?**
In two local storm waves, C1 follow-up runs breached the claude warm
TTFT budget (p50 27.6–29.9 s against 9.5 s). Their spans show
`comment.to_run_created` at 6–62 s (waiting for the live turn), then
session resume at 1.4–3.8 s and submit-to-first-event at 2–5 s. Codex
without the wait was about 4.8 s, the same as the warm-follow-up
baseline.

**What did you expect?**
The budget should judge start-up latency, which is what its baseline
measured. The wait behind a live turn should be visible but not judged.

**Steps to reproduce**
1. Run a local chaos storm with C1 tasks.
2. Read `run.performance.budget_exceeded` rows with metric `ttft` for
`issue_commented` runs, and compare them with each run's
`comment.to_run_created` span.

## What Changed

- `native-run-trace.ts`: the TTFT budget check measures from run
creation (start of `heartbeat.queue`, or end of the ingress span). The
TTFT span itself does not change.
- `native-speed-budgets.ts`: breaches can carry `inputWaitMs` (TTFT
only).
- `tests/restart-chaos/ttft-baseline-stats.ts` and `ttft-baseline.ts`:
the wait behind a live turn is its own measured step (`inputWait`), next
to the full TTFT and the TTFT from run creation. The TTFT table gains a
"Wait behind live turn" column. The full comment-to-first-token time
stays the user-visible metric.
- `doc/run-log-events.md` and the plan's speed-budget section describe
the rule.

## Verification

- `cd server && npx vitest run
src/services/native-runtime/native-speed-budgets.test.ts
src/services/native-runtime/native-run-trace.test.ts`: 43 pass, 2 new.
`pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts
tests/restart-chaos/ttft-baseline-stats.test.ts`: 12 pass, 1 new. They
cover a follow-up under budget after a 60 s wait (no breach), a
follow-up over budget (breach with `inputWaitMs`), and queueing after
run creation still judged.
- Server typecheck passes.

## Risks

- Low. This is a run-log-only change (AGENTS.md §5.7): no Telemetry and
no span attributes change.
- A slow wait before run creation is no longer a TTFT budget breach. The
TTFT span still records it, and the breach payload names it. Admission
holds after run creation are still judged.

## Model Used

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

## 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-07 20:27:41 -06:00
DottaandClaude Opus 5.5 17b9e10252 docs(plans): Phase 2 reconciler data; let chaos K4 holds reach 600 s (#19927)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Control-plane restarts leave in-progress records that each subsystem
releases in its own way. The durable-ownership plan replaces those paths
with one shared path.
> - Phase 1 added a report-only reconciler. Its light local waves showed
zero violations, and that proves little.
> - Phase 2 moves kinds to the shared path. It needs data that shows
which kinds really leak, and the speed numbers to protect.
> - This pull request records that data from harsher waves. It also
fixes the harness so that K4 outages past the reconnect grace qualify.
> - The benefit is that Phase 2 moves kinds in order of measured risk,
against a known speed baseline.

## Linked Issues or Issue Description

Refs #19831 (durable-ownership plan), #19861 (report-only reconciler),
#19863 (speed budgets), #19797 (K4 hold cap).

**What is the problem?**
Phase 2 needs per-kind invariant data and speed data from harsh restart
waves. Only light local waves existed. In addition, a K4 hold over about
405 s always failed its fault receipt, so the 600 s cap from #19797
could not be used.

**Where is it documented?**
`doc/plans/2026-11-07-durable-ownership-and-reconciliation.md` (Phase 2
rows) and `tests/restart-chaos/README.md`.

**What should change?**
Record the data in a dated plan doc. Let chaos K4 holds run up to 600 s
and still qualify.

## What Changed

- Added `doc/plans/2026-11-07-phase2-reconciler-data.md`. It covers 5
unmasked local waves (two storms, a crash loop, two K4 runs). It gives
per-kind violation counts with example rows, owner verdicts, TTFT, boot
and restart recovery against the budgets, Phase 3 gate notes, and what
is not covered (Daytona).
- Added a short pointer to that doc in the durable-ownership plan.
- `tests/restart-chaos`: new flag `--k4-hold-max-seconds` (120–600). It
raises the upper end of the seeded K4 hold. The default range (120–300
s) does not change.
- `tests/runner-e2e/controller-outage.ts`: the receipt's ready bound is
now a parameter (`CONTROLLER_READY_BY_MS`, default 420 s). Runner suites
keep 420 s. The chaos supervisor passes hold + 420 s, so a hold past the
Runner's reconnect grace qualifies.

## Verification

- `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts`:
12 files, 135 tests pass, including the new flag test.
- `pnpm exec vitest run --config tests/runner-e2e/vitest.config.ts
tests/runner-e2e/controller-outage.test.ts`: 36 pass, including the new
long-hold receipt test.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos` and
`tests/runner-e2e`: pass.
- Live: the K4 wave at seed 9203 failed 3 of 6 tasks with the old bound
(fault error "did not produce a qualifying replacement receipt"). It
passed 6 of 6 with this fix, with holds of 294–579 s.

## Risks

- Low risk. The changes are test harness code and docs only. The runner
suites keep the 420 s ready bound by default.
- The data is local only. The doc says which kinds the waves do not
reach.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, in Claude Code with
tool use (shell, file edits). The model ran the chaos waves and wrote
the analysis and 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
- [ ] I will address all Greptile and reviewer comments before
requesting merge

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 19:55:02 -06:00
DottaandClaude Opus 5.5 d157ce223c fix(evals): keep large failed private evidence and the merged report within the private checks (#19932)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The chat evaluation lane runs each chat case in a paid runner and
keeps the private evidence of each attempt in a bounded private archive
> - A failed attempt keeps its Playwright trace and video, and the
bundle has a 256 MB allowance
> - Playwright's HTML reporter copies each attachment into
`html-report/data/<sha1>.<ext>`, and the same bytes also stay in
`playwright-output/`
> - The bundle holds the original tree and the packaged tree, so a 30 MB
trace used about 125 MB of the allowance before the blob report copies
> - Large failed attempts went over the allowance, and then all of their
private evidence was lost
> - This pull request removes the `playwright-output/` copy only when
the HTML report holds the identical bytes
> - A second gap lost the whole merged campaign report of failed cells:
the report copied trace viewer font, SVG and manifest files to a place
where the trusted seal refuses them
> - This pull request stops that copy, and it names a refused report
carrier with a closed category
> - The benefit is that large failed attempts keep their evidence, the
merged report is kept, and the HTML report and its trace viewer still
open the trace

## Linked Issues or Issue Description

**What happened**
Failed chat evaluation attempts with large Playwright traces lost all of
their private evidence. The bundle went over the 256 MB private evidence
allowance. For example, the failed `tasks.organize-open-work` cell made
a 254 MB bundle. That bundle held the same 30.7 MB trace four times
(`playwright-output/.../trace.zip` and `html-report/data/<sha1>.zip`, in
both the original tree and the packaged tree), plus two blob report
copies. Larger traces went over the limit, and then the transfer refused
the whole bundle.

Also, the merged campaign report of a failed cell was refused with
`refusal: unsupported-packaged-evidence-carrier` (for example
`runs-adapters.inspect-failed-run`, run 42431162923). The cell's
evidence manifest lists its trace viewer files
`html-report/trace/codicon.*.ttf`, `playwright-logo.svg` and
`manifest.webmanifest`. `report.ts` copied them to
`normalized/evidence/<id>/attempt-1/html-report/trace/`. The
generated-report seal accepts trace viewer files only under
`html/trace/`, so it refused these three files and lost the whole
report. I reproduced this from the retained cell archive: a merge of its
blob report passes the seal rule, and the three normalized copies are
the only files that fail it.

**Expected behavior**
A failed attempt keeps its trace, and the trace uses as little of the
allowance as possible. The HTML report and the trace viewer still open
the trace for a person who downloads the bundle. The merged report of a
failed cell is kept. If the seal refuses a carrier, the refusal names
its category, not its path. No privacy, integrity or size check changes.

**Steps to reproduce**
1. Dispatch `runner-full-stack-e2e.yml` with `cadence=manual` for a
private chat case that fails with a large trace, for example
`chat-features.runner-opencode.local.tasks.organize-open-work`.
2. Download `manual-cell-<sha256(executionId)>.zip` from the private
history bucket.
3. List the archive. The trace bytes are in `playwright-output/` and in
`html-report/data/` in both `originals/` and `sealed-results/`.
4. For the report refusal: look at the "Retain complete manual merged
evidence only in restricted storage" step of the same run. It prints
`refusal: unsupported-packaged-evidence-carrier` and no
`manual-report.zip` is retained.

## What Changed

- Add `tests/runner-e2e/private-report-copies.ts` with
`dropDuplicatePlaywrightOutput`. It removes a `playwright-output/` file
only when `html-report/data/<sha1 of content><ext>` is a regular file
(not a symlink) with the same size and the same SHA-256. It writes the
removed paths, their kept paths and digests to
`snapshots/playwright-output-copies.json`.
- The function does nothing when the cell has a framework JSON report
(`framework-report.json` or `snapshots/playwright-framework.json`),
because that report registers `playwright-output/` attachments by path.
It also does nothing when `playwright-output`, `html-report` or
`html-report/data` is missing or is a symlink.
- Call it in the private manual branch of `tests/runner-e2e/launch.ts`,
before packaging and before both seals. Both evidence trees then keep
one HTML report copy of each trace and video.
- The HTML report, its trace viewer assets and the blob report do not
change.
- `tests/runner-e2e/report.ts` does not stage a cell file into
`normalized/evidence/` when its type is one the generated-report seal
refuses (the same `isStandardEvidenceFile("html-report/...")` rule). The
merged report has its own authenticated trace viewer under
`html/trace/`. The cell archive still keeps the files.
- `tests/runner-e2e/manual-private-transfer.ts` adds
`assertGeneratedReportCarriers`, which runs before the report seal. It
throws a fixed message per closed category (`font`, `svg`,
`webmanifest`, `image`, `source map`, `no extension`, `other`, `trace
viewer asset`, `trace viewer bytes`), so the refusal line names, for
example, `unsupported-generated-report-carrier-font`. It never names a
path, and the seal stays the authority.
- No change to the target-checked trusted files
(`manual-private-handoff.ts`, `manual-private-retention.ts`,
`manual-private-report-assets.ts`), so candidate branches do not need an
update. No change to the limits or the secret scans.

## Verification

- `vitest run --config tests/runner-e2e/vitest.config.ts
tests/runner-e2e/private-report-copies.test.ts
tests/runner-e2e/manual-private-cell-report-assets.test.ts
tests/runner-e2e/private-framework-report.test.ts
tests/runner-e2e/manual-private-handoff.test.ts`: 79 passed.
- The new tests use a real failed Playwright run with the HTML, JUnit
and blob reporters and a real retained trace. They show these things:
- Only the duplicate `playwright-output/` files are removed. The HTML
report and blob report bytes do not change.
- The deduplicated cell passes `packageEvidence`,
`sealStandardOriginalEvidence`, `sealManualResultEvidence`, the trusted
`validateSealedOriginals` and `validateSealedInventory`, the strict
archive pack and unpack, and `stageSealedPrivateCell`.
  - A tampered or symlinked HTML report copy keeps the original file.
  - Framework JSON cells and symlinked data directories are not changed.
- `vitest run --config tests/runner-e2e/vitest.config.ts manual-private
private-report private-refusal private-framework generated-report
report`: 25 files, 311 passed.
- `generated-report-carriers.test.ts` shows each refused carrier gets
its closed category code, the code has no path, and the real seal
refuses the same tree. `report.test.ts` shows the font, SVG and manifest
are not staged and the other files are.
- A new test runs the real launcher private branch from `launch.ts`. It
shows that the sealed originals keep one trace copy, in
`html-report/data/`.
- Live run: I applied the commit to a branch from
`chat-eval/master-candidate` (`chat-eval/case-evidence-dedupe`) and
dispatched
`chat-features.runner-opencode.local.tasks.organize-open-work` (run
42432263920). I will add the bundle size result in a comment.

## Risks

- Low risk. The change runs only in the private manual evidence branch.
It removes a file only after a full content comparison with the kept
copy. If a check fails, the file stays.
- A person who wants the trace must open `html-report/data/<sha1>.zip`
(the HTML report links to it), not `playwright-output/<test>/trace.zip`.
The receipt in `snapshots/playwright-output-copies.json` names the new
location.
- `report.ts` and `manual-private-transfer.ts` run from the master
checkout in the report job, so the report fix takes effect only after
merge. The normalized per-cell HTML report copy no longer has the viewer
font, logo and manifest. The merged report under `html/` and the cell
archive keep a complete viewer.
- The blob report still has a compressed copy of the trace, and the
bundle still holds the original and the packaged tree. A trace over
about 60 MB can still go over the allowance. This change does not fix
that case.

> 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 Opus 5.5 (`claude-opus-5-5`), run in Claude Code with
tool use (shell, file editing, GitHub CLI and AWS 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
- [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-07 19:37:12 -06:00
DottaandClaude Opus 5.5 1de794cb80 fix(evals): name runtime-built evidence refusals by their fixed template (#19920)
## Thinking Path

- #19908 made a refused private evidence transfer print a closed refusal
code instead of one generic line.
- Its first real use printed `refusal: dynamic`: the trusted modules
build some refusals at runtime (secret-leak findings,
publication-boundary limits), and those were deliberately not named
because their text can carry paths or counts.
- The fixed part of each template is safe and is exactly what an
operator needs to know which check refused.

## Linked Issues or Issue Description

**What happened**

After #19908, a refused `runs-adapters.inspect-failed-run` attempt
(workflow run 42422571919) printed `Restricted manual evidence operation
failed (refusal: dynamic)`, so the reason was still unknown.

**Expected behavior**

A refused transfer still keeps message text private, but names which
check refused it with a closed code, so the attempt can be diagnosed.

**Steps to reproduce**

1. Dispatch `runner-full-stack-e2e.yml` manually for a chat cell whose
private evidence fails transfer validation.
2. Open the "Retain complete approved manual originals" step log: it
shows the single fixed failure line with no reason.

## What Changed

- `private-refusal-code.ts`: runtime-built refusals from the trusted
modules map to fixed template codes (`template-secret-leak`,
`template-publication-boundary-exceeded`,
`template-invalid-file-metadata`,
`template-history-expiration-unprovable`). Variable parts are never
printed.
- Tests: each template is named without its variable text, and a source
scan asserts every runtime-built `new Error` in the trusted modules maps
to a named code (not `dynamic`).

## Verification

- `pnpm exec vitest run --config tests/runner-e2e/vitest.config.ts
tests/runner-e2e/private-refusal-code.test.ts
tests/runner-e2e/manual-private-transfer.test.ts`: 20 passed.
- `npx tsc --noEmit -p tests/runner-e2e`: clean.

## Risks

- Template codes reveal which kind of check refused (for example that a
secret-shaped value was found), not where or what. That is the intended
operator signal; the finding itself stays private.

## Model Used

Anthropic Claude Opus 5.5 (`claude-opus-5-5`), via 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
- [x] I will address all Greptile and reviewer comments before
requesting merge

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 18:41:39 -06:00
DottaandClaude Opus 5.5 ba3fe09954 fix(evals): name why a private evidence transfer refused, without its text (#19908)
## Thinking Path

- Paid chat evals retain their evidence through a trusted transfer
(`tests/runner-e2e/manual-private-transfer.ts`) that runs from master,
separate from the target checkout.
- When the transfer refuses an attempt, it prints one fixed line,
"Restricted manual evidence operation failed; no public fallback", and
keeps nothing. The closed diagnostic on the target side cannot see this
step either.
- So a refused attempt leaves no evidence and no reason.
`runs-adapters.inspect-failed-run` lost three attempts in a row this
way, and `goals.create-goal-tree` lost one; nobody could tell which
check refused.
- This change keeps the privacy rule (no message text in public logs)
but names the refusal with a closed code.

## Linked Issues or Issue Description

**What happened**

A paid chat-eval attempt whose private evidence the trusted transfer
refused printed only "Restricted manual evidence operation failed; no
public fallback". The receipt showed `attemptedSourceBytes: 0` and
`transfer.status: failed_or_unknown`, and no evidence was kept, so the
reason was unknowable. `runs-adapters.inspect-failed-run` lost three
consecutive attempts this way.

**Expected behavior**

A refused transfer still keeps message text private, but names which
check refused it with a closed code, so the attempt can be diagnosed.

**Steps to reproduce**

1. Dispatch `runner-full-stack-e2e.yml` manually for a chat cell whose
private evidence fails transfer validation.
2. Open the "Retain complete approved manual originals" step log: it
shows the single fixed failure line with no reason.

## What Changed

- New `tests/runner-e2e/private-refusal-code.ts`:
`privateRefusalCode(error)` returns a slug of the message only when it
is a fixed literal `new Error("...")` in the trusted evidence modules
(transfer, handoff, retention, report assets, history, framework report,
history budget, evidence, redaction). Otherwise it returns `fs-<code>`
for Node error codes, `error-type-<Name>` for typed errors,
`non-error-thrown`, or `dynamic`. Message text is never returned.
- `manual-private-transfer.ts`: the failure line now includes `(refusal:
<code>)`.
- Tests: fixed literals are named; dynamic, filesystem, typed and
non-error throws never leak their text.

## Verification

- `pnpm exec vitest run --config tests/runner-e2e/vitest.config.ts
tests/runner-e2e/private-refusal-code.test.ts
tests/runner-e2e/manual-private-transfer.test.ts`: 15 passed.
- `npx tsc --noEmit -p tests/runner-e2e`: clean.
- The three files the transfer requires to match between trusted and
target checkouts (`manual-private-handoff.ts`,
`manual-private-retention.ts`, `manual-private-report-assets.ts`) are
unchanged.

## Risks

- The public log line gains a code. Codes are either fixed literal
messages (already public in this repository's source), Node error codes,
or error type names. Dynamic messages, which can contain paths, are
reported only as `dynamic`.
- Reading the module sources once at failure time adds a small file read
on the error path only.

## Model Used

Anthropic Claude Opus 5.5 (`claude-opus-5-5`), via 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
- [x] I will address all Greptile and reviewer comments before
requesting merge

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 16:28:03 -06:00
DottaandClaude Opus 5.5 fc6e1b0346 perf(native): keep local Codex and OpenCode runners warm by default (#19888)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - When someone messages an agent, the turn must start fast. Time to
first token (TTFT) is a first-class metric.
> - The Phase 0 baseline (#19846) found that every local turn launches a
new runner and loads the provider session again, because `per_turn` is
the default lifecycle. That made a follow-up message slower than a new
task.
> - Warm runners keep the runner and provider session alive between
runs. #19874 fixed the defect that broke warm OpenCode follow-ups, and
#19879 bounds an idle warm runner whose controller dies.
> - This pull request makes warm the default for local Codex and
OpenCode agents, and migrates existing agents that only carried the old
creation default.
> - The benefit: follow-up TTFT p50 drops from 12.0 s to 4.7 s (Codex)
and from 21.7 s to 4.2 s (OpenCode), with no new restart-chaos failures.

## Linked Issues or Issue Description

Refs #19846 (TTFT baseline), #19852 (startup sub-spans), #19874 (warm
completion contract), #19879 (warm reconnect grace).

**What existing behavior does this improve?**
The default runner lifecycle for native agents, and how a follow-up
message starts.

**Current behavior**
Agent creation writes `lifecycleMode: "per_turn"` into every native
agent. Every follow-up launches a new runner and restores the provider
session: 4.2 s (Codex) and 16.3 s (OpenCode) at p50 before the turn
starts.

**Proposed behavior**
New native agents store no lifecycle. On a local target, an unset
lifecycle resolves to `warm` with the default 5-minute idle window for
Codex and OpenCode. Other harnesses, remote targets and managed
providers without warm support stay `per_turn`. An explicit choice
always wins. Migration 0443 clears the stored creation default from
existing agents, except where an edit set "Turn by turn" explicitly.

**Reason and benefit**
Measured locally (`doc/plans/2026-11-07-warm-follow-up-ttft.md`). Warm
follow-ups are 2.6–5× faster for Codex and OpenCode, and restart
recovery latency is equal or lower.

## What Changed

- `native-lifecycle.ts`: an unset `agentMode` resolves to warm (5-minute
idle window) for local `codex_app_server` and `opencode_server` agents.
`runtime-selection.ts` passes `localTarget`. Explicit `per_turn` or
`warm` wins. An admitted session keeps its persisted policy. ACPX Claude
stays `per_turn`: it relaunches its provider process on every follow-up
by design, so warm did not shorten its follow-up (10.7 s warm vs 10.5 s
per_turn).
- Creation and configuration writes no longer store `lifecycleMode:
"per_turn"` for Codex, Claude or OpenCode. This covers
`normalizePaperclipRunnerAdapterConfig` (runs on every create and
update), `paperclipRunnerTransitionConfig` (switching to the native
runner), `agent-runner-selection.ts`, `codex-local` `build-config.ts`,
and the simplified new-agent setup (`paperclipRunnerCreationLifecycle`
in `@paperclipai/shared`). Grok, Pi and Cursor keep their explicit
value.
- Agent form: the Runner lifecycle field shows the unset state as
"Automatic". "Turn by turn" and "Warm session" remain.
- Migration `0443_native_warm_follow_up_default.sql`: clears
`lifecycleMode = 'per_turn'` from native Codex, OpenCode and Claude
agents unless `agent_config_revisions` shows an edit that set it to
`per_turn`. Each migrated agent gets an `agent.updated` activity entry
with the old value (`actor_id =
migration:native-warm-follow-up-default`). Creation records no revision,
so a "Turn by turn" pick made at creation cannot be told apart from the
old default. The product owner approved migrating those agents. Claude
agents follow the default, which is `per_turn` today.
- Restart chaos harness and TTFT driver: `--lifecycle
per_turn|warm|default` (saved in the wave plan). `default` removes the
catalog's pinned lifecycle, so a wave exercises the real default path.
- Docs: `doc/DEVELOPING.md` "Runner lifecycle default", and the results
in `doc/plans/2026-11-07-warm-follow-up-ttft.md`, which also documents
baseline finding 4 (Claude's resumed turn-start gap is ACPX's per-turn
process generation, not the provider).

## Verification

- `server`: `native-lifecycle.test.ts` (new default cases: local Codex
and OpenCode warm, Claude, remote, unknown target, unmeasured harness,
managed Claude, admitted per-turn session, explicit choices, non-native
adapter), `agent-runner-selection.test.ts`, `runtime-selection.test.ts`
(335 pass).
- `packages/db`: `native-warm-follow-up-default-migration.test.ts` on
embedded Postgres. Default agents are cleared, the explicit edit is
kept, other harnesses and legacy adapters are untouched, audit rows are
written once, and a rerun is a no-op. Without the explicit-edit filter
the test fails. `check:migrations` passes.
- `codex-local` `build-config.test.ts` (65), `ui`
`config-fields.test.tsx` (40), restart-chaos unit tests (134), `tsc
--noEmit` for `server`.
- `adapter-utils` `paperclip-runner-lifecycle-default.test.ts`:
configuration writes and adapter conversion store no lifecycle for
Codex, OpenCode and Claude, keep `per_turn` for the others, and preserve
explicit choices. It fails without the fix.
- End-to-end default check on this branch (`ttft-baseline.ts --lifecycle
default`, codex and opencode, 2 samples): the agents read back with no
stored lifecycle, and every follow-up reused the runner and provider
session (no `runner.process.launch`). Follow-up TTFT was 3.6–4.8 s. This
check found the per-write `per_turn` injection fixed here.
- Restart chaos (codex and opencode, A1/B1/B5, K1/K2/K6, seed 4242), 8/8
PASS each with no open signatures: forced warm, forced per_turn, and
`--lifecycle default` on this branch (agents created without a
lifecycle).
- `pnpm verify` selects broader CI (harness config is a global input),
so required CI supplies the broad coverage.

## Risks

- Behavior change: local Codex and OpenCode agents keep a runner and
provider process for up to 5 minutes after a run. Instance sleep waits
for that idle window. The process is reaped by the idle timer, by
graceful shutdown, or by the #19879 reconnect grace if its controller
dies.
- Migration: agents whose "Turn by turn" was chosen at creation become
Automatic. The activity entry records the old value; choosing "Turn by
turn" restores it.
- Previous-release story (`rules/upgrade-safe-run-state.mdc`): a task
session keeps the lifecycle policy it was admitted with, so sessions
saved before this change stay `per_turn`. New sessions get the default.
No run, checkpoint or launch state changes shape.

> 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 Opus 5.5 (`claude-opus-5-5`) in Claude Code, with
tool use (shell, file edit, test 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 Opus 5.5 <noreply@anthropic.com>
2026-11-07 14:37:36 -06:00
DottaandClaude Opus 5.5 6cfb69b4b0 docs(plans): Phase 0 ownership inventory and TTFT baseline (#19846)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A control-plane restart can land between two durable writes. It
leaves an in-progress record owned by a dead process. About 69
restart-chaos PRs fixed that shape one subsystem at a time.
> - The durable ownership plan (#19831) replaces those fixes with one
owner stamp, one prover, write-ahead intents and one reconciler. Phase 0
must first list every in-progress record kind and measure how fast turns
start today.
> - Without the inventory, Phase 1 cannot choose one stamp or one proof.
Without the baseline, no later phase can show that it kept turns fast
(invariant 8).
> - This pull request adds the Phase 0 inventory, the TTFT, boot and
restart-recovery baseline, and the run-log-only driver that produced the
baseline.
> - The benefit is one reviewed map of 83 record kinds to plan sections
A–G, and measured p50/p95 budgets per adapter.

## Linked Issues or Issue Description

Refs #19831. Refs #19838 (the TTFT anchor that the baseline uses).

**What is the problem?**
Phase 0 of the durable ownership plan has no inventory of in-progress
record kinds and no speed baseline.

**What is the expected outcome?**
Two reviewed documents in `doc/plans/`, plus a repeatable way to measure
the baseline again in later phases.

**Why does it matter?**
Phase 1 cannot start without them, and every later phase's exit criteria
compare against the baseline.

## What Changed

- `doc/plans/2026-11-07-ownership-inventory.md`:
- 83 durable in-progress record kinds (94 reader rows) across native
runtime, environments, workspaces, accounting and the wake queue;
- each kind with its table and column, writer, states, owner, release
rule, proof, obligation, invariants and plan sections A–G;
- the overlaps, the merge decisions and the zero-downtime upgrade record
kinds.
- `doc/plans/2026-11-07-ttft-baseline.md`:
- TTFT, agent boot, pre-turn steps and restart recovery per adapter
(codex, claude, opencode), local, cold and warm, p50/p95;
- restart recovery from the overnight chaos evidence, the slowest steps
ranked, and the gaps.
- `tests/restart-chaos/ttft-baseline.ts`: an on-demand driver on the
chaos supervisor. It reads the run log only, never Telemetry, and always
stops its embedded Postgres.
- `tests/restart-chaos/ttft-baseline-stats.ts` and its unit tests: pure
percentile, boot, pre-turn ranking and recovery statistics.

No runtime behavior changes.

## Verification

- `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts
tests/restart-chaos/ttft-baseline-stats.test.ts`
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`
- The baseline numbers come from the driver run named in the report. To
reproduce them, follow the report's "Method" section.
- The owner spot-checked the inventory's file:line claims at
`f2d8a51027`. One example: `processLossRetryCount` has no writer, and
`heartbeat/recovery.ts:2675` is its only reader.

## Risks

- Low risk. This adds documentation and an opt-in test driver. Nothing
runs in CI except the pure stats unit tests.
- The driver spends a small amount of provider credit when someone runs
it by hand.
- The baseline sample is small (6 per cell). The local host was shared
with other worktrees, so the p95 tails include load. The report states
both.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), via Claude Code with tool use,
code execution and five parallel read-only subagents for the inventory.

## 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-07 10:47:31 -06:00
377bc87df1 feat(connectors): show and select personal AI account defaults (#19845)
Show a crown on each saved personal AI provider default. Add a provider-specific menu action to other eligible accounts. Share saved-state queries, block duplicate writes, and keep the saved crown until a write succeeds.

Keep account details available for accessible grants. Show session lookup errors with Retry. Resolve list permissions once per request and index account lookups. Add UI and API regression coverage and account documentation.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <Paperclip-Paperclip@users.noreply.github.com>
2026-11-07 10:26:08 -06:00
DottaandPaperclip f2d8a51027 fix(ui): show skill descriptions and preserve composer line height (#19502)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - People write requests in the new-task and task-page composers.
> - Both composers use the shared Markdown editor for skill selection.
> - Skill names alone do not explain which skill to use.
> - Inline skill pills can increase the height of a text line.
> - This pull request shows descriptions in a full-width menu and gives
inline skills a color and icon.
> - People can choose a skill and read the request with normal line
spacing.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

Skill selection and inline skill display in the new-task and task-page
composers.

**Subsystem affected**

ui/ — shared Markdown editor and task composer stories.

**Current behavior**

The slash menu shows skill names in a narrow popup. Inline skills use
pill styles that can make a text line too tall.

**Proposed behavior**

Match the slash menu to the active composer width. Put each skill name
and description on one row. Truncate the description with an ellipsis.
Show each inline skill in purple with the Boxes icon and normal text
line height.

**Reason and benefit**

People can compare skills before selection. Inline skills remain easy to
find without changing paragraph spacing.

**Breaking changes**

None. Skill links retain the same Markdown labels and references.
Mention selection and task submission retain their existing behavior.

**Additional context**

Related: #19272 changes the submission key in these composers. The
browser cases here check skill links after task creation and reply
submission. Searches found no duplicate change for this display issue.
This is a small improvement to the shipped skills UI in ROADMAP.md.

## What Changed

- Align the slash menu with the active composer. Observe composer size
changes and keep the menu inside the viewport.
- Show each skill name and description on one row. Keep skills without
descriptions available.
- Use theme tokens for purple skill text and the Boxes icon. Preserve
the surrounding text line height. Include both prefixed and standard
mask declarations, consistent with the other icon rules.
- Add a composer anchor to the task-page surface. Keep the shared editor
responsible for menu placement and skill display.
- Increase the contrast of menu details and the task composer model
label in the covered stories.
- Add task-page stories and extend new-task stories. Cover desktop,
mobile, light and dark themes, keyboard insertion, and Markdown
submission.
- Add 18 browser cases for menu size and alignment, description
truncation, inline line height, and submitted skill references.
- Use the production mobile context in task-page stories. Check mobile
text size, long skill names without descriptions, and the saved new-task
skill URL.

## Verification

- Before the compatibility correction,
`a6371219ad1ce0ba908b63b55b7999e4e683cace`: all 130 focused editor,
mention, destructive-theme, and avatar tests pass across six files. No
existing test was changed.
- Current head `c7091dd5d34897f82b752bf93f94b1026cc7a82f` adds the
prefixed mask declaration requested by review. All 12 affected
theme/avatar tests pass. A fresh Storybook production build and all 18
composer browser cases pass on the identical source tree, with two
workers, traces enabled, and no retries. Token gates and whitespace
checks pass.
- Browser cases check both composers, desktop/mobile text size,
light/dark themes, popup width and alignment after resizing, single-row
ellipsis, names without descriptions, inline paragraph height, and
submitted skill links.
- The preceding head has a fully successful CI run (`42260195469`,
attempt 2). Current-head Greptile is 5/5 and its check completed
successfully. All four review threads are resolved. Current-head CI
[42270466448](https://github.com/paperclipai/paperclip/actions/runs/42270466448)
passed, including typecheck, build, all general and serialized test
shards, all Runner checks, and all eight end-to-end shards. The
aggregate verify gate passed.
- Screenshots for both composers were reviewed and accepted before this
PR was opened. Stories use production components with fixture APIs and
submissions. They do not prove backend persistence or agent execution.
Mobile uses a resized Chromium viewport with production mobile props.
The visual baseline is unpublished, so captures support inspection, not
pixel comparisons.
- CI run `42254435625` on `9193b47d` failed two existing
destructive-theme checks because the new skill-only theme blocks came
before the main blocks. The failure reproduced locally. Moving the
unchanged declarations into the main blocks made both checks pass.
- CI run `42258164662` on `7fab621f` caught one more existing CSS-reader
contract: the avatar test expects `--background` first in `.dark`. That
failure also reproduced locally. The final source puts the four skill
declarations at the ends of the existing blocks. A mechanical comparison
proves every pre-existing stylesheet line and all four skill declaration
values are identical.
- The first run also failed an unchanged Runner credential test during
its first lease acquisition. All 42 tests in that file pass in a local
diagnostic. The original CI cause remains unknown; no Runner fix is
claimed. The second run's Runner package job was cancelled by the
corrective push, so it supplies no passing verdict. The final-head
Runner package check passed, which shows recovery without explaining the
original lease error.
- One `7fab621f` mobile play function failed during keyboard insertion
(17 browser cases passed, one failed). Its saved DOM contains the skill
and an open link dialog without the trailing sentence. The cause remains
unknown. Three focused traced executions, a bounded frame-delay
diagnostic, and a later full 18-case run passed without source changes.
No reliability fix is claimed. The original failure remains recorded
separately from the final-head 18-case pass.
- Final-head CI run `42260195469`, attempt 1, failed one unchanged
runtime startup test in `idle-completed-history.test.ts`. Its HTTP
readiness check received connection refusals for 10 seconds. The same
test passed in 2.35 seconds in the preceding CI run; the only
intervening source change moved four CSS declarations. A focused local
run passed the case (one passed, 74 filtered out). A full-file local
diagnostic was interrupted after about four minutes without a verdict;
it is not counted as a pass. The startup failure cause remains unknown.
No runtime source or test was changed. Failed CI jobs were rerun once on
the same head; attempt 2 passed. This shows recovery, without explaining
the first startup failure.
- The broad local typecheck could not complete because Cargo is absent.
The affected-test selector requests broad CI for the browser test.
Required CI supplies broad typecheck, test, and build coverage. Linux
local checks used Node 25.9.0, pnpm 9.15.4, and Playwright Chromium 151.
- Earlier evidence retains a Storybook build interrupted by SIGTERM, a
dev-server play error during HMR, and a browser attempt that could not
launch before Chromium installation. Later successful runs do not
explain the earlier interruptions.

## Risks

The shared editor also serves other mention types. Their existing
placement path stays in place. Slash menu placement must stay aligned
during viewport and composer size changes. The browser cases cover both.
The mask correction was checked in Chromium; older WebKit was not
executed. Mobile cases use a resized Chromium viewport. They do not test
a physical phone or its virtual keyboard.

## Model Used

OpenAI GPT-6 through Codex, with reasoning, tool use, and code
execution. The runtime does not expose a more specific model ID or
context-window size.

## 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-07 08:26:19 -06:00
DottaandPaperclip 27170dca49 fix(workspaces): preserve the read-only capture phase budget (#19803)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Remote workspace saves must preserve the files produced by a
completed turn.
> - When a sandbox disk is full, the existing fallback transfers a
stable source and seals it on the controller.
> - The fallback gives metadata checks and the portable codec the same
two-minute timer.
> - Slow but successful metadata checks can leave the codec only
milliseconds, although normal sandbox capture allows five minutes.
> - This change keeps the metadata budget and gives the local codec the
existing finite capture budget.
> - All source checks, archive checks, ownership checks, and failure
cleanup remain required.

## Linked Issues or Issue Description

Refs #19465. Related: #19364 and #19619. The open diagnostic change in
#19619 records fallback causes; it does not separate these phase
budgets. No duplicate budget fix was found.

**What happened?**

A retained workspace failed five read-only exports at `stage=archive`
with `transport_timeout`; four warnings also named `ABORT_ERR`. The
fallback had already transferred the source and passed the received-tree
and source inventory checks. Its controller codec received the same
two-minute signal used by those checks.

**Expected behavior**

Metadata checks share a finite two-minute budget. Sealing uses the same
finite five-minute policy as normal sandbox capture. The final source
check uses only the unused metadata budget. A changed source, expired
budget, or cancelled codec must not produce a preservation receipt.

**Steps to reproduce**

1. Create a standalone Git fixture with staged changes, an untracked
result, and a confined symlink.
2. Make the initial sandbox helper report the exact disk-full error.
3. Transfer real files and run the actual inventory scripts. Delay a
successful second inventory reply until just before the native
two-minute deadline.
4. Observe that the real portable codec starts with a live signal, then
aborts when that shared timer expires.

## What Changed

- Name and reuse the existing 300-second command policy for controller
capture.
- Retain the aggregate 120-second metadata budget. Preserve its unused
remainder for the final source recheck.
- Keep the exact disk-full error, source comparisons, file/Git checks,
and cleanup behavior.
- Add six real-filesystem and real-codec controls. Document the separate
phase limits and the current absence of a caller abort signal on the
save interface.
- Repair the existing onboarding test's fake-environment mapping with
the same real-agent PATCH mapping used by its sibling. Verify the
persisted agent through a real GET. Production UI code is unchanged.

## Verification

- Native-timer RED on base `35d41f11bb`: a successful source reply
arrived at 119,981.62 ms. Its command took 119,854.47 ms within its
119,873 ms cap. The actual codec started at 119,982.70 ms with a live
signal and aborted at 120,001.84 ms. Its nested cause matched the
retained warning: `stage=archive reason=operation_failed
transport=transport_timeout code=ABORT_ERR`. The source inventory was
unchanged and private staging was removed.
- The real-archive control sealed 10,166,395 bytes and 533 entries in
555.55 ms. It restored HEAD, staged data, untracked data, and the
symlink, and passed Git fsck. The recorded delay is synthetic; it is not
a measurement of the failed hosted workspace or provider throughput.
- The native RED leaves about 18 ms for the final check too. The
repaired code must still refuse that exact exhausted-metadata case; it
is not claimed as a native-clock GREEN.
- The committed fast regressions control phase time and delegate to the
actual inventory commands and portable codec. They cover successful
restore after the old metadata signal expires during sealing,
pre-capture metadata expiry, codec deadline/cancellation, and final
metadata expiry after a real completed archive.
- From `server/`: `node ../node_modules/vitest/vitest.mjs run
src/services/remote-workspace-readonly-capture-budget.test.ts
src/services/remote-workspace-transport.test.ts
src/services/remote-workspace-readonly-inventory.test.ts
src/services/remote-workspace-transfer-timeout.test.ts --fsModuleCache`
— 156 tests passed in 42.30 seconds before removal of a redundant test
duration assertion. The final-head six-case regression also passes after
rebasing onto `fff70fe49e`; only unrelated Runner and E2E files changed
in that rebase.
- Review follow-up `ad565dd984` makes the phase clock fully controlled.
It checks the requested final 2,000 ms budget separately from the real
subprocess's 30-second test limit. All six controls pass in 11.94
seconds, and server TypeScript passes again. The earlier
real-time-plus-offset clock could falsely fail on a loaded machine; that
review finding is fixed.
- Server TypeScript and module boundary checks pass. The first
TypeScript attempt could not resolve the unbuilt plugin SDK. Building
that existing workspace prerequisite resolved the setup errors; no
source workaround was added.
- The first preflight attempt used the host pnpm shim and was stopped.
The second lacked sandbox network access and was stopped. The canonical
cached preflight then completed in 22.1 seconds with the Runner build
restored from cache. A later test launch used the wrong working
directory for a test rewrite; its wrapper was stopped before waiting
through a second native-timer test. None of these interrupted attempts
is a pass.
- Final-head CI first failed one unchanged Runner-support privacy test
at 30 seconds: [job
failure](https://github.com/paperclipai/paperclip/actions/runs/42359560195/job/128365192635).
The other 6,864 tests passed and four were skipped. The failing test and
its report owners match base `fff70fe49e`; the exact case passes locally
unchanged in 2.97 seconds (one pass, 40 unselected). Job cleanup
reported an orphan `unzip`; that does not establish the timeout cause.
GitHub rejected the scoped retry while the parent workflow was still
running, so no retry has started. No old-head retry was started because
the separately proven onboarding fixture repair requires a new head and
a fresh full pipeline. The original Runner failure remains recorded.
- The same CI run failed onboarding case 160 at 60 seconds; its retained
page showed `Environment not found`. The fixture injected an environment
only in browser responses, then sent that fake ID to the real agent
update. The original case passed locally in 37.44 seconds despite
rejected HTTP 422 updates. Adding a real update-success guard produced a
local RED in 26.83 seconds (expected 200, got 422). Reusing the sibling
fixture's fake-ID-to-local-default PATCH mapping produced GREEN in 22.52
seconds. A real GET verifies the same persisted agent ID, name, and
default environment. Existing sign-in, cancellation, and no-hire
assertions are unchanged. CI screenshot/error context and the local
persistence RED remain retained.
- All other product checks on `ad565dd984` passed, including build,
TypeScript, all general and serialized server shards, and native Runner
checks. Final head `58bb4ee2411dfefca462f0e9b5a701d84ce1326e` passes the
fresh pipeline: [CI
run](https://github.com/paperclipai/paperclip/actions/runs/42360927780).
Runner support passes in 3m36s and onboarding shard 5/8 passes in 5m46s,
with no Runner-support source change. The original failures remain part
of this verification history.
- Environment: macOS arm64, Node 26.4.0, real Node/Git/filesystem
operations. No provider or recovery calls, replayed turn, source
mutation, or hosted recovery acceptance was performed. Final-head
required CI and review pass; the exact gate state is recorded below.

- Exact-head final gate: 57 successful checks, three skipped, two
neutral, and no failures. Both required gates (`ci / verify` and `ci /
e2e`) pass. Greptile reviewed this exact head at 5/5, and zero review
threads remain open. The non-required companion `review` job is still
queued with no steps; it has not passed, and GitHub therefore reports
`UNSTABLE` while the PR remains mergeable.

## Risks

- Controller work after transfer can now take up to two minutes for
metadata plus five minutes for sealing. Each phase remains bounded.
Existing entry, repository, configuration, and transfer limits stay in
force.
- The save interface has no caller abort signal. This change adds no
Stop guarantee and does not replace the recovery retry window with an
operation deadline. Existing physical ownership and publication fences
remain in force.
- This proves the timer mechanism with a synthetic slow inventory. It
does not measure the actual dirty workspace size or show that its
archive finishes within five minutes. A guarded file-only recovery and
independent durable readback remain required after normal delivery.
- The onboarding fixture maps only its exact synthetic environment ID.
It still calls the real agent update and read routes. It does not loosen
production environment validation.
- No schema, provider transport, task status, recovery policy, or source
cleanup authority changes.

## Model Used

OpenAI GPT-6 family through Codex, with reasoning, repository tools,
code execution, and regression tests. The exact serving model ID and
context window are not exposed in this session.

## 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 (both required gates and all
product checks pass; non-required companion review remains queued as
stated above)
- [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-07 03:58:24 -06:00
DottaandClaude Opus 5.5 6fb6220d96 fix(restart-chaos): rebuild the Daytona plugin when its sources change (#19798)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness runs Daytona waves with a local control
plane that installs the bundled Daytona sandbox-provider plugin from
`packages/plugins/sandbox-providers/daytona`
> - That plugin is excluded from the pnpm workspace, so workspace builds
and `scripts/agent-preflight.mjs` never rebuild it
> - The harness built it only when `dist/` was missing. A long-lived
chaos worktree that moved across many master commits kept serving the
plugin it built on day one
> - As a result, plugin-side fixes such as #19696 (reclaim sandboxes
left by a restart during acquire) and #19754 (keep a runner sandbox
alive through the reconnect grace) were not under test in those waves,
while server and runner fixes were
> - This pull request keys the plugin build on a content hash of its
inputs and rebuilds when the hash changes
> - The benefit is that every Daytona wave tests the plugin of the
commit it claims to test

## Linked Issues or Issue Description

**Bug description**
`ensureDaytonaPlugin()` in `tests/restart-chaos/main.ts` returned early
whenever `dist/manifest.js`, `dist/worker.js` and the Daytona SDK
existed.

**Steps to reproduce**
Build the plugin on commit A, check out a later commit B that changes
`packages/plugins/sandbox-providers/daytona/src`, and run a `--env
daytona` wave. The wave installs the commit-A plugin. In one chaos
worktree, `dist/plugin.js` was 20 hours older than `src/plugin.ts` and
lacked `runnerReconnectGraceMs` from #19754.

**Expected behavior**
The harness rebuilds the plugin whenever its sources, lockfile or the
plugin SDK source changed since the last build.

Refs #19544

## What Changed

- `tests/restart-chaos/plugin-build.ts` (new): `pluginBuildInputHash`
hashes the plugin's `package.json`, `pnpm-lock.yaml`, `tsconfig.json`
and `src/**`, plus extra inputs: the plugin SDK `src`, `package.json`
and `tsconfig.json`, and the root `tsconfig.json` and
`tsconfig.base.json` that both packages extend. It excludes
`node_modules` and `dist`. `pluginBuildState` compares that hash with a
stamp in `dist/.restart-chaos-build-input.sha256` and reports a reason.
- `tests/restart-chaos/main.ts`: `ensureDaytonaPlugin` installs, links
the SDK and builds when the state is stale, then writes the stamp. It
records `daytona_plugin_build_state` and `daytona_plugin_built` in the
timeline.
- `tests/restart-chaos/plugin-build.test.ts` (new): an unstamped build
is stale; the stamped build is up to date; a changed source, lockfile or
SDK source is stale; changes to `node_modules` and `dist` are ignored;
missing outputs or dependencies are stale.

Other out-of-workspace packages, checked as requested:
- `pnpm-workspace.yaml` excludes only
`packages/plugins/sandbox-providers/**`.
- The restart chaos local mode (`--env local`) does not install a
sandbox provider.
- CI's runner-e2e "Prepare bundled Daytona plugin" step runs on a fresh
checkout per job, so it cannot see a stale `dist/`.
- The other sandbox providers are not used by these harnesses. The risk
is therefore limited to long-lived local worktrees running Daytona
waves, which this change covers.

## Verification

- `vitest run --config tests/restart-chaos/vitest.config.ts`: 121/121
pass, including the new staleness tests.
- `tsc -p tests/restart-chaos/tsconfig.json`: no errors in the changed
files.
- Manual: in the chaos worktree, `dist/plugin.js` (built 2026-11-06
11:16) lacked `runnerReconnectGraceMs`. After a rebuild at master
35d41f11bb it contains it.

## Risks

- This change touches the test harness only.
- The first wave after this change rebuilds the plugin once in each
worktree, because existing builds have no stamp. That takes a few
seconds.

> 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, in Claude Code with
tool use (shell, file edits, GitHub CLI). Default context window,
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 Opus 5.5 <noreply@anthropic.com>
2026-11-07 03:44:25 -06:00
DottaandClaude Opus 5.5 9cc3760552 test(runner-e2e): allow controller outages up to ten minutes (#19797)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness and the runner-e2e recover-controller
cells stop the control plane with `replaceController`
(`tests/runner-e2e/controller-outage.ts`), hold it down, then start it
again
> - `parseControllerOutage` refuses holds longer than 300000 ms
> - Restart chaos needs longer holds. One example is proving that a
Daytona sandbox survives an outage longer than its old 5-minute
auto-stop but inside the 7-minute runner reconnect grace (RC-54)
> - With the cap, such a fault errors before the controller stops, so
the scenario cannot run
> - This pull request raises the test-harness cap to 600000 ms
> - The benefit is that chaos runs can now inject 5–10 minute outages;
production code and qualification contracts are unchanged

## Linked Issues or Issue Description

**Bug description**
A restart chaos replay with a 360000 ms K4 hold failed with "Controller
outage requires SIGTERM/SIGKILL and 0..300000 integer milliseconds". The
controller was never stopped.

**Expected behavior**
The test harness accepts outages of up to 10 minutes. The
recover-controller receipt check keeps its own readiness limit
(`readyAfterMs < 420000`).

Refs #19544

## What Changed

- `tests/runner-e2e/controller-outage.ts`: adds
`MAX_CONTROLLER_OUTAGE_MS = 600_000` and uses it in
`parseControllerOutage` and its error message.
`validControllerOutageReceipt` is unchanged and still requires readiness
inside the 7-minute grace.
- `tests/runner-e2e/controller-outage.test.ts`: the rejection cases now
use 600001. A new test accepts 360000 (SIGKILL) and 600000 (SIGTERM).
The existing five-minute contract test is unchanged.

## Verification

- `vitest run tests/runner-e2e/controller-outage.test.ts
tests/runner-e2e/server-restart-request.test.ts`: 52/52 pass.
- `server-restart-request.test.ts` now uses 600001 as its out-of-range
`controllerOutage`. The separate restart-request `controllerDowntimeMs`
limit (five minutes) is unchanged.

## Risks

- This change is test-only. No product or server code changes.
- A suite that sets `PAPERCLIP_RUNNER_E2E_CONTROLLER_OUTAGE_MS` above
300000 now runs instead of being refused. The recover-controller receipt
check still fails any receipt whose readiness is outside the grace.

> 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, in Claude Code with
tool use (shell, file edits, GitHub CLI). Default context window,
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 Opus 5.5 <noreply@anthropic.com>
2026-11-07 03:11:10 -06:00
github-actions[bot]andlockfile-bot e95dcadbd2 chore(lockfile): refresh pnpm lockfiles (#19773)
## Thinking Path

> - Paperclip uses a dependency lock for reproducible builds.
> - Dependency changes on master need matching lock entries.
> - Paid eval reports also pin a reviewed offline correction.
> - Reports must install before paid model work starts.
> - This automated PR keeps the lock and report proof together.

## Linked Issues or Issue Description

**What happened?**

Dependency manifests on master changed after the last committed lock
refresh.

**Expected behavior**

Frozen installs must match those manifests. Eval reports must retain
matching offline lock proof.

**Steps to reproduce**

Run a frozen install before and after this lock refresh.

## What Changed

- Refresh the dependency lock from the manifests on master.
- Refresh the sandbox provider lockfiles from their manifests on master.
- Include the matching offline report correction and hashes.

## Verification

- Pinned pnpm resolution completed with lifecycle scripts disabled.
- The report proof refresh accepted the reviewed committed baseline.
- The workflow rejected changes outside the lock and report proof files.

## Risks

The lockfile bot merges this PR without CI or review when every added
package version is at least 3 days old on npm. Otherwise the PR waits
for a maintainer, and the daily refresh tries again. The bot merges only
the exact commit this workflow generated from manifests already reviewed
on master. Unknown report lock baselines remain blocked. This update
changes no permissions or paid enable gates.

## Model Used

None — generated by pnpm and the deterministic report-lock updater.

## Checklist

- [x] Thinking path and problem description included
- [x] Generated lock and matching report proof included
- [x] Release-age gate decides between bot merge and maintainer review

Co-authored-by: lockfile-bot <lockfile-bot@users.noreply.github.com>
2026-11-07 07:14:22 +00:00
96bc021edb fix(server): certify settled legacy processes for future retries (#19377)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Bounded retries must preserve current authority, budgets and
deadlines.
> - The tenant can wait for supported future work after the source
process and its cleanup stop.
> - Retained process identity alone cannot prove that cleanup finished.
> - This change records host-owned settlement and checks its exact
identity before a retry can cover its wait.
> - Uncertain process state, changed authority, live claims and cleanup
debt keep the tenant awake.

## Linked Issues or Issue Description

Refs #19001. Related dependencies: #19019 and #18988. Related #19340
covers explicit Native task Retry; this change covers future Legacy
process retries.

Current integrated source: `1645970a9322d1213ba25fc7b52366c285682977`,
normally merged with actual master
`131b566ca4fa866c4348739c75cfe48c805b3398`. The prerequisites above are
merged. This PR targets `master`.

**What happened?**

A future issue, company or review retry can retain its failed source
process identity. The deadline reader could not prove that host cleanup
finished. Supported waits stayed blocked. Ordinary issue retries also
needed an exact source-settlement check, and represented coalesced
requests needed exact input coverage.

**Expected behavior**

A supported future retry covers only its existing wait, under current
company and actor authority, after exact host settlement. Missing,
malformed, stale, cancelled, Native or unfinished source state stays
blocked. No retry gains new execution authority.

**Steps to reproduce**

Fail an ordinary process or fail environment preparation before adapter
entry. Let normal cleanup schedule the existing retry. Inspect its
settlement and deadline, stop and reconstruct the owner, and admit one
useful successor at the saved deadline. Hold cleanup or change authority
to verify that the reader refuses coverage.

## What Changed

- Add nullable internal `legacyProcessSettlement` state through normally
generated migration `0439_cultured_impossible_man`. No backfill or
adapter result creates the record.
- Write exact host settlement after stopped-process and cleanup checks.
Compare the current run and reject live claims or cleanup debt. Optional
persistence failure preserves normal cleanup and keeps sleep coverage
blocked.
- Add a separate preparation receipt for a claimed Legacy run that
failed before any adapter or Native dispatch. Require the same boot, run
times, preparing stage, released claim and completed cleanup. A null PID
alone proves nothing.
- Clear settlement on a new claim, process metadata or Native selection.
Omit the internal record from public serialized data.
- Check exact source lineage and current authority for ordinary issue,
company and taskless retries. Cover completed coalesced requests only
when the admitted retry represents their exact inputs.
- Add the closed `current_review_retry` wait with current reviewer,
stage and existing issue authority checks. Require a nonempty current
stage ID.
- Add actual producer, cold reconstruction, SQL negative, redaction and
optional-persistence controls.
- Repair CI fixture expectations for the additive column, startup mock,
budget owner and popup return URL. Bind the controlled Native comment
server explicitly to loopback.
- Replace temporary Slack clock logging with an exact persisted-deadline
assertion. Wait until that real deadline before the existing single
default poll. Keep both forced polls, business assertions and timing
limits.

## Verification

- On final integrated source `1645970a`, normal Shared/SDK
prerequisites, Runner TypeScript and the entire normal App layered
typecheck pass. Tracked source and lockfile remain unchanged.
- All 26 whole-file settlement, recovery-reader and bootstrap controls
pass. Seven separate real PostgreSQL producer/negative cases pass at
`cf16c6a`; each has one selected passing case and 18 filtered
registrations. Filtered registrations are not acceptance.
- Master then changed the preparation source-lock scope. Its three
intersecting cases were rerun on final `1645970a`: an actual failed
company process, an actual failed ordinary issue process, and held
preparation cleanup. Each preserves its source identity and original
deadline and admits one useful successor. Issue and preparation cases
exercise normal automatic retry; the company case uses the normal
explicit bounded-retry producer.
- The unchanged 26 controls and four other PostgreSQL cases retain their
`cf16c6a` proof after a scoped source comparison. Optional
settlement/preparation persistence failures keep certification blocked
while normal cleanup completes. An entered adapter without process
identity stays uncertified. This is scoped carry, not a claim that every
earlier case ran again on the final head.
- All recorded test owners and controlled groups closed. Final read-only
SQL audits returned no test sessions. One earlier postcase resource
barrier observed a transient unattributed database backend; that
database and original evidence remain preserved. Its later disappearance
is recovery, not an explained cause. No assertion, deadline or test body
was relaxed.
- Migration0439 was generated normally after master history. All
executable SQL and journal history are preserved; the five retained
snapshots are0435–0439. Every Legacy-owned patch and unrelated
actual-master blob/mode is preserved.
- Published exact head `1645970a9322d1213ba25fc7b52366c285682977` passes
all current required checks: 68 successful contexts and eight expected
skips after exhaustive pagination and superseded-run accounting. The
fresh complete Greptile review scores this exact head 5/5. All four
inline review threads are resolved. The latest Ponytail turn stopped
without a verdict; one normal retry was requested, with no active turn
or new finding observed. No old-head score is used. The original
old-base policy failure, its dependent aggregate failure, and the
corrected description-gate failure remain recorded. No full local
repository suite is claimed; remote typecheck, test and build gates
pass.

To repeat the real database coverage, set both
`PAPERCLIP_RECOVERY_DEADLINES_TEST_DATABASE_URL` and
`PAPERCLIP_RECOVERY_DEADLINES_TEST_ADMIN_DATABASE_URL` to the same fresh
dedicated test database and run the selected
`recovery-deadline-acceptance.test.ts` cases with one worker. An unset
URL, skipped case or filtered registration is not acceptance.

The fixtures use controlled local processes and real PostgreSQL.
Reconstruction creates new service owners inside the same test process.
They do not prove deployed Cloud alarms, physical host/database
suspension, new-host recovery or Native capacity settlement. The
original30-second retry remains below the Cloud270-second minimum wake
lead.

The merge projection also includes actual master
`c3180398faca815fed7a62674bd4b0c4e88884ae`, whose common completion path
now sends deferred/recovery notifications. Its unpublished normal merge
tree is `16b028d57150f8caa576e913f38343a365b542dd`. Normal Shared/SDK,
Runner TypeScript and the full App graph pass on that combined source.
One unchanged ordinary-issue acceptance case was rerun there: an actual
failed process retains the original automatic retry deadline, admits one
useful successor, and passes the settlement, authority and no-duplicate
assertions. It passed in 45.055 s; 18 filtered registrations are not
acceptance. All known owners and groups closed, and the independent
final database audit found zero remaining test sessions. The successful
database was removed normally. Recorded worker socket peak is
unavailable for this run; parent-process zero values are not used as
worker measurements. All 29 Legacy-owned paths and every unrelated
master blob and mode are preserved.

Two outside-diff review observations concern existing waiting-chat
compaction and cross-host Native result-lease recovery. Their files are
byte-identical to current master. This focused Legacy change does not
fix or qualify those separate paths.

## Risks

Settlement proves exact local closure. It does not cover live provider
work, Native ownership, cancellation, costs, claims, cleanup debt or
changed authority. Independent checks remain.

Ship normal migration0439 with the reader and writer. Preserve
executable history and the newest five snapshots0435–0439. Generate the
MCP child's next migration normally after this parent.

The prerequisites are merged; the final remote checks must use this
integrated exact head. This proof uses controlled local processes and
external PostgreSQL. It does not establish deployed Cloud alarm
acceptance, physical host or database suspension, cold host recovery, or
R5 Native capacity settlement. The30-second retry is below the Cloud
minimum wake lead.

This focused repair follows the roadmap's bounded recovery and Cloud
deployment work. It adds no new provider policy.

## Model Used

OpenAI `gpt-6.1-sol` assisted with readers, fixtures, source review and
verification through Codex. A coordinating GPT-6-family agent
implemented and reviewed shared lifecycle wiring and integration. Tools
included Git, native TypeScript checks and controlled tests. Exact
served revisions and context limits were not exposed.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used and its available capability
details
- [x] I have checked ROADMAP.md and related work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have linked existing public work and described the issue
- [x] I have not included internal issue IDs or private links
- [x] My branch name describes the change
- [x] Required focused checks pass at their explicitly recorded
source/carry scopes above
- [x] I have added or updated meaningful tests
- [x] I have recorded the internal contract and migration requirements
- [x] I have documented risks and evidence limits
- [x] All required published exact-head Paperclip CI gates are green
- [x] Published exact-head Greptile is 5/5 with no open findings in this
focused diff; separate unchanged master observations are identified
above
- [x] I will address reviewer findings before requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Codex <noreply@openai.com>
2026-11-07 00:56:04 -06:00