Commit Graph
7558 Commits
Author SHA1 Message Date
DottaandClaude Fable 5.2 b01959cb15 fix(recovery): release a handed-off run's saved-files hold for the person it handed the task to (#20941)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - An agent can hand its own task to a person for review. With #20884,
a native run that does this keeps running and finishes its turn.
> - A restart can land between that hand-off and the end of the same
turn. Restart recovery then settles the run as failed and keeps its
unsaved sandbox files, so no work is lost.
> - Automatic workspace recovery saves those files. But the
workspace-repair hold then stayed on the person's task, because the hold
only released for a newer agent owner.
> - This pull request releases that hold for the person who received the
task through the stopped run's own recorded hand-off, once the files are
saved.
> - The benefit is that the person's task rests in review with no stale
"Saved work recovered" hold, and the agent's files are still saved
first.

## Linked Issues or Issue Description

Follow-up to #20884 (merged). It was reviewed there as a known limit:
"Restart blocks the person's task". It uses the
`resultJson.humanSelfHandoff` record that #20884 added. Related: #19184,
which released the same hold for a newer agent owner.

**What happened?**

A native run handed its own task to a person, and a restart landed
before the run finished its turn. Restart recovery settled the run and
retained its files. After automatic recovery saved them, the task still
had a workspace-repair hold ("Saved work recovered. Send a message to
continue."). The person had nothing to do with that run's work, and
nothing released the hold.

**Expected behavior**

The hold protects the workspace until the files are saved. After that,
the task rests with the person and has no execution blocker.

**Steps to reproduce**

1. A native run records a hand-off to a person, and the task is now
`in_review` with that person.
2. A restart settles the run as failed, with its prepared remote
workspace retained.
3. Automatic workspace recovery completes.
4. Before this fix, `getExecutionBlocker` still returns the run's
workspace-repair hold.

## What Changed

- `server/src/services/execution-blocker.ts`:
`restoreHoldReleasedForCurrentOwnerPredicate` now also releases a
recovered file-preservation hold in one more case. The task must be
assigned to a person, and the stopped run's
`resultJson.humanSelfHandoff` must name that task and that person. The
existing proof is still required: the completed-recovery receipt bound
to the action, and a copyback rather than a snapshot-only save. Until
the files are saved, the hold stays.
- `server/src/services/workspace-recovery-admission.test.ts`: two
embedded-Postgres tests.
- The hold blocks the person's task before recovery and releases after
it.
  - A hand-off recorded for a different person keeps the hold.
- `doc/execution-semantics.md`: one sentence on the restart case.

## Verification

- Red: with the `execution-blocker.ts` change removed, `releases a
handed-off run's recovered files hold for the person it handed the task
to` fails. `getExecutionBlocker` still returns the hold after
`recoverRetainedWorkspace`.
- Green: `cd server && npx vitest run
src/services/workspace-recovery-admission.test.ts
src/__tests__/interrupted-preparation-hold-settle.test.ts
src/services/explicit-native-continuation.test.ts
src/services/native-runtime/native-human-self-handoff.test.ts`. All 617
tests pass. The server typecheck passes.
- Chaos: the H2 workload from #20884 covers the hand-off with a K1
restart after it. Locally the runs had no unsaved files, so no hold
appeared. This PR's case needs a remote sandbox with unsaved files. **No
Daytona wave was run.**

## Risks

- The release applies only when the stopped run's own record names the
current person and task, and only after the completed-recovery receipt.
Other holds, snapshot-only saves and unsafe archives are unchanged.
- No schema change. A run without the record behaves as before.
- This branch is rebased onto master after #20884 merged. The only
conflict was an adjacent import line.

> 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 (Anthropic), model ID `claude-opus-5-5`, through Claude Code
with tool use and code execution. The session does not expose the
context window or reasoning mode.

## 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 Fable 5.2 <noreply@anthropic.com>
2026-11-11 15:52:35 -06:00
DottaandClaude 0bf3236bc8 test(restart-chaos): staging class R cells CR1-CR3 and sandbox capture on every Daytona wave (#20909)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart-chaos harness (`tests/restart-chaos/`) reproduces
staging failures before users meet them
> - Staging fails with `runner_remote_process_identity_unavailable` when
the remote Runner or its provider is gone after a deploy, a stall or a
re-wake, and nobody has seen the sandbox side
> - The harness had no cell for a provider-only death or a plugin RPC
stall, and its sandbox capture ran only on K15 waves
> - This pull request adds the staging class R cells CR1–CR3, and
captures the sandbox side on every Daytona wave
> - The benefit is that a wave that reproduces the refusal also keeps
the evidence of why the Runner or provider died

## Linked Issues or Issue Description

**What is the problem?**
The harness cannot answer why a remote runnerd or its provider dies. Its
capture ran only around K15 restarts. It recorded no process start
times, sockets or provider log files. It had no fault that kills only
the provider or stalls a plugin RPC.

**What should change?**
Add cells for each staging shape, and capture the sandbox before, during
and after each one, on every Daytona wave.

**Context**
Refs #20907 (keeps runnerd's stderr in the sandbox, and reports it on
failed recovery). Refs #20893 (W2 re-wake cells).

## What Changed

- `class-r.ts` (new) adds three faults, selected with `--faults`:
- **CR1:** a K15 redeploy with a held outage. `--cr1-hold-seconds`,
0–600, default 360.
- **CR2:** SIGKILL of only the provider process under the run's
`paperclip-runnerd`, inside its sandbox. Runnerd stays alive. By default
the controller stays up; `--cr2-hold-seconds` adds a K15 outage.
- **CR3:** SIGSTOP of the server's Daytona plugin worker.
`--cr3-stall-seconds`, 41–600, default 45.
  
  Each CR task must pass the `class_r_fault_applied` check.
- `daytona-runner-logs.ts` adds `CLASS_R_PROBE`. It reads process start
times and `wchan`, sockets, the runtime file listing, and the tails of
the Runner's and the provider's state and log files. It also adds
`executeInWaveSandboxes`, the fixed-script runner that CR2 uses.
- `main.ts` and `supervisor.ts`:
- every Daytona wave captures with the class R probe when a task rests
and at wave end;
- CR cells also capture before the fault, after the stop, before the
restart, and every 15 s for 180 s after ready;
  - `--sandbox-watch-seconds` captures on a clock for the whole wave.
- The README has a new "Staging class R cells" section.

## Verification

- `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts`:
49 files, 600 tests pass, including the new `class-r.test.ts`. It covers
option parsing, planning, the kill command's quoting and its refusal of
anything but a run id, plugin worker selection, the watch schedule and
the applied check.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos` passes.
- Daytona waves run from this branch plus #20907. The results are in the
follow-up comment.

## Risks

- This change is limited to the harness. CR2 is its only in-sandbox
mutation: a SIGKILL of the provider, run by a fixed script only in
sandboxes labelled with the wave's company and environment. The script
never signals runnerd.
- More captures add Daytona toolbox calls on every Daytona wave. Each
capture has a 45 s deadline, and a failed capture is recorded and never
fails the wave.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-11-11 15:50:59 -06:00
DottaandClaude Opus 5.5 48d23d1530 fix(runner): keep a connected remote Runner through a stalled plugin supervision probe (#20940)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A remote run starts `paperclip-runnerd` in a sandbox. The controller
monitors the process with a health probe that goes through the
environment plugin (`environmentExecute`).
> - A stall of the Daytona plugin worker makes that probe RPC time out
after 40 seconds. The monitor treated the timeout as "process
supervision failed".
> - The transport then failed closed with
`runner_process_termination_unconfirmed`. The run ended
`notification_transport_failed`, `recoverable: false`, and the close
path suspended a healthy Runner. The task rested blocked with Retry.
> - Nothing was wrong with the sandbox, the Runner or the provider. The
Runner held its own authenticated connection to the controller during
the full stall.
> - This pull request makes a failed or timed-out probe "unknown". While
the Runner stays connected on its own channel, the monitor keeps the
same runner and turn and probes again, for a bounded time.
> - The benefit is that a short plugin stall no longer kills a healthy
run. The fail-closed cases stay the same.

## Linked Issues or Issue Description

Refs #20909 (chaos cell CR3, which reproduces this failure)

**What happened?**
A 45-second stall of the Daytona plugin worker killed a healthy run.
Chaos cell CR3 reproduced it 2 of 2 times (OpenCode and Claude,
Daytona). A staging run had the same failure with no deploy: a plugin
`environmentExecute` RPC stalled for more than 40 seconds. The run
failed with "Notification transport failed closed:
runner_process_termination_unconfirmed: runnerd process supervision
failed: RPC call \"environmentExecute\" timed out after 40000ms". The
close path then sent `runner.suspend`, and the healthy Runner suspended
and exited. The task rested blocked with Retry.

**Expected behavior**
A probe that times out or fails in transport is not proof that the
process stopped. While the Runner stays connected on its own channel,
the run continues. If supervision cannot come back within a bound, the
run rests with a working Retry. A probe that positively reports the
process gone still starts the existing recovery.

**Steps to reproduce**
1. Start a remote (Daytona) run with a T1 workload.
2. Send SIGSTOP to the server's Daytona plugin worker for 45 seconds,
then SIGCONT.
3. The run fails `notification_transport_failed` about 40 seconds into
the stall.

**Paperclip version or commit**
master at 7cd5558e41.

## What Changed

- `createRemoteRunnerProcessLauncher`
(`server/src/services/native-runtime/native-session-executor.ts`): when
the health probe throws (RPC timeout or transport error) and the Runner
holds an authenticated connection, the monitor waits one poll interval
and probes again. It does not reject the process completion. The unknown
period has a bound: the remote runner reconnect grace (7 minutes). When
the Runner is not connected, the monitor keeps the old behavior: it
rejects after the 30-second probe budget, without exit proof.
- Probe results that positively answer (exit 45 "process gone", or
identity and generation failures) keep their exact current handling.
- `RunnerProcessLaunchSpec`
(`packages/paperclip-runner/src/control-plane/durable-prp-control-plane.ts`)
has a new optional `isRunnerConnected` callback. `spawnRunner` passes it
to the process owner and keeps it on restart.
- The runnerd transport and the external runnerd driver supply
`isRunnerConnected` from the control plane's count of authenticated
Runner connections.
- `doc/execution-semantics.md` describes the unknown-probe rule and its
bound.

## Verification

- Red, on master: `npx vitest run
src/services/native-runtime/native-session-executor.test.ts -t "plugin
RPC timeouts until"` (in `server/`). The completion rejected after the
first stalled probe with `{ stage: "process_verification", reason:
"probe_transport_failed" }`. The transport turns that into
`runner_process_termination_unconfirmed`, which is the staging error.
- Red, on master: `npx vitest run
src/live/runnerd-codex-transport.test.ts -t "live evidence of the
runner"` (in `packages/paperclip-runner/`). The launch spec had no
`isRunnerConnected` (`expected undefined to be true`).
- Green: both tests pass. New tests also prove:
- three 40-second plugin RPC timeouts with the Runner connected leave
the same runner supervised, and a later exit-45 probe still confirms
exit;
- with the Runner disconnected, the same timeout rejects without exit
proof (`probe_transport_failed`, `isExitConfirmed() === false`);
- with the Runner connected and probes that never answer, supervision
rejects without exit proof after the 7-minute bound.
- `server/src/services/native-runtime/native-session-executor.test.ts`,
`remote-runner-recovery-probe.test.ts`,
`packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts`,
`src/control-plane/durable-prp-control-plane.test.ts` and
`src/drivers/external` pass locally.
- `tsc --noEmit` passes for `server` and `packages/paperclip-runner`.
- Chaos: CR3 results are in a follow-up comment on this PR.

## Risks

- While the Runner stays connected and the plugin cannot answer, the
controller waits up to 7 minutes before it fails closed. Before this
change it failed after about 40 seconds. During that time the Runner's
own connection is the liveness evidence, and the controller never
reports an exit.
- When the Runner is not connected, the timing is the same as before.
Sandbox loss still fails closed after the 30-second probe budget.
- The new spec field is optional. A process owner that does not read it
keeps the old behavior. State saved by an older version is not changed.
- No wait is added before `turn.start`.

> 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, through Claude Code
with tool use (shell, file edit, code search). Extended reasoning was
on.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 15:45:25 -06:00
DottaandClaude Opus 5.5 884a9e19ea fix(native): launch a warm follow-up from its predecessor's saved journal, and restart a Runner that exited before session.open (#20928)
## Thinking Path

- Paperclip runs agent work through a controller (server) and a runner
(`paperclip-runnerd`) that exchange numbered commands; the runner
refuses any command whose number is not exactly one past the last it
processed.
- Staging's most common failure is "Runner gone"
(`runner_remote_process_identity_unavailable`,
`native.remote_runner_recovery_failed` with `process_not_running`,
`runnerLifecycle: ready`, `runnerSettled: false`). Chaos wave
`w-20261111T192658-s5201` (Daytona, warm, Codex, W2/W2Q/W2C re-wake
cells) captured it live: the new runner wrote `controller sequence must
be contiguous: expected 13, received 11` (and `expected 5, received 3`)
and exited 0.15 s after authenticating.
- In all three captured failures the follow-up run started 1–2 s before
the predecessor's warm session was closed (`transport_close_requested`
for the predecessor arrives after the follow-up's `run started`). The
close is issued by the follow-up's own admission.
- Admission first materializes the controller projection for the session
root. With an idle retained warm owner the root only holds the
external-persistence marker, so `restoreWarmRunnerdController` copies
that owner's saved checkpoint into the root. Later in the same
admission, `executePaperclipNativeSessionWithinScope` finds the owner
cannot be reused (configuration, process or GitHub access changed) and
retires it. Retirement sends `runner.drain` and `runner.suspend`; both
are saved to the owner's checkpoint before they are sent, but the root
still holds the projection taken before the close.
- The follow-up then opens its controller from that projection. Its
journal is two commands behind the runner's durable
`lastControllerCommandSeq`, so its first command repeats sequence 11;
the runner treats this as fatal and exits, and every later attempt only
reattaches to a process that is gone.
- So the saved controller journal was persisted correctly; it was not
selected: the projection is never refreshed after the retirement that
extends it.

## Linked Issues or Issue Description

- Refs #20874 (the runner-exit proof this extends from a rejected
`session.open` to a Runner that exited before answering it)
- Refs #20907 (makes the runnerd exit text, including `controller
sequence must be contiguous`, visible in the run log; independent of
this fix)
- Refs #20853 (related: resumes a remote Runner that died at a settled
`ready` cut; this PR covers the unsettled `ready` state left by an exit
before `session.open`)
- Duplicate search: searched open and closed PRs for "controller
sequence contiguous", "runner_remote_process_identity_unavailable" and
"Runner gone"; none fixes the stale controller projection.

## What Changed

- `server/src/services/native-runtime/warm-controller-journal.ts` (new):
- `refreshRetiredWarmOwnerProjection` reloads the retired owner's
canonical checkpoint into the session root after retirement, only when
the root still projects that owner and the saved journal extends the
projected one. A diverged journal holds admission with
`native_runner_checkpoint_projection_held:retired_owner_journal_diverged`;
a projection changed underneath holds with `...:journal_changed`.
- `reconcileControllerJournalWithRunner` is the defence for state
already saved in the bad shape (old-saved/new-resumed, mid-run deploys).
When the remote runner state shows its journal ahead of the controller
projection and the runner's last command is a completed
`runner.suspend`, it reconciles only if every extra sequence is a
completed `command_close_drain_*`/`command_close_suspend_*` in the
runner's processed-command journal and the predecessor's canonical
checkpoint holds exactly those commands. Anything else refuses before
launch with
`native_controller_recovery_missing_evidence:controller_journal_behind_runner`
instead of guessing. A runner whose last command is not a completed
suspend is left to the existing live-adoption checks.
- Defence in depth for the server side of the same failure
(`native_session_recovery_failed: runnerd exited while waiting for
session.open`, then `runner_remote_process_identity_unavailable` on
every same-run retry): a Runner proven to have exited while this
attempt's opening command (`session.open`, or the recovery probe) was
still unanswered is a failed fresh start, not a live Runner to reattach.
- `runnerd-codex-transport.ts`: `runnerExitConfirmed()` (#20874's proof)
now also holds for a resumed session whose provider identity was
preloaded, when the attempt's opening command is still pending at the
proven exit.
- The Codex driver's recovery result, `HarnessDriverBackend` and
`native-session-runtime` carry that proof on the
`native_session_recovery_failed` error.
- The executor records `runnerExitedBeforeSessionOpen` on the settled
failure; restart recovery puts `exitedBeforeSessionOpen` on the
`reattach_remote_runner` claim; `verifyRemoteRunnerOrSettledExit` and
`verifyStagedRemoteRunner` then start a new Runner when the process is
gone and the saved Runner state has no pending terminal delivery,
provider cleanup or warm transition (any lifecycle other than an active
turn). Attempt budget and identity checks are unchanged.
- `native-session-executor.ts`: calls the refresh right after the
configuration-changed retirement of a warm owner, and the reconciliation
in `restoreRemoteSuspendedRunnerCheckpoint` with the runner state it
already reads (no added remote read, no wait before `turn.start`).
- Tests:
- `native-session-executor.test.ts`: new `changed-external-checkpoint`
scenario of "rotates an owned warm codex remote runner before checking
changed configuration" with a real `DurablePrpControlPlane`: the retired
owner's close saves drain/suspend, and the journal the follow-up
launches from must end with them.
- `warm-controller-journal.test.ts`: refresh, divergence hold, untouched
marker/other run, reconcile of a settled close, refusal when the
canonical journal cannot prove the close, refusal for any other extra
command, and no action for a live runner.
-
`packages/paperclip-runner/src/live/runnerd-stale-controller-journal.test.ts`:
real `paperclip-runnerd` + fake Codex. A follow-up controller on the
predecessor's suspended runner with a journal that lacks the close
commands reproduces `controller sequence must be contiguous` and loses
the runner; with the current journal it resumes.

## Verification

- Red for the added part: `verifyRemoteRunnerOrSettledExit` with the
staging state (process gone, lifecycle `ready`, unsettled outbox)
rejects with `runner_remote_process_identity_unavailable` without the
change; with `exitedBeforeSessionOpen` it resolves to a fresh start, and
stays fail-closed for a pending cleanup, terminal delivery, active turn
or exhausted budget. Without the propagation, the backend recovery
result lacks `runnerExitConfirmed`; the real-runnerd stale-journal test
asserts the transport's exit proof after the contiguity exit (`expected
false to be true` on the old transport).

- Red on master (executor unchanged, new scenario): `AssertionError:
expected [] to deeply equal [ 'runner.drain', 'runner.suspend' ]` — the
follow-up launched from the projection taken before the predecessor's
close.
- Green: `cd server && npx vitest run
src/services/native-runtime/native-session-executor.test.ts -t "rotates
an owned warm codex remote runner before checking changed
configuration"` (14 passed), `npx vitest run
src/services/native-runtime/warm-controller-journal.test.ts` (7 passed).
- Real runner: `cd packages/paperclip-runner && npx vitest run
src/live/runnerd-stale-controller-journal.test.ts` (stale journal →
`controller sequence must be contiguous`; current journal → resumes).
- `pnpm --filter @paperclipai/server typecheck`, `pnpm --filter
@paperclipai/paperclip-runner typecheck:typescript`; `pnpm verify`
selected broader verification (a new runner test file is outside the
resolved graphs), so broad coverage comes from required CI.

## Risks

- The refresh replaces the root projection only with a checkpoint whose
identity matches and whose journal extends the projection; otherwise it
holds admission with an existing
`native_runner_checkpoint_projection_held:*` code, which is safer than
launching a runner that will exit.
- The defence refuses launch when a runner is ahead by anything other
than a provable settled close. Before this change that launch always
ended in a runner exit and a failed run, so the refusal only changes the
error code and the moment of failure.
- This PR does not change the runner. The runner still exits on an
out-of-order new command; a follow-up runner PR will turn that into a
rejected command result.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), 1M-token context, tool
use (shell, file edits, tests) 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-11 15:41:03 -06:00
DottaandClaude Opus 5.5 edd00526e7 fix(native): park a response_wake with no admitted response instead of self-waking (#20935)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A native agent run ends with a structured result. The status arbiter
turns that result into an issue status and next actions.
> - A result can report `yielded` with `continuation.kind:
"response_wake"`. This means "wake me when somebody responds".
> - Only a direct Board comment gave this kind a passive wait. Every
other `response_wake` became an immediate `issue_status_changed` wake to
the same agent.
> - The agent woke, saw nothing new, reported `response_wake` again, and
woke again. Nothing bounded this loop. It spent provider budget and its
back-to-back runs caused other failures on warm sandboxes.
> - This pull request parks such a result as a visible wait, and puts
declared continuation wakes under the rewake throttle as a backstop.
> - The benefit is that an agent that waits for input stops and costs
nothing until a real response arrives.

## Linked Issues or Issue Description

**What happened?**

Restart-chaos waves on master `683fd1e9a5` (no faults injected) showed
one task run 30 to 66 times, about 12 s apart. Example: wave
`w-20261111T190352-s5101`, task t02 (W2C), 65 runs in 13 minutes. Each
run reported `yielded` with `continuation.kind: "response_wake"` and the
summary "No changes made. This status-change wake does not trigger phase
2". Each run was woken by `issue_status_changed`, and each finalization
committed `live_continuation_registered` with an `enqueue_continuation`
effect of kind `response_wake`.

Root cause: `arbitrateNativeStatus()`
(`server/src/services/native-runtime/status-arbiter.ts`) only treats
`response_wake` as passive when `boardResponseWaitAuthorized` (from
`readNativeBoardResponseWaitSource`), external chat, or plan wait is
true. Otherwise it falls to the generic yielded branch, and
`status-decision-committer.ts` (`enqueue_continuation`) enqueues an
`issue_status_changed` wake to the same agent. `issue_status_changed` is
not in `THROTTLED_ISSUE_REWAKE_REASONS`, and the native committer writes
an `issue.updated` activity row with the run's id for every decision, so
the rewake throttle saw every native run as progress.

**Expected behavior**

A `response_wake` with no admitted response source parks the task. A
comment, an interaction answer or a status change by somebody else wakes
the agent. No path can loop without a bound.

**Steps to reproduce**

Run `tests/restart-chaos/main.ts --env local --adapters codex
--workloads W2,W2Q,W2C --concurrency 10 --tasks 10 --seed 5101
--lifecycle warm --triggers time`. W2/W2C tasks loop after phase 1.

## What Changed

- `status-arbiter.ts`: a `yielded` / `response_wake` result with no
admitted response source and no blocking remaining work now commits
`response_wake_waiting_for_input`, keeps the task `in_progress`, and has
no effects. Policy version is `phase6-v14`. Board, external chat, plan,
governance, dependency, pause and unfinished-work branches are unchanged
and still come first.
- `native-runtime/native-response-wake-wait.ts` (new) and
`recovery/service.ts`: recovery treats that applied decision as a
durable wait while it is the issue's latest decision, the issue is
`in_progress` with the same agent assignee, and no user comment arrived
after the run started. This applies to the stranded-work sweep and to
the paused-agent passive-wait check.
- `execution-projection.ts`: a committed run with this reason shows the
phase `waiting_for_answer`, label "Waiting for a reply", and a next
action that tells the user to comment or change the status. This shows
only while that decision is still the issue's latest decision and the
issue is `in_progress`; an older run shows "Completed".
- Backstop in `issue-rewake-throttle.ts` and `heartbeat/queue.ts`: an
`issue_status_changed` wake that carries a declared `continuationKind`
(the `enqueue_continuation` effect) is now a throttle candidate. The
native status committer's own `issue.updated` / status-decision activity
no longer counts as run progress or as new input. After two consecutive
successful runs with no issue-visible progress, such a wake waits for
the existing escalating cooldown (2 min, doubling, max 30 min). The
native status dispatcher (`dispatchNativeStatusWakeBatch`) does not drop
a throttled intent: it keeps the intent `queued`, reports the cooldown
deadline to the deferred-receipt worker, and dispatches it again when
the cooldown ends. Status wakes without a declared continuation (blocker
owner, notify owner, retry) are unchanged.
- Restart chaos: new enforced universal check U5 (`universal.ts`). A
task with more than 10 runs fails as `U5:run_budget_exceeded`
(`RESTART_CHAOS_U5_MAX_RUNS`). The README records the calibration.
- `doc/architecture/native-status-arbitration.md`: the decision table
and the wait rules.

## Verification

- RED on master (same reason as the waves):
`heartbeat-process-recovery.test.ts` "parks a native response_wake that
no response admitted…" fails with decision `reasonCode:
"live_continuation_registered"` and effect `{ kind:
"enqueue_continuation", continuationKind: "response_wake" }`.
`heartbeat-issue-rewake-throttle.test.ts` "throttles a
same_agent/response_wake/retry continuation…" fails 3/3 because the wake
is admitted.
- GREEN: the integration test finalizes the run, dispatches native
status wakes and runs the stranded-work sweep three times. It asserts
one run total, one wake request, no recovery action, the task
`in_progress`, and the projection "Waiting for a reply".
- `heartbeat-process-recovery.test.ts` "delays a no-progress native
continuation until the rewake cooldown…" finalizes a real `same_agent`
continuation after a no-progress run, dispatches twice (one throttle
receipt, intent stays `queued`, deadline equals `nextAllowedAt`), then
dispatches after the cooldown and gets the run. It fails when the
dispatcher drops the throttled intent.
- Focused suites pass: `heartbeat-process-recovery`,
`heartbeat-issue-rewake-throttle`, `status-arbiter`,
`native-status-arbiter-corpus`, `runtime-mode`, `execution-projection`,
`chat-run-milestone-work`, `heartbeat-dependency-scheduling`,
`heartbeat-run-summary` (904 tests), and all restart-chaos unit tests
(584 tests). `pnpm --filter @paperclipai/server typecheck` passes.
- Universal audit over the 17 saved waves of 2026-11-11: U5 flags 12
tasks, all looping tasks (12 to 66 runs), and 0 tasks on all-pass waves.
- Chaos rerun of local warm seed 5101 (W2, W2Q, W2C): result is added in
a comment on this PR.

## Risks

- Behavioral shift: a model that reports `response_wake` to mean
"continue now" no longer gets an immediate run. The task waits visibly
for a reply. The runner prompt already describes `response_wake` as a
real response wait, and `same_agent` stays available for server-owned
continuations.
- The throttle now applies to native runs. A chain of declared
continuations with no issue-visible output (no comment, document, work
product or other activity) waits for the cooldown after the second such
run. `same_agent` continuations are server-owned (repository
preparation), so this affects only repeated no-output chains.
- Old saved state: decisions committed under `phase6-v13` keep their
recorded effects. A run that finalizes after deploy uses the new policy.
No migration.
- Budget hard-stop behavior is unchanged.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 15:40:44 -06:00
DottaandClaude 80b2369f26 fix(native): keep remote runnerd exit evidence and report it on failed recovery (#20907)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs on Daytona start a detached `paperclip-runnerd` in the
sandbox, and the controller adopts it after a restart
> - On staging, recovery fails with
`runner_remote_process_identity_unavailable` when the Runner is gone,
but nobody can see why it exited
> - The launch script sends runnerd's stdout and stderr to `/dev/null`,
so only a panic or a graceful error reaches `runnerd.stderr.log`, and
the recovery event does not read that file
> - This pull request keeps runnerd's stderr in a bounded file in its
private diagnostics directory, and adds the exit reason to the
`native.remote_runner_recovery_failed` event
> - The benefit is that the reason a Runner exited shows in the run log
on staging without SSH, which the class R investigation needs

## Linked Issues or Issue Description

**What happened?**
Staging runs fail with `runner_remote_process_identity_unavailable`
after the remote Runner exits (`native.remote_runner_recovery_failed`
with `stage: process_verification`, `reason: process_not_running`). The
event does not say why the Runner exited.

**What did you expect?**
The run log names the Runner's exit reason: its durable
`recoverableFailure` and diagnostics, and what it wrote to stderr.

**Steps to reproduce**
Restart-chaos re-wake and stall cells reproduce the refusal. Each failed
run log shows only `runnerLifecycle` and `runnerSettled`.

Refs #20893

## What Changed

- `REMOTE_RUNNER_CHILD_LAUNCH_SCRIPT` sends runnerd's stderr to
`diagnostics/runnerd.process-stderr.log` (mode 0600, in the 0700
directory) instead of `/dev/null`.
  - At launch, a file above 1 MiB is rotated once to `.1`.
- While runnerd runs, a detached trimmer is a disk backstop. Past 8 MiB,
it keeps the last 1 MiB. It checks every 15 s and ends when runnerd
ends. It is double-forked with its own argv, so it is not a child of
runnerd and does not carry runnerd's arguments. runnerd's own exit
reason goes to `runnerd.stderr.log`, which is never trimmed.
- A symlink or non-file at that path keeps the old `/dev/null` behavior.
- Stdout is unchanged (`/dev/null`). Runnerd's PID is still the launch
shell's PID, so the identity marker and fingerprint are unchanged.
- `remote-runner-recovery-diagnostics.ts` has a read-only
`REMOTE_RUNNER_EXIT_EVIDENCE_SCRIPT`. It prints a 2 KiB tail of
`runnerd.stderr.log` and of `runnerd.process-stderr.log`. It reads the
session diagnostics directory first, then the shared runtime-root
directory used by external Runners (Dot, Muse, grok_bot). It never reads
through a symlink. Each file keeps its own share of the 4 KiB budget.
- When recovery's process verification finds `process_not_running`, the
diagnostic adds these fields:
- `runnerRecoverableFailure`: a code-shaped string from
`runner-state.json`;
- `runnerDiagnostics`: the last 4 entries, redacted, 500 characters
each;
  - `runnerExitEvidence`: the redacted file tails, 4 KiB at most.
  
  `native.remote_runner_recovery_failed` carries them.
- The live process monitor's exit report now puts the durable state
summary first, then the file tails. Before, it used one or the other.
- `run-failure-diagnostics.ts` is unchanged. It still copies only the
closed labels, so no free text reaches failure reports.

## Verification

- `pnpm exec vitest run
server/src/services/native-runtime/remote-runner-launch-stderr.test.ts`:
the launch writer keeps stderr in the file, writes the identity marker
with the child's PID, appends across relaunches, rotates an oversized
file, trims a growing file while the child runs, and never writes
through a symlink.
- `pnpm exec vitest run
server/src/services/native-runtime/remote-runner-recovery-diagnostics.test.ts`:
the evidence script output is bounded and refuses symlinks. Redaction
and bounds hold, and the new fields survive the event payload.
- `pnpm exec vitest run
server/src/services/native-runtime/native-session-executor.test.ts`
passes (1374 tests). One expectation was updated for the combined exit
report.
- `npx tsc --noEmit -p server/tsconfig.json` is clean.
- Older runner images: the script is server-side. It adds only `wc`,
`tr`, `tail`, `cat`, `mv`, `sleep` and `kill`. The script already needed
`node`, `date`, `chmod`, `mv` and `printf`, so any image that launched
runnerd before has these tools. The runner content ID is unchanged, so
the current Daytona image runs it unmodified. The restart-chaos waves
run on that image.

## Risks

- Low. A sandbox disk that is already full could make the stderr
redirect fail. `exec ... 2>>file` then fails, and the launch reports
`runner_remote_process_identity_unavailable` the same way a launch that
cannot write its identity marker does. The marker is written first, in
the same directory tree.
- Stderr content stays in the sandbox. Only a redacted 4 KiB tail goes
into the instance's own run log (run-log data path; not Telemetry and
not Observability).
- No wait is added before `turn.start`. The evidence read runs only on
the recovery failure path, with its own 10 s bound.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-11-11 15:31:04 -06:00
DottaandClaude Fable 5.2 b2779054d4 fix(issues): let a native run hand its own task to a person without cancelling itself (#20884)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A task has one assignee. An agent often finishes its work by handing
its own task to a person for review.
> - The task update route stops the outgoing run before it saves a
reassignment.
> - Since #16082, a legacy run that hands its own task to a person is
exempt, so it finishes its turn. Native runs were not exempt.
> - On staging, a native run (ACPX Claude, 11.5 minutes of work) sent
`PATCH /api/issues/{id}` to hand its task to a person. The route
cancelled that run with `issue_reassigned`. The agent saw
`api_transport_failure`. The task moved to the person, but it kept a
stale execution blocker ("Saved work recovered. Send a message to
continue.").
> - The native runtime assumes a running run still owns its task. So
widening the exemption alone is not enough: the run then fails after its
result is accepted, and its finalizer can act on the person's task.
> - This pull request applies the exemption to native runs and makes the
native terminal fence, restart recovery and finalizer handle a run that
gave its task away.
> - The benefit is that an agent can hand its task to a person and
finish normally, with no blocker on the person's task.

## Linked Issues or Issue Description

Refs #16082 (legacy exemption). Related: #20541 (agent-to-agent self
handoff) and #18293 (agent key handoff). Those PRs cover other actors
and targets. This PR covers native runs that hand their task to a
person.

**What happened?**

A native agent run handed its own task to a person (`assigneeAgentId:
null`, `assigneeUserId`, status `in_review`) through its signed run
token. The route cancelled the run that sent the request
(`issue_reassigned`, "Cancelled before issue reassignment"). The
cancelled native run then went through workspace recovery. The person's
task kept an execution blocker with `restore_failed`.

**Expected behavior**

The run that hands its own task to a person keeps running and finishes
its turn. The task rests `in_review` with the person. No execution
blocker or recovery action stays on the task. The outgoing run cannot
change the task after the hand-off.

**Steps to reproduce**

1. Run a native agent on its assigned task.
2. From that run, send `PATCH /api/issues/{id}` with `assigneeAgentId:
null`, a person's `assigneeUserId`, and status `in_review`.
3. Before this fix, the route cancels the run with `issue_reassigned`.

**Why the exemption was legacy-only**

#16082 limited the exemption to the run shape of its incident and kept a
verified stop for every other case. The new H2 chaos workload showed why
native needed more than the route change. With only the route widened:

1. The run's terminal fence `assertCurrentWakeCommentsRead` requires the
run to still own the task. It threw
`native_current_wake_comments_binding_changed` after the result was
accepted. The run then failed with `native_session_lease_lost`, and its
finalizer recorded `workspace_finalization_failed` as an agent-owned
recovery on the person's task.
2. A restart after the hand-off left the run `running`, because restart
recovery refused it with `issue_execution_lock_changed`.
3. The native finalizer only superseded a `done` decision. A
`needs_review` report moved the person's task back to `in_progress` and
queued a continuation for the outgoing agent.
4. The REST guard against the outgoing run's later mutations
(`issue_handed_off`) applied to legacy runs only.

## What Changed

- `server/src/routes/issues.ts`: the signed current-run human
self-handoff exemption now applies to native runs. All other checks
stay: exact run, agent, company and task, `agent_jwt` source, human
recipient. For a native run the route first records the hand-off on the
run (`resultJson.humanSelfHandoff`, new `native-human-self-handoff.ts`).
Board reassignment, agent keys, and handoffs to an agent or to nobody
still stop the outgoing run.
- `current-wake-comments.ts`: the terminal fence accepts a run whose
recorded hand-off matches the person who now owns the task. Without the
record (for example a board reassignment), or after a later
reassignment, it still refuses.
- `native-restart-recovery.ts`: after a restart, a handed-off run is not
resumed. It passes the execution-lock check and is then settled by the
existing ownership gate (`task_ownership_or_status_changed`) without
changing the person's task, instead of staying `running`.
- `status-arbiter.ts` and `native-run-finalizer.ts`:
`supersedeHandedOffDecision` keeps the person's task status and strips
retries, continuations, owner wakes, recoveries, blockers and reviewer
bindings, also from decisions that already preserve the status. Two
effects stay: a governed request's interaction, and the
workspace-finalization repair, so saved work is not lost. Review runs
are not affected.
- `server/src/run-task-ownership.ts` (renamed from
`legacy-run-task-ownership.ts`): `assertHandedOffRunTaskOwnership`
rejects later task mutations from the outgoing legacy or native run, in
the route and under the update transaction's locks.
- `tests/restart-chaos/`: new opt-in workload `H2`. The agent saves a
brief, then hands the task to its creator in review as its last step.
The oracle requires the task to rest `in_review` with the creator, every
run to settle without `issue_reassigned`, readable non-empty run
history, and no execution blocker or active recovery action.
- Docs: `doc/execution-semantics.md`, task feature map and agent access
map, chaos README.

## Verification

- Red on `origin/master` (same tests, master source): the route cancels
the native run (500, and 409
`runner_goal_reassignment_stop_unconfirmed`); the finalizer moves the
person's task to `in_progress` with a continuation for the outgoing
agent; a late `done` from the outgoing native run closes the person's
task.
- Red chaos wave on master's server (`--server-from` a master checkout,
`--adapters claude --workloads H2 --faults K1 --matrix`): 0/2 pass. Both
runs ended `cancelled` / `issue_reassigned`
(`T:handoff_did_not_cancel_run`).
- Green focused tests on this branch:
- `native-human-self-handoff.test.ts` (embedded Postgres): hand-off
through the real issue service; the run stays `running`; `done`,
`needs_review` and failed-turn reports leave the task `in_review` with
the person, with `getExecutionBlocker` null and no recovery action; the
terminal fence accepts only the recorded hand-off; a late change from
the outgoing run fails with `issue_handed_off`; pure policy cases.
- `issue-agent-mutation-ownership-routes.test.ts`: native and legacy
self-handoffs persist without `cancelRun`, and native records the
hand-off before the update. Board, agent key, other run, task, agent and
company still stop the run.
- Native runtime suite (restart recovery, wake comments, arbiter,
stale-owner completion), `issues-service`, `low-trust-red-team-routes`:
pass. Server and restart-chaos typecheck pass. `node
scripts/check-maps.mjs` passes.
- Green chaos waves on this branch (local, `--adapters claude
--workloads H2`):
- `--matrix --faults K1 --seed 4459`: 2/2 pass (K0 baseline and a K1
crossing).
- `--tasks 3 --faults K1 --fault-after-seconds 60` and
`--fault-after-seconds 100`: 3/3 pass each. In the first, all three
restarts landed after the hand-off while the runs were live; the runs
settled and the tasks stayed with the person.
- The final waves ran on the pre-rebase head `8a2a3b6a74`. The rebase
onto master only merged the chaos workload lists (`types.ts`,
`config.ts`, `journeys.ts`). No Daytona wave was run.

## Risks

- The outgoing native turn keeps running after the person owns the task.
Its native task tools already require the run to own the task, so they
refuse (`paperclip_runner_tool_binding_not_authorized`). Its REST
mutations are rejected with `issue_handed_off`.
- A restart after the hand-off settles the run as failed
(`native_restart_recovery_blocked`) instead of resuming it. The person's
task is not changed and needs no Retry.
- The finalizer rule covers any native run whose task belongs to a
person at finalization, not only a self-handoff. Before this change such
a run could still move the person's task and queue its own continuation.
- Saved state: `resultJson.humanSelfHandoff` is a new optional run key.
An older server ignores it, so a run handed off on the new server and
finished on an old one fails its fence as today. No schema change.
- No API shape change.

> 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 (Anthropic), model ID `claude-opus-5-5`, through Claude Code
with tool use and code execution. The session does not expose the
context window or reasoning mode.

## 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 Fable 5.2 <noreply@anthropic.com>
2026-11-11 15:30:59 -06:00
Nicky LeachandClaude Opus 5.5 d7b04a8b6d refactor(runner): rename codex-runtime-adapter to acpx-runtime-adapter (#20938)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Paperclip Runner opens agent sessions through one ACPX runtime
adapter
> - The adapter began as the Codex ACPX adapter, so its file is
`codex-runtime-adapter.ts`
> - The adapter now opens sessions for every ACPX agent: Claude, Codex,
Pi, Cursor, Grok, Kimi, Copilot and Antigravity
> - The Codex name makes readers look for Claude and other agent
behavior in the wrong place
> - This pull request renames the module to `acpx-runtime-adapter.ts`
and changes no behavior
> - The benefit is a file name that tells readers what the module does

## Linked Issues or Issue Description

No issue exists. Refs #20933, which adds Claude restore behavior to this
module.

**What existing behavior does this improve?**
The name of the shared ACPX runtime adapter module in Paperclip Runner.

**Subsystem affected**
Paperclip Runner, ACPX driver
(`packages/paperclip-runner/src/drivers/acpx/`).

**Current behavior**
The module that opens ACPX sessions for all agents is named
`codex-runtime-adapter.ts`. Its test file, which holds Claude, Pi, Grok,
Cursor and Copilot tests, is named `codex-runtime-adapter.test.ts`.

**Proposed behavior**
The module is `acpx-runtime-adapter.ts` and its test file is
`acpx-runtime-adapter.test.ts`. All imports, scripts and docs use the
new name.

**Reason and benefit**
The name matches the module's scope. Reviewers and contributors find
agent-specific session behavior faster.

**Breaking changes**
None. No exported name, behavior or contract changes.

## What Changed

- Rename `src/drivers/acpx/codex-runtime-adapter.ts` to
`src/drivers/acpx/acpx-runtime-adapter.ts`, and its test file in the
same way. Git records both as renames with no content change, except the
test file's import path.
- Update the imports in `acpx-runtime-sidecar.ts`,
`codex-acpx-driver.ts` and four test files.
- Update the `dist` import in `scripts/grok-native-smoke.mjs` and the
test file key in `scripts/vitest-lane-durations.json`.
- Update the links and file names in
`doc/RUNNER-INTEGRATION-PLAYBOOK.md`,
`doc/architecture/runner-cursor-capabilities.md` and
`doc/architecture/runner-rich-acp-capabilities.md`.
- Do not change the historical profile identity fixtures under
`packages/paperclip-runner/test/fixtures/`. They record the sources of
earlier releases with the old path, and tests check them by hash.

## Verification

- `npx tsc -p tsconfig.json --noEmit` in `packages/paperclip-runner`: no
errors in changed files. The only errors are in
`src/eval/workflow-harness.ts`, because
`@paperclipai/paperclip-eval-kernel` was not built in the local
worktree.
- `npx vitest run src/drivers/acpx
src/cli/acpx-runtime-sidecar.test.ts`: 93 files, 1859 passed, 9 skipped.
- `node --test scripts/contracts/contract-fingerprints.test.mjs`: 11
passed. The pack contract digest reads protocol schemas, the Rust crate
and the ACPX patch, not TypeScript sources, so the rename does not
change it.
- `node --check scripts/grok-native-smoke.mjs`, and
`scripts/vitest-lane-durations.json` parses.
- `git grep codex-runtime-adapter` finds the old name only in the
historical fixtures.
- Merge check with #20933: a local merge of that branch into this one
completed with no conflicts. Git applied #20933's changes to the renamed
files. The merged adapter tests passed (133 passed).

## Risks

- Low risk. The change renames files and updates references only.
- An open PR that edits the old file path must merge or rebase after
this PR. Git rename detection handled #20933 with no conflicts. A PR
that adds a new import of `./codex-runtime-adapter.js` fails typecheck
until it uses the new path.
- `openCodexAcpxRuntime` keeps its name. It is an alias of
`openQualifiedAcpxRuntime`, and this PR does not change exported names.
- `acpx-runtime-adapter.ts` and its test file were already longer than
1,000 lines. This PR does not add lines to them.

> 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, local test runs). Extended reasoning was
on.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [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>
canary/v2026.1111.0-canary.14
2026-11-11 21:29:25 +00:00
DottaandClaude Opus 5.5 7cd5558e41 fix(authz): send agents denied a cost-recording route to the Costs page (#20922)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - When an agent is denied a board-only action, the denial names the
board page where a person can do it, and agents relay that link
> - Cost recording retry ("Retry cost recording") is board-only and
lives on the run's row on the Costs page
> - The denial for every `/companies/:id/costs` route named the Budgets
page instead
> - So an agent asked to fix a run's missing cost sent the person to
Budgets, where the control does not exist
> - This pull request maps cost routes to the Costs page and names that
page in the retry route's catalog entry
> - The benefit is that the person lands on the control the agent told
them to use

## Linked Issues or Issue Description

**What happened**
In paid chat eval run 43086418488
(`budgets-costs.repair-cost-recording`), the CEO tried `POST
/api/companies/{companyId}/costs/retry-accounting`, got `403
board_access_required` whose remediation said `[Open
budgets](/<prefix>/activity/budgets)`, and told the person to use the
retry on Budgets. The retry control is on the Costs page.

**Expected behavior**
Board denials for cost routes name `[Open
costs](/<prefix>/activity/costs)`. Budget and budget-incident routes
still name Budgets.

**Steps to reproduce**
1. As an agent, call `POST
/api/companies/{companyId}/costs/retry-accounting`.
2. Read the 403 `remediation`.
3. It links `/<prefix>/activity/budgets`, a page with no cost retry
control.

## What Changed

- `server/src/routes/authz.ts`: `COMPANY_COSTS_PAGE`
(`/<prefix>/activity/costs`); `/companies/:id/costs/*` denials use it.
Budgets and budget incidents keep `COMPANY_BUDGETS_PAGE`.
- `server/src/routes/openapi.ts`: the retry route's description says a
board user retries with Retry cost recording on the run's row on the
Costs page.
- `board-access-denial.test.ts`: regression row for the retry route (red
before the change, green after).

## Verification

- `pnpm exec vitest run src/__tests__/board-access-denial.test.ts
src/__tests__/openapi-routes.test.ts
src/__tests__/agent-refusal-finality.test.ts
src/__tests__/cost-data-quality-routes.test.ts
src/services/native-runtime/runner-api.test.ts` (server): all pass. The
new row fails without the `authz.ts` change.
- End-to-end: the chat eval case `budgets-costs.repair-cost-recording`
grades that the agent's guidance links the Costs page; it will be rerun
after this merges.

## Risks

- Low. Only the link text and target in cost-route denials and one
catalog description change. Agents that previously relayed Budgets now
relay Costs.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), 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-11 14:54:36 -06:00
DottaandClaude Opus 5.5 c1edb9ec02 fix(native): keep an unreachable remote Runner alive when a failed run closes (#20923)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs on a remote sandbox (Daytona) start a detached
`paperclip-runnerd`. After a recoverable failure, the server resumes the
same run and reattaches to that Runner
> - On staging, `runner_remote_process_identity_unavailable` was the
most frequent failure. The tasks stayed blocked until someone clicked
Retry
> - In 31 of the 34 failed runs the reattachment found the Runner gone
with an unsettled `ready` journal (`runnerSettled: false`). The Runner
was not lost by accident. The controller's own close stopped it with
`SIGKILL`: close could not prove suspension within its 10 s budget, so
it force-stopped the Runner in the middle of a turn
> - The server then scheduled a same-run reattachment that could never
succeed, because the close had just removed the Runner it needed
> - This pull request keeps an unreachable remote Runner alive when a
failed run closes. A Stop, a cancelled turn and a `stop_cleanup`
recovery still stop it
> - The benefit is that the same-run reattachment finds the live Runner.
The Runner then settles into a `suspended` journal, which the existing
relaunch path resumes. The task continues without Retry, and no provider
work is replayed

## Linked Issues or Issue Description

Refs #20853, #20907

**What happened?**
Remote native runs failed with
`runner_remote_process_identity_unavailable` and error code
`native_continuation_requires_reconciliation`. The tasks were left
blocked. In the failed runs the first attempt failed with a recoverable
transport error, for example `provider_transport_failed: PRP command
turn.start timed out`, `native_session_interrupted` or a server restart.
The run log then showed `runner did not prove durable suspension before
checkpoint` and `runner shutdown failed: Error: Durable recovery runner
timed out.` The second string comes from `waitForProcess`, which sends
`SIGKILL` when its deadline passes. The same-run reattachment ran 20–60
s later and reported `process_verification` / `process_not_running` with
`runnerLifecycle: ready` and `runnerSettled: false`. Most of these runs
had done provider work before the failure (turns and tool calls), so the
automatic Retry correctly refused them (`provider_turn_unsettled`).

**What did you expect to happen?**
When a run fails for a reason that the server resumes on the same run,
the close does not destroy the Runner that the reattachment needs. The
reattachment adopts it, and the run continues without Retry and without
a repeated tool effect.

**Steps to reproduce**
1. Start a remote native run and let a turn start.
2. Make the Runner unreachable (a broken transport or a slow event
pump), so that the turn fails and the close cannot prove suspension
within its budget.
3. The close sends `SIGKILL` to the Runner. The same-run reattachment
then fails with `runner_remote_process_identity_unavailable`, and the
task is blocked.

**Relation to open PRs**
- #20853 covers the 3 runs whose journal was a settled `ready` cut
(`runnerSettled: true`). This PR does not change that path.
- #20907 makes the Runner's exit reason visible. It does not change
recovery. This PR does not overlap with it.

## What Changed

- `runnerd-codex-transport.ts`: `close(reason, {
retainUnsettledRemoteRunner })` accepts a new option. When the option is
set, the Runner is remote (a process launcher or an adopted Runner), the
close is not a `stop_cleanup`, the Runner is still alive and suspension
is not proven, the close does not stop the Runner. It skips the
`waitForProcess` kill, the adopted-runner `SIGKILL` and `forceKill`. It
still releases the controller route, writes the unsettled checkpoint and
throws `NativeSessionCloseUnrecoverableError`, so the session stays
quarantined and is never reused. The settlement diagnostic records
`runnerRetained: true`, and a new diagnostic records `remote runner left
alive unsettled for same-run reattachment`.
- `HarnessNativeSession.close` asks for retention only when the session
was not explicitly cancelled. After `cancel()` (Stop), it asks for the
old behavior, so Stop still stops the Runner.
- `CodexHarnessSession.close` passes the option to the transport.
`HarnessSession.close` and `CodexAppServerTransport.close` accept the
optional field. Callers that do not set it keep the old behavior.
- Docs: `doc/execution-semantics.md` and `doc/run-log-events.md`
describe the rule and the `runnerRetained` field.

How the run continues: the reattaching controller adopts the live
Runner. The Runner then completes the `runner.drain` and
`runner.suspend` that the failed close queued, and exits with a settled
`suspended` journal. That attempt fails as
`native_runner_process_exited`, which the server resumes on the same
run. The next reattachment finds the process gone with a settled
`suspended` journal and relaunches the Runner over it (the existing path
from #19745). If the Runner was only slow, it suspends by itself and the
first reattachment relaunches.

## Verification

- Red, on `master` with only the tests added:
`runnerd-codex-transport-remote-retention.test.ts` fails 2 of 5
(`expected 'gone' to be 'alive'`): the close killed the Runner.
`harness-driver-backend.test.ts -t "retain an unsettled"` fails 2 of 2.
- Green with the fix: `cd packages/paperclip-runner && npx vitest run
src/live/runnerd-codex-transport-remote-retention.test.ts
src/backends/harness-driver-backend.test.ts`.
- The staging shape: a real `paperclip-runnerd` started through a
process launcher, a turn in progress, the Runner frozen with `SIGSTOP`
during close. The close throws `NativeSessionCloseUnrecoverableError`
with `runnerRetained: true`, the Runner stays alive, and its journal
stays `ready`.
- A second controller attaches through `adoptExistingRunner` (no
duplicate launch). The Runner settles into `lifecycle: suspended` with
an empty outbox and no pending terminal delivery, then exits.
- An explicit cancellation, a caller that does not ask for retention,
and a `stop_cleanup` close still stop the Runner.
- Also run: `src/backends`, `src/drivers/codex`,
`src/drivers/acpx/codex-acpx-driver.test.ts`,
`src/drivers/acpx/codex-runtime-adapter.test.ts`,
`src/native-session-runtime.test.ts` (1045 tests pass; 2 `spawnSync
ETIMEDOUT` under full-parallel load pass alone), and all of `src/live/`
(files that timed out under full-parallel load pass at
`--maxWorkers=2`).
- `npx tsc --noEmit -p packages/paperclip-runner/tsconfig.json` is
clean.
- `pnpm verify` selects a broad plan (exit 2) because
`doc/execution-semantics.md` is a global CI input. CI supplies that
coverage.
- Not run: a live Daytona run. The fix is in the controller library that
runs on the server, so it needs no new Runner image.

## Risks

- Medium. A failed close now leaves an unreachable remote Runner alive
instead of killing it. This is the same state a restart detach leaves
today, and the same-run reattachment or a Stop cleanup owns it.
- If the server does not resume the run on the same Runner, the Runner
stays alive until something reattaches or the sandbox is released.
Remote Runners have no reconnect deadline
(`runnerProcessReconnectGraceMs: null`). Before this change they were
killed. The close still quarantines the session, so it is never released
for reuse.
- Stop is unchanged: an explicit cancellation, `settleCancelledTurn` and
`stop_cleanup` close without retention, so they still stop the Runner.
- Previous-release story: this change adds and changes no persisted
state. State that an older release left (a killed Runner with an
unsettled `ready` journal) keeps its current handling: the named refusal
`runner_remote_process_identity_unavailable` and a working Retry. Tasks
already blocked on staging keep their Retry button. A deploy during a
run changes nothing, because the decision is made in the closing
process.
- No wait is added before `turn.start`. The change only affects the
failure path of close.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), via Claude Code with tool use
(shell, file edits, test runs, read-only staging API reads).

## 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-11 14:17:25 -06:00
DottaandClaude Fable 5.2 e8e548e856 feat(db): classify migrations for application under the running release (#20913)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - `packages/db` owns the schema and its migrations. There are 467
migrations, and several ship each day
> - A zero-downtime deploy starts the new release while the previous
release still serves the same database
> - Core does not say whether a migration is safe to apply under the
previous release. A deployer must treat each pending migration as a
reason to pause, or a person must review it
> - On stacks that may not pause, releases wait for hours
> - Core owns the schema, so Core must own this knowledge and publish it
as data
> - This pull request adds a conservative classifier, an in-file
override, the published result, and a guard for new migrations
> - The benefit is that a deployer can apply routine compatible
migrations automatically, and incompatible changes become visible at
review time

## Linked Issues or Issue Description

**Subsystem affected**

Database migrations (`packages/db`), the cloud migrator artifact
(`scripts/cloud-migrator-artifacts.mjs`), and the pull-request policy
job.

**Problem or motivation**

Core has no machine-readable statement that a migration can be applied
while the previous release still reads and writes the database. A
deployer cannot tell an added nullable column from a dropped column. It
must pause for both, or refuse the release.

**Proposed solution**

Classify each migration as `online_compatible`, `requires_staging`, or
`unknown`. Publish the result in the migrator manifest. Fail a pull
request when a migration it adds is `unknown` and has no reviewed
override in the file.

**Alternatives considered**

- A hand-kept list in the deployer. It goes stale and it is not reviewed
with the SQL.
- A sibling JSON file in the artifact. It needs a workflow change, and
the attestation of `manifest.json` would not cover it.

**Roadmap alignment**

This is release infrastructure. It adds no product feature.

## What Changed

- `packages/db/src/migration-compatibility.ts`:
`classifyMigrationSql(sql)` returns `{ compatibility, classified,
reasons, override, overrideError }`. It parses statement by statement
(breakpoints, `;`, comments, string and dollar quoting, guarded `DO`
blocks). A statement that it does not recognize is `unknown`. A
migration takes the worst class of its statements.
- Lock and duration hazards have their own reason codes
(`lock_hazard_*`) and classify as `unknown`, not as compatible with a
warning.
- Override: `-- paperclip:migration-compat online_compatible:
<justification>` (or `requires_staging`) in the leading comment block of
the migration file. The justification is mandatory (10 characters or
more). A malformed, repeated, or misplaced directive is an error and
never improves the verdict.
- A table counts as new after a plain `CREATE TABLE`, or after `CREATE
TABLE IF NOT EXISTS` when no earlier migration created that table.
`classifyMigrationSequence` classifies migrations in apply order with
that knowledge.
- `packages/db/scripts/migration-compatibility.ts` (run with `pnpm exec
tsx` in `packages/db`): classify files, print the distribution, print
JSON, or run the guard (`--check`). No new package script.
- Published data: `manifest.json` of the cloud migrator artifact has a
new additive field `migrationCompatibility`. `version` stays `1`. No
existing field changes. The build reads the SQL from the packed
`db.tgz`, so the data describes what the migrator applies. `validate`
(publisher) and `verify` reject data that is malformed or does not match
the packed migrations. The workflow file does not change.
- Guard for new migrations:
`.github/scripts/check-pr-migration-order.mjs` (the existing "Validate
migration ordering against target branch" policy step) now also
classifies the migrations that the pull request adds. `check:migrations`
runs the same guard against the merge base with `origin/master`, so the
`@paperclipai/db` typecheck and build report it locally. `unknown`
without an override fails. `requires_staging` prints a notice and
passes. A new migration that uses `CONCURRENTLY` also fails when it is
not in `MIGRATIONS_WITH_TOP_LEVEL_STATEMENTS`
(`packages/db/src/client.ts`), because the migrator would run it inside
a transaction.
- `doc/DATABASE.md`: new section "Migration compatibility with the
running release" under "Schema change workflow".

Published shape:

```json
"migrationCompatibility": {
  "version": 1,
  "migrations": [
    {
      "idx": 468,
      "tag": "0468_busy_wendell_rand",
      "sqlSha256": "56a6e272…",
      "compatibility": "online_compatible",
      "classified": "online_compatible",
      "override": false,
      "reasons": [
        { "code": "add_column", "compatibility": "online_compatible", "message": "Adds a column that is nullable or has a constant default.", "statements": [1] }
      ]
    }
  ]
}
```

`sqlSha256` is the hash that the migrator records in the migration
history. `overrideJustification` is present only when `override` is
`true`. Entries are in apply order (journal `idx`, then file name; the
shipped journal repeats `idx` 178).

### Distribution over the existing migrations

No existing migration is rewritten or annotated.

| Class | All 467 | Newest 100 |
| --- | --- | --- |
| `online_compatible` | 228 (48.8%) | 67 |
| `requires_staging` | 114 (24.4%) | 12 |
| `unknown` | 125 (26.8%) | 21 |

Migrations per blocking reason (one migration can have several):

| Migrations | Class | Reason |
| --- | --- | --- |
| 74 | `requires_staging` | `create_unique_index_existing_table` |
| 70 | `unknown` | `lock_hazard_create_index` |
| 66 | `unknown` | `lock_hazard_validated_foreign_key` |
| 56 | `unknown` | `lock_hazard_data_backfill` |
| 54 | `unknown` | `drop_constraint` |
| 41 | `requires_staging` | `add_validated_check` |
| 33 | `unknown` | `drop_index` |
| 16 | `unknown` | `do_block_procedural` |
| 15 | `unknown` | `replace_function` |
| 13 | `unknown` | `create_trigger_existing_table` |
| 12 | `unknown` | `data_change_existing_table` |
| 10 | `unknown` | `drop_object`, `lock_hazard_set_not_null`,
`set_default` (each) |
| 8 | `requires_staging` | `drop_column` |

## Verification

- `pnpm --filter @paperclipai/db exec vitest run
src/migration-compatibility.test.ts`: 113 tests pass. They cover a
positive and a negative case for each rule, each reason code, the
override (missing, short, misplaced, and repeated justification are
rejected), unknown by default, the guard, the shipped migrations, the
`CONCURRENTLY` list, and the command line.
- `node --test scripts/cloud-migrator-artifacts.test.mjs
.github/scripts/tests/check-pr-migration-order.test.mjs`: 21 tests pass.
- Real bundle at the first commit of this branch: `preview-artifacts.mjs
pack`, then `cloud-migrator-artifacts.mjs build`, `validate`, and
`verify-install` pass. The manifest lists 467 migrations and is 312 KB.
This run found the repeated journal `idx`, which the second commit
handles.
- `pnpm --filter @paperclipai/db run check:migrations` passes. With
`--check --base HEAD~150` the guard fails on three `unknown` migrations
and prints notices for two `requires_staging` migrations.
- `pnpm verify` exits 2 because `.github/scripts/**` is a global CI
input. Required CI supplies the broad coverage.
- Not run: the publish job (it needs the master-only AWS role), and no
deployer reads the new field yet.

## Risks

- The manifest grows from about 1 KB to about 312 KB. A consumer that
rejects unknown manifest fields would fail. The verifier in this
repository ignores unknown fields; consumers outside this repository
were not inspected.
- From this merge, an open pull request that adds an `unknown` migration
fails the policy step and the `@paperclipai/db` typecheck until the
author rewrites the migration or adds the override. `drizzle-kit` emits
validated foreign keys and plain indexes for existing tables, so this is
common. The error text and `doc/DATABASE.md` give the fix.
- `ADD CONSTRAINT ... NOT VALID` classifies as `online_compatible`.
PostgreSQL still enforces it on new writes, so the author must make sure
that the previous release does not write rows that violate it.
- The classifier reads SQL only. It cannot see that new code writes
values that the previous release cannot read (for example a new enum
value). The `requires_staging` override exists for that case.
- `master` cannot fail the guard: the guard only judges migrations that
are not on the merge base.

## Model Used

- Claude (Anthropic), model ID `claude-fable-5-2`, in Claude Code with
tool use and code execution.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [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 Fable 5.2 <noreply@anthropic.com>
2026-11-11 14:14:40 -06:00
DottaandClaude Opus 5.5 fc14f1ade2 fix(execution): keep resumed native runs alive past a stale finalization deadline (#20920)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The execution control reconciler bounds how long a run may spend
settling after its provider work ends
> - A run deadline from one attempt stayed on the run row after the run
was handed to a same-run resume or a restart claim
> - The reconciler then failed the resumed, live run with
`execution_finalization_deadline_exceeded` and told a person to inspect
the provider
> - This breaks two product rules: recovery must work without a person,
and users must not get operator instructions
> - This pull request makes a live native owner or a scheduled same-run
resume the owner of the run, and it replaces the operator text with
plain wording and Retry
> - The benefit is that runs that recover after a restart keep running,
and a genuine expiry shows plain words and one working Retry

## Linked Issues or Issue Description

No public issue exists. This is the bug description, in the fields of
`.github/ISSUE_TEMPLATE/bug_report.yml`.

**What happened?**

On a hosted deploy, 11 tasks became blocked with
`execution_finalization_deadline_exceeded` in two hours. Three runs had
the same sequence. The native session failed or the server restarted. A
same-run resume or a restart claim then took the run. The new controller
held a live native lease and prepared the run again. The reconciler then
found the earlier attempt's run deadline and failed the run. The
recorded original failure code was empty, so no real failure caused the
block. The run error told a person to inspect the failed run and verify
that its provider stopped.

**Expected behavior**

A run that a live controller owns, or that has a scheduled same-run
resume, continues. A deadline that really expires shows plain wording
and a working Retry. Users do not get operator instructions.

**Steps to reproduce**

1. Save a running native run with an `executionControlDeadlineAt` in the
past.
2. Give its finalization row a live lease (a restart claim), or phase
`retryable_failure` with no lease (a scheduled resume).
3. Run `reconcileAbandonedExecutionControl`.
4. The run fails with `execution_finalization_deadline_exceeded`, and
its error tells a person to inspect the provider.

**Paperclip version or commit**

`master` at 6154b9485e.

**Deployment mode**

Hosted (cloud) instance with Daytona remote sandboxes.

**Agent adapter(s) involved**

Not adapter-specific (core bug). Seen with Codex and OpenCode native
runs.

## What Changed

- `execution-control-reconciliation.ts`: when a running native run has
no accepted result and its finalization owner holds a live lease or has
a scheduled same-run resume, the reconciler clears the stale run
deadline and does not fail the run.
- `execution-control-reconciliation.ts`: when the live owner's own
settlement deadline is due, the reconciler moves the deadline to the
lease expiry. It surfaces the failure only after that lease lapses.
- `execution-control-deadline.ts`: adds the cause constant and a plain
message. The reconciler writes this message to the run error, the run
next action, and the recovery action.
- `execution-blocker.ts`: a run saved by an earlier release with this
error code, and an active recovery action with this cause, show the
plain message. Retry stays available. No migration is needed.
- `doc/execution-semantics.md`: documents that a control deadline bounds
one attempt and not a later attempt of the same run.

## Verification

- Red before the fix (5 failures): `npx vitest run
src/services/execution-control-reconciliation.test.ts
src/services/execution-blocker-read.test.ts` in `server/`. The
resumed-run cases failed with `expected 1 to be +0` (the run was
surfaced as failed). The wording cases received the old operator text.
- Green after the fix: the same command passes, 46 of 46.
- Related files pass: `execution-control-work`, `task-retry`,
`workspace-recovery-warm-handoff`, `native-safe-replacement`,
`closed-native-receipt-accounting`, `execution-recovery-resolution`,
`workspace-recovery-admission`, `execution-projection`.
- `npx tsc --noEmit -p server/tsconfig.json` passes.
- `pnpm verify` selects a broad plan because
`doc/execution-semantics.md` changed. CI supplies that coverage.
- Previous-release story: in-flight rows with a stale run deadline and a
live owner continue after deploy. Rows that an earlier release already
failed show plain wording and Retry when read. No schema change.

## Risks

- A native owner that keeps renewing its lease while its settlement
hangs is not failed by this backstop until the lease lapses. The
executor stops lease renewal before it starts failure settlement, so the
wait is bounded by one lease period.
- A scheduled same-run resume now owns the run. Restart recovery
reschedules these resumes at boot. A coordinator deadline that is due
still surfaces the failure.
- Tasks that an earlier release already blocked keep their block. They
now show plain wording and Retry. Chat-bound tasks keep their existing
"send a message" continuation.

## 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-11 14:13:56 -06:00
DottaandClaude Opus 5.5 64afd22d82 fix(runner): keep the OpenCode MCP bridge on keep-alive so OpenCode keeps its Paperclip tools (#20921)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native OpenCode agents call Paperclip tools through a local MCP
bridge in the runner's OpenCode proxy
> - #20886 made the bridge send `Connection: close` on every response,
as hardening for one stalled parallel tool call that was never
reproduced
> - After #20886 merged, many native OpenCode runs started without any
Paperclip MCP tools
> - Agents then said the Paperclip tools were missing, worked only with
bash and file tools, and never called paperclip_finish
> - This pull request removes the `Connection: close` header and keeps
the delivery diagnostics from #20886
> - The benefit is that OpenCode agents get their Paperclip tools again,
and a recurrence of the original stall still shows in the run log

## Linked Issues or Issue Description

Refs #20886.

**What happened**
In chat eval runs on CI, every OpenCode session before #20886 sent a
first model request of about 34k–36k input tokens (prompt plus the
Paperclip tool schemas). After #20886, about 9 of 25 sessions sent only
about 8k–15k tokens on the same prompts (for example 14,711 for
questions-and-approvals.necessary-question, 15,481 for
cases.cases-disabled, 14,096 for cases.update-case-facts). The prompt
envelope size did not change (about 32.7k characters). Those sessions
made no Paperclip MCP calls: zero tool-delivery records, no
paperclip_finish. The agents' own reasoning said "The paperclip tools
aren't in my toolset". Sessions on a commit without #20886 (8d2e34eac1)
all had the full size.

**Expected behavior**
Every native OpenCode session starts with the Paperclip MCP tools.

**Steps to reproduce**
1. Run several chat-features OpenCode cells on master after #20886.
2. Compare the first usage record of each run (inputTokens plus
cacheReadTokens) and the tool names in the run events.
3. See sessions with about 15k tokens and no paperclip_* tool calls.

## What Changed

- `mcp-bridge.ts`: removed `response.setHeader("Connection", "close")`.
The bridge uses Node's default keep-alive again, as before #20886.
- The per-call delivery diagnostics from #20886 stay, so a recurrence of
the stalled parallel call still shows socket reuse, idle time and
`written` in the run log.
- `mcp-bridge-delivery.test.ts`: the pooled-client test now asserts that
the bridge does not close connections, that a later call reuses a
socket, and that each call still gets a correct delivery record.

## Verification

- `pnpm exec vitest run` for mcp-bridge-delivery, mcp-bridge,
mcp-bridge-diagnostics, opencode-proxy-finish-diagnostics,
opencode-app-server-proxy and opencode-proxy-abandoned-tool-call tests:
45 passed.
- The exact mechanism inside OpenCode/Bun is not reproduced locally (a
macOS Bun client against a `Connection: close` server completed 300
MCP-shaped handshakes without error). The evidence is the before/after
correlation above. The next chat eval runs will show the first-request
token counts again.

## Risks

- The original five-minute stall (one occurrence, cause not proven) can
recur. The diagnostics from #20886 will then name the socket state.
- No persisted run, session or pack state changes.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), 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-11 14:10:56 -06:00
DottaandClaude Opus 5.5 c5e6ab5ca8 test(evals): retarget instance-settings from the removed Agent Chat toggle to the Cases experiment (#20897)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The chat catalog eval `instance-operations.instance-settings` checks
that a person who asks the chat agent to turn on an instance experiment
gets the supported operator path, and that the setting really works
> - The case asked "Enable Agent Chat on this instance." Agent Chat is
always on since #19360, and the catalog says so (#20169), so the agent
correctly answered "already on" and the scripted human could not find
the removed toggle
> - The premise is obsolete, so this pull request changes it explicitly
to the Cases experiment (`enableCases`), which still exists and has a
real route gate
> - It also replaces a reply-wording regex with structural checks
> - The benefit is that the case measures its original promise on
today's product

## Linked Issues or Issue Description

**What happened**
In the full chat-catalog run, the case failed in the human journey: the
locator `getByRole('switch', { name: 'Toggle agent chat experimental
setting' })` found no element. The agent's reply ("Agent Chat is already
enabled ... there's no toggle to change") was correct for the current
product. The grader also used reply regexes
(`experimental|enableAgentChat`, `operator|admin|human|settings`, and a
negative "I have enabled" pattern).

**Expected behavior**
The case tests an instance experiment a person can still turn on. The
agent cannot change instance settings, does not change the setting, and
links Experimental settings. The person follows that rendered link,
turns the feature on, sees it persist, and the feature's page opens.
Grading reads saved state and rendered links, not wording.

**Steps to reproduce**
1. Run
`chat-features.runner-opencode.local.instance-operations.instance-settings`
on master.
2. See the agent answer that Agent Chat is always on.
3. See the human step time out on the missing Agent Chat switch.

Refs #19360, #20169. Product companion: #20894 (the denial and the
catalog name the Experimental settings page). It is cherry-picked here
so the paid run measures it.

## What Changed

- Premise change (explicit): query "Enable Agent Chat on this instance."
becomes "Turn on Cases for this instance."; context and expected outcome
follow. Recorded in the manifest `changedFields`, pinned in the manifest
test, inventory digest updated.
- Setup: refuses a hidden Cases setting, starts Cases off (cleanup
restores the target's value), and probes GET/PATCH experimental settings
with the agent's own key (403 `board_access_required`, setting
unchanged).
- Human journey: checks that Cases is still off after the agent's turn
and that `/<prefix>/cases` redirects to the dashboard. Then follows the
Experimental settings link that the reply rendered (no typed-URL
fallback), turns Cases on, checks it after reload, and opens the Cases
page.
- Grader: the `supported-human-path` regex check is replaced by
`agent-links-experimental-settings` (rendered link to the page, and the
person followed it). Saved-state checks cover the off route, the save,
persistence and the open Cases page.
- `oracleVersion` is now
`paperclip.chat-catalog.privileged.instance-settings.v2`. `sourceFiles`
follow the code.
- Calibration: the valid reply and a reworded reply pass. Each missing
state, a missing link, a wrong page, a foreign origin and a literal
`<prefix>` link fail.

## Verification

- `pnpm exec vitest run --config tests/runner-e2e/vitest.config.ts
tests/runner-e2e/chat-catalog-domains/privileged-companions.test.ts
tests/runner-e2e/chat-catalog-domains/privileged-budget.test.ts
tests/runner-e2e/chat-catalog-contract.test.ts
tests/runner-e2e/chat-catalog-consistency.test.ts`: 25 passed.
- `node --test
tests/runner-e2e/chat-full-catalog-testops-manifest.test.mjs`: 10
passed.
- `node tests/runner-e2e/source-sdk-public-inputs.mjs <HEAD> --check`:
unchanged.
- `npx tsc --noEmit -p tests/runner-e2e`: no errors in changed files.
- The paid end-to-end run is not done yet.

## Risks

- The case now depends on the Cases experiment. If Cases becomes always
on or is removed, the case needs another retarget.
- The agent can still say something wrong in prose. The grader cannot
detect a false "I turned it on" claim without wording checks, so a human
reads the reply. The saved state proves that the agent did not change
the setting.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), 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-11 14:02:01 -06:00
DottaandClaude Opus 5.5 34f71f7d57 fix(server): defer a run that meets a busy agent directory instead of failing it (#20829)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Each run of an agent with managed agent files gets a private copy of
the agent directory. Remote copies serialize their prepare, checkpoint,
collect, and release operations through one Postgres advisory lock per
agent, taken with `pg_try_advisory_xact_lock`
> - When the lock is busy, the operation fails at once with "Agent
directory operation is already active"
> - On staging, a deploy restarted the controller while three runs of
the same agent were active. When one run resumed, another run of that
agent was checkpointing its directory. The resume failed, used its last
attempt, and the task became blocked with "Retry or send a message"
> - A run must not fail because another run of the same agent is briefly
using the agent directory
> - This pull request keeps the fail-fast lock and, when it is busy
during preparation, puts the same native run back on the existing
same-run retry schedule a few seconds later
> - The benefit is that lock contention delays a run by seconds instead
of ending it as failed

## Linked Issues or Issue Description

**What happened?**
A task's run failed with `native_continuation_requires_reconciliation`
and the error "Agent directory operation is already active". The task
was blocked and asked the user to retry. The same error ended at least
two other runs of the same agent within one day.

**Steps to reproduce**
1. Use an agent with managed agent files in a remote sandbox.
2. Let one run of the agent checkpoint or collect its agent directory.
3. At the same time, let another run of the same agent prepare (for
example, a same-run resume after a controller restart).
4. The second run fails immediately with "Agent directory operation is
already active".

**Expected behavior**
The second run waits until the first operation is done, then continues.
It does not end as failed.

## What Changed

- `server/src/services/agent-directory-working-copies.ts`: The busy lock
now throws `AgentDirectoryOperationBusyError`, still a 409 with the same
message. Lock behavior is unchanged from `master`: a fail-fast
`pg_try_advisory_xact_lock` held for the life of the holder's
transaction.
- `server/src/services/heartbeat/agent-directory-busy.ts`:
`prepareWithAgentDirectoryDeferral` wraps only instruction preparation
and turns the busy error into `AgentDirectoryPreparationDeferral`
("Another run of this agent is still using its files"). A busy lock met
while collecting files after a turn is not wrapped and keeps its
existing path.
- **Before native admission** (a brand-new run, such as the first run of
a task): `server/src/services/heartbeat/retries.ts`
`finalizeAgentDirectoryBusyDeferral` cancels the run as an expected
control-plane wait (`providerWorkStarted: false`) and schedules an
`agent_directory_busy` retry 5–15 seconds later. The task keeps its
execution lock on the retry. The lane spends no failure retries
(`execution-recovery-attempt.ts`), its count is the retry run's
`scheduledRetryAttempt`, and it stops after 30 attempts.
`pre-dispatch-execution-evidence.ts` classifies this wait like the
existing workspace and AI-connection waits, so retry scheduling does not
ask for reconciliation.
- **A resumed native run**:
`server/src/services/native-runtime/native-controller-recovery.ts`
`deferNativeRunForBusyAgentDirectory` keeps the same run on the existing
same-run retry schedule without spending an attempt. It uses the same
guards as `deferNativeControllerRecovery`: it locks the task, requires
the task's execution run to be this run, requires the current lease
owner, and refuses after a Stop, a startup cancellation or a pending
native cancellation, so those settle normally. Its count is stored in
the run's `contextSnapshot`, which a resume claim does not clear, and it
stops after 30.
- `server/src/services/heartbeat.ts`: routes a preparation deferral to
the same-run path or, before admission, to the scheduled-retry path.
After the bound it follows the normal failure path with the plain
message.
- No schema or migration. Postgres releases a dead holder's advisory
lock with its session. The staging evidence showed a live holder
(another run's checkpoint), not a stale one.
- `doc/DEVELOPING.md`: Documented both paths and the bound.

Earlier revisions of this PR waited on the lock and added idle-session
expiry, keepalive, ownership fences, fenced remote removal and attempt
slots. This revision removes all of them.

### How the directory-wait retry keeps the original wake

The retry no longer replaces the wake. `scheduleBoundedRetryForRun`
takes `preserveWakeContext` for this wait: the retry keeps the original
`wakeReason`, `retryReason` and the whole wake context (interaction,
comment, continuation and admission fields), and its wake request keeps
the original reason. The wait is recorded separately as
`preparationWait: { kind: "agent_directory_busy", attempt, nextAttemptAt
}`, next to the carried `executionRetryAccounting`. Gates that key on
the wake reason therefore treat the retry exactly as they treated the
original wake.

| # | Site | Treatment for the directory wait | Why |
| --- | --- | --- | --- |
| 1 | `heartbeat/retries.ts` `scheduleBoundedRetryForRun` | Changed:
`preserveWakeContext` keeps the original wake context and reason and
adds `preparationWait` | Root cause: the retry overwrote the wake reason
|
| 2 | `run-dispatch/domain/wake-context.ts`
`isResolvedInteractionContinuationWakeContext` /
`PREPARATION_WAIT_RETRY_REASONS` | Special case removed | The original
wake context is kept, so the continuation is classified from it |
| 3 | `heartbeat/retries.ts` `interactionRunnerWait` | Special case
removed | The interaction fields are kept in the context |
| 4 | `run-dispatch/adapters/postgres.ts` scheduled-retry dependency
facts | Changed: a preparation-wait retry whose original wake was
admitted as a dependency-blocked interaction
(`dependencyBlockedInteraction`, recorded at wake time) is admitted the
same way | Without the wait the original wake would have run; this gate
is only met because of the wait |
| 5 | `run-dispatch/domain/wake-context.ts`
`isNonAssigneeWorkspaceBusyRetry` + `heartbeat/retries.ts`
`finalizeAgentDirectoryBusyDeferral` | Kept:
`preparationWaitDeferredWhileAssignee`, derived only from the recorded
admission authority | The scheduled-retry ownership gate has no
wake-reason path; the workspace and AI waits use the same recorded flag
|
| 6 | `heartbeat/retries.ts` explicit user continuation admission |
Kept: admits the `agent_directory_busy` scheduling reason | The
successor needs its own authorization receipt |
| 7 | `execution-recovery-attempt.ts` uncharged lane and
`agentDirectoryWaits` | Kept | Cap and accounting |
| 8 | `pre-dispatch-execution-evidence.ts` (`isPreDispatchResourceWait`,
`isAgentDirectoryPreparationWait`), used by
`legacy-execution-recovery.ts`, `cancelled-native-startup.ts`,
`explicit-native-continuation.ts` | Kept | Describe the cancelled
pre-provider parent run, not the retry's wake |
| 9 | `explicit-native-continuation.ts`
`undeliveredLegacyUserCommentIds`, `run-dispatch/adapters/postgres.ts`
`unstartedUserTurnOrigin` | Kept | The cancelled parent did not consume
the user's comment |
| 10 | `execution-projection.ts` label and saved accounting,
`task-interruption-presentation.ts`, UI toast and chat suppression |
Kept | Presentation of the wait |
| 11 | `run-dispatch/domain/policy.ts` AI-wait lock requirement;
AI-subscription lock recheck; AI wait detection
(`execution-configuration.ts`, `provider-limit-wait.ts`, `heartbeat.ts`,
`ai-connections.ts`, `ai-connection-runtime.ts`, `local-ai-login.ts`) |
Not applicable | AI-subscription specific |
| 12 | `workspace-busy-wait.ts`, `environment-selection.ts`,
`workspace-preparation.ts`, UI workspace-holder banners | Not applicable
| Workspace holder parking; the directory wait is a timed retry |
| 13 | `cancelled-native-startup.ts` paused workspace-retry repair;
`failureRetriesBefore*Wait` | Not applicable | Historical workspace and
AI records only; every directory wait saves its ledger |
| 14 | `failedRunRetryBudgetSpent`; storage-wait exhaustion escalation;
`status-relevance.ts` `isAdmissionWait` | Not applicable | A directory
wait never fails the run, stops at its own bound, and its run has
started |

## Verification

- Original wake kinds after a directory wait
(`heartbeat-agent-directory-busy.test.ts`, real heartbeat): an
assignment runs; a plain user comment on a dependency-blocked task is
answered with `dependencyBlockedInteraction`; an accepted interaction
continuation keeps its parked-task and reassignment gates; a user
message continuation runs with its comment; an accepted non-assignee
mention runs; and a reassignment during preparation does not let the
previous assignee restart. Each retry keeps the original `wakeReason`
and records `preparationWait`. With `preserveWakeContext` off, five of
these fail. Restart recovery keeps its attempt credit
(`native-runner-restart-recovery.integration.test.ts`).

- Non-assignee wake: an accepted mention for an agent other than the
assignee meets a busy directory; its retry records
`preparationWaitDeferredWhileAssignee: false`, survives promotion, runs
and succeeds once the directory is free. Without the shared clause in
`isNonAssigneeWorkspaceBusyRetry`, promotion cancels it.

- Interaction continuations: `heartbeat-agent-directory-busy.test.ts`
runs an accepted interaction continuation into a busy directory. The
retry keeps `mutation`, `interactionId`, `interactionKind` and
`interactionStatus` and is still classified as a continuation. If the
task is moved to todo, or reassigned, while the retry waits, the retry
does not run and the task stays parked or with its new owner. With the
old explicit storage/Runner lists, both cases fail.

- Red/green, new run:
`server/src/__tests__/heartbeat-agent-directory-busy.test.ts` runs a
real heartbeat for a new assignment whose first instruction preparation
meets a busy lock. Without the preparation wrapper, the run ends failed
(red). With it, the first run is cancelled as a wait, an
`agent_directory_busy` retry is scheduled with the task still locked to
it, the retry runs and succeeds, and no run of the task is failed
(green).
- Red/green, same-run: with the deferral as a no-op, the same-run
deferral tests failed (`expected false to be true`); they now pass.
- `native-controller-recovery.integration.test.ts` covers repeated
claim-and-defer cycles of a resumed run: each cycle keeps the attempt
count, cannot be claimed before the delay, stores the count on the run,
and the fourth deferral past a bound of 3 is refused. It also covers
Stop after the busy error, a foreign lease owner and a moved task (each
refused, nothing changed), a run before admission, and that a raw busy
error (as from collection) is never deferred.
- Focused suites: 232/232 passed (`agent-directory-working-copies`,
`instruction-working-copy`, `heartbeat-agent-directory-busy`,
`native-controller-recovery.integration`, `heartbeat-retry-scheduling`).
`tsc --noEmit` for `server` reports no errors in the changed files.
- Staging evidence (read-only): the failed run had a graceful restart at
07:25Z, then same-run resumes. Its third attempt failed at 07:31:08Z.
Another run of the same agent recorded a warm checkpoint receipt at
07:31:37Z. A second run of the same agent failed the same way at
07:59:46Z.

## Risks

- A deferred run waits 5 to 15 seconds per deferral, up to 30 times. If
the holder is still busy after that, the run fails as before with a
plain message.
- Before admission, deferral cancels the run and creates a retry run,
like the existing workspace-busy 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-11 13:56:45 -06:00
DottaandClaude Fable 5.2 a56b648602 fix(upgrade-readiness): report restartable runs and a handoff verdict that blocks only on lost work (#20910)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A deployer can replace the server while agent runs continue. Before
the swap, it reads the upgrade-readiness report of the old server
> - The version 1 report is ready only when every active run is
adoptable and no wake admission is in flight
> - On a busy instance, new runs arrive each minute. Some runs are
always in preparation, so the report is almost never ready and the
instance does not upgrade
> - A run in preparation has done no agent work. Restart recovery starts
it again. A swap loses nothing
> - This pull request adds a `handoff` object to the report. It
separates recoverable, restartable and blocking runs
> - The benefit is that a deployer can wait only for work that a swap
would lose

## Linked Issues or Issue Description

Refs #20852 (reference only; this change does not depend on it and does
not use its version 2 shape).

**What happened?**

A busy instance had 16 running runs and 1 queued run. Fourteen had a
live process. Two or three native runs had not launched a process yet.
One was a legacy run. `upgradeReadiness.ready` needs `blockingRuns === 0
&& pendingWakes === 0` in one probe. That state almost never occurs, so
the deployer never swaps the server.

**Expected behavior**

The report tells the deployer which runs a swap would lose. Runs that
only wait for a launch, and wake admissions in flight, do not block a
graceful replacement.

**Steps to reproduce**

1. Keep a steady flow of new runs on an instance.
2. Read `GET /api/instance/task-drain?upgradeReadiness=1` repeatedly.
3. See `ready: false` each time, because one or more runs are in
preparation or a wake admission is in flight.

## What Changed

- `summarizeUpgradeReadiness` returns an additive `handoff` object:
`version`, `ready`, `recoverableRuns`, `restartableRuns`,
`blockingRuns`, `pendingWakes`, `unsavedWakes`.
- `handoff.ready` is true when the instance is not draining, the census
is complete, and `handoff.blockingRuns` and `handoff.unsavedWakes` are
0. Saved pending wakes do not change it.
- The wake queue tracks each public wake admission until its wake
request and queued run commit. `unsavedWakes` is the count of admissions
before that commit. Without a valid count, every pending wake is
unsaved.
- The top-level version 1 fields keep their values and meaning. `ready`
and `blockingRuns` are unchanged, so a current consumer sees no looser
verdict.
- The census query marks a run `restartable` only with all of these:
`paperclip_runner` adapter; no process identity; no provider session
before or after; no checkpoint header; no dispatched runner task; no
execution-start event. It must also be a native run in the `observed`
phase with preparation fencing and an unsettled finalization record, or
a legacy run in the `preparing` stage under a recorded controller.
- All other runs without adoption evidence stay blocking. This includes
local adapters, legacy runs with a child process, follow-up runs that
carry a provider session, and runs the census could not observe.
- The OpenAPI description and `doc/testing-native-remote-continuity.md`
describe the new fields and their limits.

Why these classes are correct on master:

- Restartable native runs: restart recovery claims a run with no
checkpoint and no provider evidence as `bootstrap_incomplete` and
executes the same run again (`native-restart-recovery.ts`
`classifyNativeRunnerRecoveryEvidence`; `heartbeat/recovery.ts` executes
each claim). A graceful shutdown suspends the run and notifies recovery.
Existing tests: "recovers an unclaimed remote startup once without
requiring a provider checkpoint" and "recovers an unclaimed remote
startup that a graceful shutdown suspended during preparation".
- Restartable legacy runs: process-loss recovery retries a `preparing`
run with `providerWorkStarted: false`. Existing test: "continues a chain
whose preparations were killed twice and then interrupted again, without
a Retry".
- Pending wakes: the count is wake admissions in flight in the process,
not queue rows. A wake commits its wake request and its queued run in
one transaction. After the commit, the next server dispatches the queued
run at startup, so a saved wake does not block. Before the commit,
nothing is saved. Shutdown waits at most 30 seconds and then exits, so
an unsaved wake can be lost and blocks.

## Verification

- `cd server && npx vitest run
src/services/heartbeat/upgrade-readiness.test.ts
src/services/heartbeat/upgrade-readiness-post-result.test.ts`: 28
passed.
- Red/green: before the implementation, the 7 new tests failed (6
summary tests, 1 PostgreSQL census test). After it, all pass.
- Red/green for unsaved wakes: a test holds the agent row lock while a
wake admission waits. Without the fix, `handoff.ready` was true with no
saved wake. With the fix, it is false with `unsavedWakes: 1`, and the
wake saves after the lock releases.
- New cases: only recoverable runs; recoverable plus unlaunched runs
plus pending wakes (`handoff.ready` true, top-level `ready` false and
`blockingRuns` unchanged); a started run without recovery evidence; an
unobserved execution; draining; truncated census.
- The PostgreSQL test changes one fact at a time (process identity,
provider session, phase, fencing, checkpoint, adapter, each
execution-start event) and expects the run to become blocking.
- `cd server && npx vitest run src/__tests__/openapi-routes.test.ts
src/__tests__/instance-settings-routes.test.ts`: 157 passed (187 with
the two readiness files).
- `cd server && npx tsc --noEmit`: passed.
- `pnpm verify` exited 2 (a documentation path is a global CI input).
Required CI supplies the broad coverage.
- Not run: a live server replacement. The classification relies on the
existing recovery tests named above.

## Risks

- Low risk for current consumers. The change is additive and the version
1 fields are unchanged.
- `handoff.ready` is an observation at one instant. A wake admission
that starts after the probe is unsaved until it commits.
- Restart recovery applies its own proofs and a bounded attempt budget
before it starts a restartable run again. The report is an observation,
not a guarantee for each run.
- The probe adds one indexed `not exists` on run events per running row,
inside the existing two-second statement timeout.

> 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 Fable 5.2 (`claude-fable-5-2`), 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 Fable 5.2 <noreply@anthropic.com>
2026-11-11 13:55:47 -06:00
DottaandClaude Opus 5.5 fb695f7ae6 fix(secrets): let an approved binding fill a blank agent config value (#20908)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents can propose binding a company secret to another agent; a
person approves it with "Create binding" on a chat card
> - Approval refuses when the target config key already holds a
different value, so it never overwrites a working credential
> - An empty string or empty plain value also counted as "already
exists"
> - So an agent whose key was cleared could not be repaired through the
governed binding card: approval returned 409
> - This pull request treats a blank value like an unset key; non-empty
values still conflict
> - The benefit is that the supported binding approval repairs a member
with a blank credential

## Linked Issues or Issue Description

Refs #20879

**What happened**
In the `teams.configure-team` chat eval, the CEO proposed binding the
company OpenRouter secret to the QA member as `env.OPENROUTER_API_KEY`.
QA's saved value was an empty string. Approving that proposal fails with
409 "Agent config path already exists: env.OPENROUTER_API_KEY".

**Expected behavior**
Approval fills a blank value (empty string or empty plain value) with
the secret reference. A non-empty value still blocks the approval.

**Steps to reproduce**
1. Save an agent with `adapterConfig.env.TOKEN = ""`.
2. Have an agent propose a binding of a company secret to that agent as
`env.TOKEN`.
3. Approve the proposal as the board user.
4. The approval returns 409 and the binding is not created.

## What Changed

- `secret-proposals.ts`: `applyBindingApproval` treats `undefined`, `""`
and `{ type: "plain", value: "" }` as an unset key for both `env.<KEY>`
and other config paths.
- `secret-proposals-routes.test.ts`: regression tests for an empty
string and an empty plain value. The existing test that a non-empty
value conflicts still passes.

## Verification

- `pnpm exec vitest run src/__tests__/secret-proposals-routes.test.ts`
(in `server/`): 49 passed.
- Red check: without the service change, the two new tests fail.

## Risks

- Low risk. Only a blank value changes behavior. A blank value carries
no credential, so nothing working is replaced.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), 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-11 13:55:22 -06:00
DottaandClaude Opus 5.5 7e8901aa60 fix(recovery): let Retry and the person's answer continue a held task with a pending decision (#20872)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A native run on a remote sandbox can lose its Runner process.
Recovery then saves the sandbox files and puts an execution hold on the
task
> - The hold offers Retry. A person can also continue the task with a
new message
> - Many held tasks also have a pending decision: the question that a
parked turn asked, or an older connection request
> - Continuation admission refuses every continuation while a decision
is pending ("decision_pending"). The answer to that decision is not a
message, so admission defers it behind the hold ("execution_recovery")
> - So Retry waits for the answer and the answer waits for the hold. The
task shows a Retry that does nothing, and nothing continues it
> - This pull request lets a person's Retry pass an open question, and
lets the person's own answer continue the task through the hold
> - The benefit is that a task with a lost Runner and a pending decision
always has a control that works

## Linked Issues or Issue Description

**What happened?**
On staging, the same-run retry of six remote native runs found the
Runner process gone (`process_not_running`, lifecycle `ready`). Each run
failed with `runner_remote_process_identity_unavailable`. Saved-work
recovery then recovered the sandbox files, and the task showed "Saved
work recovered. Retry or send a message to continue." with `canRetry:
true`. Four of these tasks also had a pending decision: the question
that the parked turn asked, or an older pending connection request. For
those tasks, Retry is deferred with `decision_pending`, and an answer to
the decision is deferred with `execution_recovery`. No control continues
the task.

**What did you expect to happen?**
Retry starts one new run. The person's answer to the pending decision
also continues the task.

**Steps to reproduce**
1. A remote native run asks the user a question and parks the turn.
2. The Runner process exits. The same-run retry fails with
`runner_remote_process_identity_unavailable`.
3. Saved-work recovery recovers the files. The task offers Retry.
4. Click Retry: the Retry waits with "A pending approval or question
must be resolved before this message can start."
5. Answer the question: the answer waits with "Waiting for execution
recovery. Your message is saved."

Refs #20838, #20853

## What Changed

- `admitExplicitNativeContinuation`
(`server/src/services/explicit-native-continuation.ts`): a task Retry is
no longer held by a plain pending question (`ask_user_questions`) or
connection request (`connection_intent`). Any interaction with approval
semantics still holds Retry: a plan review (`issue_document` target), a
governed tool action, a secret proposal, a connection authorization, and
other confirmations. A pending approval still holds every continuation.
A new message supersedes a plain question, as on master, and waits for a
decision with approval semantics.
- `admitExplicitNativeContinuation`: the person's own resolution of an
interaction (`mutation: "interaction"`, answered/accepted/rejected,
resolved by the same user) is explicit user input. It continues the held
task like a new message: a fresh session, the hold resolved as
`explicitUserContinuation` with the `interactionId`, and
`issue.execution_recovery_settled` activity. It never replays the failed
run. Governed tool-action confirmations are excluded. The existing
`readQueuedInteractionResponse` reads the answer.
- `heartbeat/queue.ts`: both admission calls pass the wake payload to
the admission.
- `buildExecutionContinuation`
(`server/src/services/execution-continuation.ts`): dispatch rechecks the
user authorization. It accepted an interaction receipt only through a
saved interrupt queue, so the answered successor failed with
`continuation_user_authorization_missing`. It now also accepts the
person's own answer: the interaction named by the durable authorization
was resolved by that user and not by an agent, and the authorization
names this successor run. The check does not depend on the run's current
trigger, so a later answer that replaces the trigger cannot invalidate
it.
- New API-level test
`server/src/services/governed-wait-runner-loss.test.ts` with the two
staging shapes (a parked question from the failed run; an older
connection request on a monitor wake). It checks that Retry starts
exactly one run and leaves the decision pending, that the person's
answer starts exactly one run and settles the hold, and that an answer
resolved by a different user keeps the hold.
- `doc/execution-semantics.md`: describes the rule.

## Verification

- `cd server && pnpm exec vitest run
src/services/governed-wait-runner-loss.test.ts`: 5 passed. The answer
tests now also build the successor's continuation as dispatch does and
check that the agent receives the answer.
- Red for dispatch (first head, ac17aebe): both answer tests fail with
`continuation_user_authorization_missing`.
- Ponytail follow-ups: a two-answer test (the second answer waits behind
the successor; dispatch also holds with a replaced trigger) and 'keeps
Retry on hold while %s is pending' for four approval-bearing kinds. On
head 60beb773, 5 of these fail. Two more tests show that a new message
supersedes a plain question and waits for a plan review (both unchanged
from master). All 12 tests in the file pass now. The PR adds no code
comments; the rationale is in `doc/execution-semantics.md` and here.
- Continuation suites (execution-continuation,
execution-continuation-provenance, legacy-continuation-authority,
explicit-native-continuation, task-retry, governed-wait-runner-loss):
634 passed.
- Red on master (product hunks removed, test kept): 4 of 5 fail. Retry
tests: `successors: 0`, deferred `decision_pending`. Answer tests:
`successors: 0`, deferred `execution_recovery`. Every case asserts
`runError: "runner_remote_process_identity_unavailable"`.
- Related suites: explicit-native-continuation, task-retry,
workspace-recovery-admission, retry-workspace-continuity,
workspace-recovery-retry-copyback, workspace-recovery-fallback,
native-workspace-reconstruction, native-recovery-preparation-failure,
issue-thread-interactions-service. 957 of 959 passed. The 2 failures are
30 s `afterEach` hook timeouts in native-workspace-reconstruction; all
51 of its tests pass alone, and the same file on unchanged master shows
5 hook timeouts on this machine.
- `cd server && pnpm exec tsc --noEmit -p tsconfig.json`: passes.

## Risks

- Medium. A Retry can now start while a plain question or connection
request is pending; decisions with approval semantics still hold it. The
new run sees the open decision in its task context, and the decision's
answer still wakes the agent later. Approvals keep holding Retry, so
approval gates are unchanged.
- An answer now continues a held task in a fresh session. All other
admission gates still run: stop proof, saved-work recovery, workspace
repair, active execution, pause, budget and ownership. Only the user who
resolved the interaction can continue this way.
- This does not make recovery automatic. A task whose Runner was lost
still rests on the hold until a person clicks Retry or answers.
Automatic settlement of a parked governed wait is a separate change.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), with tool use and code execution
in an agent harness.

## 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-11 13:42:36 -06:00
Nicky Leach 25f0957c24 Select the AI Connection sign-in computer automatically (#20906)
## Thinking Path

> - Paperclip manages agents and their AI Connections.
> - Codex and Claude subscription sign-in can run on a company agent
computer.
> - Core stores the credential and uses connection permissions to
control access.
> - The Connections page asks the user to choose a computer just to host
sign-in.
> - This change lets Core choose an eligible computer when sign-in
starts.

## Linked Issues or Issue Description

Refs #20410 and #20417.

**What existing behavior does this improve?**

OpenAI and Anthropic subscription sign-in from Connections with an agent
computer as the sign-in environment.

**Current behavior**

The user must choose a sign-in computer before the provider flow can
start.

**Proposed behavior**

Start sign-in from the provider form. Core chooses a computer in the
company and sign-in environment. The session stays bound to that
computer and reports when no eligible computer is available.

**Reason and benefit**

The saved credential and its permissions are independent of the computer
that hosts sign-in, so users can proceed without picking an agent.

## What Changed

- Select an eligible computer when a managed Codex or Claude login
starts without an explicit target. Prefer ready computers and skip
active login leases.
- Remove the picker and let the browser resume the existing session by
environment and connection intent. Continue to honor explicit agent
targets in agent settings. Remove the unused picker fallback from the
legacy form.
- Update the AI Connections guide, the plugin environment contract, and
the OpenAPI descriptions.

## Verification

- Passed server and UI typechecks.
- Passed 317 tests across the login environment, setup-token route,
device-login service, and two UI suites. The Postgres test covers
eligibility, inherited environments, explicit targeting, occupied
computers, and the no-computer response.
- Passed 45 OpenAPI route tests.
- Reran the affected UI suites on the final revision. All 230 tests
passed.
- UI tests cover starting from the provider form and resuming both
provider flows with a server-selected target.
-
[CI](https://github.com/paperclipai/paperclip/actions/runs/43086849302)
and [Greptile
5/5](https://github.com/paperclipai/paperclip/pull/20906#issuecomment-6805135968)
passed on the final commit.
- Local verification used test transports. Live provider sign-in remains
to be checked in a Cloud composition that contains this change.

## Risks

- Two sign-ins that select a computer before either lease is recorded
can contend for the same computer. The existing provider admission guard
handles the conflict. A retry can select another computer once the lease
is visible.
- The computer selection uses existing lifecycle and lease data. Plugin
admission still validates the live computer.
- No schema migration or plugin protocol change is required.

## Model Used

OpenAI Codex (GPT-6), with shell execution and local test tooling.

## 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
2026-11-11 11:42:31 -08:00
DottaandClaude Opus 5.5 eb5ad4c920 fix(cases): say a Cases-disabled denial is final and name the Experimental setting (#20892)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Cases is an experimental feature; an instance admin turns it on in
Experimental settings
> - When Cases is off, every case and workflow route answers 403 "Cases
are disabled" with no other information
> - A chat agent asked to "open a customer case" could not tell whether
the denial was final or what to offer
> - In one eval run it created a project and a task in place of the
case; in another it spent most of its turn searching routes before it
replied
> - This pull request gives the denial a code and a final, actionable
remediation, like the board-only denial
> - The benefit is a fast, truthful reply that names the setting and
lets the person choose

## Linked Issues or Issue Description

**What happened**
In the chat eval cases.cases-disabled ("Open a customer case for this
bug." with Cases off), the agent got 403 "Cases are disabled". In one
run it then created a project and a bug task without asking. In a later
run it searched many routes and instance settings for about 100 seconds
before it replied.

**Expected behavior**
The denial says that Cases is off, that the denial is final, where an
instance admin turns it on, and that the agent must not create a
substitute record before the person chooses. The agent replies at once
and offers the setting or an ordinary task.

**Steps to reproduce**
1. Turn off Cases in Experimental settings.
2. As an agent, call GET /api/companies/{companyId}/cases.
3. See a bare 403 "Cases are disabled" with no code or remediation.

## What Changed

- Rebased on #20894, which already gave the case routes a
`cases_disabled` code, a final remediation and the hidden-settings
check. This PR moves that remediation into one helper used by every case
and workflow path, and adds: no substitute task or project before the
person chooses, and the offer of an ordinary task.
- New `casesDisabledError()` in `server/src/services/cases-disabled.ts`:
same 403 message, plus `code: "cases_disabled"` and a remediation. The
remediation says the denial is final, forbids a substitute task or
project before the person chooses, links `[Experimental
settings](/<prefix>/company/settings/instance/experimental)`, and offers
an ordinary task.
- Native tool results carry only the error message, so the native case
tools, workflow tools and runner tool authority inline the remediation
in the message (`casesDisabledError({ inlineRemediation: true })`).
- Case routes, workflow service, case handoffs, native case tools,
native workflow tools and the runner tool authority use the helper
instead of a bare `forbidden("Cases are disabled")`.
- Tests: `cases-routes.test.ts` checks the code and remediation;
`native-case-tools.integration.test.ts` checks the native message
carries the remediation (red on master, green here).

## Verification

- `npx vitest run src/__tests__/cases-routes.test.ts
src/services/case-handoffs.test.ts
src/services/native-runtime/native-case-tools.integration.test.ts
src/__tests__/heartbeat-case-handoff.test.ts` (166 passed).
- The cases.cases-disabled chat eval is rerun on a temporary branch;
results are recorded in the PR conversation.

## Risks

- Low risk. The status and message do not change. The error body adds
`code` and `remediation`.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), 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-11 13:36:08 -06:00
DottaandClaude Opus 5.5 6dc36700b1 fix(costs): mark unpriced usage in the agent, model and project cost reports (#20889)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Cost reports show what agents and projects spent
> - A run can have usage with no known price; Paperclip saves it with
costStatus "unpriced" and costCents 0
> - The summary and the provider, biller and user reports count those
events, but the agent, agent-model and project reports did not
> - An agent that read the by-agent-model report saw a native run at
costCents 0 and told the person it cost $0.00
> - This pull request adds unpricedEventCount to those three reports and
tells agents what it means
> - The benefit is that missing charges are reported as unpriced, not as
free

## Linked Issues or Issue Description

**What happened**
In the chat eval budgets-costs.spend-breakdown, the person asked "How
much did Launch cost this week, broken down by agent?". The week
included a real native run whose usage had no known price (data-quality
reason missing_price). The agent read /costs/by-agent-model and
/costs/by-project. Both returned costCents 0 for that usage and no
unpriced count. The reply showed "1 native run ($0.00)" and gave the
total as complete.

**Expected behavior**
The reports say which rows include unpriced usage. The agent says that
usage is unpriced, marks the total incomplete, and links the record.

**Steps to reproduce**
1. Record one priced and one unpriced metered cost event for an agent in
a project.
2. Call GET /api/companies/{companyId}/costs/by-agent, /by-agent-model
and /by-project.
3. See costCents counts only the priced event, and no field shows the
unpriced event.

## What Changed

- `costService.byAgent`, `byAgentModel` and `byProject` return
`unpricedEventCount` (same filter as the summary: unpriced and not
subscription_included).
- Shared types `CostByAgent`, `CostByAgentModel` and `CostByProject` add
`unpricedEventCount`.
- The OpenAPI catalog entries for the cost reports that count unpriced
usage now say that costCents counts priced usage only, that unpriced
usage must be reported as unpriced (never $0.00), and that
/costs/data-quality lists those runs.
- Tests: embedded-Postgres service test (red on master, green here) and
an OpenAPI description test. A UI test fixture adds the new field.

## Verification

- `npx vitest run src/__tests__/costs-service.test.ts` (19 passed). The
new test fails with the master service.
- `npx vitest run src/__tests__/openapi-routes.test.ts
src/services/native-runtime/runner-api.test.ts` (2146 passed).
- `tsc --noEmit` for packages/shared and server.

## Risks

- Low risk. One added aggregate column per report and an added response
field. The UI does not read it yet.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), 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-11 13:36:03 -06:00
DottaandClaude Opus 5.5 77757a3a93 fix(evals): serve case-facts its own evidence; accept block-on-design's blocked-interaction reply (#20891)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The chat-feature eval catalog runs real chat journeys and grades the
saved product state
> - Two cases failed in eval run gha-43069861174 because of the eval,
not the product
> - `cases.update-case-facts` gave its investigator the wrong tool
content. `task-coordination.block-on-design` waited for a run count that
the product does not promise
> - A wrong fixture or a wrong wait hides real product results, so these
must be correct
> - This pull request gives the case-facts investigator the content its
case would really serve, and lets block-on-design accept the product's
bounded blocked-interaction reply
> - The benefit is that both cases test their real feature again, and
the checks still reject the failures that matter

## Linked Issues or Issue Description

**What happened**
`cases.update-case-facts`: the shared Website Brief app served the
website prerequisite text ("record a short implementation note and
complete the task") to the case investigator. The investigator said that
the text did not match the case. It then asked the board for the missing
evidence. The task went to `in_review`, so the harness's later board
handoff got a correct 422 ("Resolve task mode, blockers, or review
before handing off").
`task-coordination.block-on-design`: the CEO saved the Design blocker
and also commented on the blocked Website task. The product woke Morgan
in its bounded blocked-interaction mode (`dependencyBlockedInteraction:
true`). Morgan only acknowledged the hold. The pre-approval wait
required exactly one run, so it timed out.

**Expected behavior**
Each fixture gives the agent the facts its own scenario has. Each wait
accepts every behavior that the product contract allows.

**Steps to reproduce**
1. Run `chat-features.runner-opencode.local.cases.update-case-facts`.
Morgan's continuation reads the website brief and asks the board for
evidence. The review handoff gets a 422.
2. Run
`chat-features.runner-opencode.local.task-coordination.block-on-design`.
The CEO comments on Website. Morgan gets a blocked-interaction run. The
first wait times out.

## What Changed

- `startRunBriefGate` takes an optional `briefText`. The default text
does not change for the other cases.
- `cases.update-case-facts` serves "no further reproduction evidence has
been received yet; the affected device is still unconfirmed" after
release (oracle v7).
- `task-coordination.block-on-design` (oracle v8):
- The pre-approval wait accepts Morgan runs only when the server stamped
them as blocked interactions on Website that name the Design blocker,
and they succeeded. Any other Morgan run, a failed run, or a second CEO
run rejects the wait at once.
- New check: before approval, Website has no documents and its status is
not `in_progress`, `in_review` or `done`.
- After approval, exactly one Morgan work run must still produce the
saved result.
- Calibration tests: the valid interaction passes. An unstamped run, a
wrong blocker, a wrong task, a failed run, a missing declared run, a
document saved before approval, and a second Morgan work run each fail.

## Verification

- `pnpm exec vitest run --config tests/runner-e2e/vitest.config.ts
--testTimeout=180000
tests/runner-e2e/chat-catalog-domains/runtime-dependency.test.ts
tests/runner-e2e/chat-catalog-domains/integrator-case-facts.test.ts
tests/runner-e2e/chat-catalog-contract.test.ts
tests/runner-e2e/chat-catalog-consistency.test.ts`: 34 passed.
- `npx tsc --noEmit -p tests/runner-e2e`: no errors in the changed
files.
- `node tests/runner-e2e/source-sdk-public-inputs.mjs <HEAD> --check`:
unchanged.
- A paid eval run of the three affected cases is in progress. The PR
comments will have its result.

## Risks

- Low risk. Only eval fixtures, waits and graders change. The other
cases that use `startRunBriefGate` keep the same text.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), extended thinking, 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-11 13:32:27 -06:00
DottaandClaude Opus 5.5 951d1b0b1b fix(opencode): ask for the empty-response correction with reasoning off (#20890)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The Runner drives OpenCode turns for agents that use OpenCode
models, such as DeepSeek through OpenRouter
> - Sometimes the model ends a turn inside its reasoning. It reasons
that it will reply and call `paperclip_finish`, and then it stops with
no answer and no tool call
> - The driver sends one correction prompt. The correction used the same
reasoning variant, and in the failed runs the model stopped inside its
reasoning again
> - The run then failed with `opencode_empty_response`, and the person
saw "The OpenCode run failed before it completed."
> - This pull request sends the correction prompt with the no-reasoning
variant, so the final answer comes in the answer channel
> - The benefit is fewer failed chat replies, with no extra prompts and
no change to the normal turn

## Linked Issues or Issue Description

**What happened**
A chat reply failed with `opencode_empty_response` in eval run
gha-43069861174 (`agent-chat.conference-room`, OpenCode with
`openrouter/deepseek/deepseek-v4.1-flash-1017`). The first completion
and the one correction each held only a reasoning item (403 and 411
output tokens). Both reasoning items said the model would reply and call
`paperclip_finish`. No answer text and no tool call followed. In
retained eval evidence, 14 of 879 attempts needed this correction and 3
of those 14 failed. Each failed correction was another reasoning-only
completion.

**Expected behavior**
After an empty completion, the correction gets a final answer or a
completion report.

**Steps to reproduce**
1. Start an OpenCode turn with reasoning enabled on an OpenRouter model.
2. Let the provider return a completion that holds only reasoning.
3. See that the correction prompt uses `paperclip-default` (reasoning
on), so the model can end inside its reasoning again.

## What Changed

- `OpenCodeServerDriver` sends the empty-response correction with the
`paperclip-no-reasoning` variant. Normal turns keep their own reasoning
mode.
- `empty-response.test.ts` starts the `reply` and `empty-again` cases
with reasoning on. It checks that the first prompt uses
`paperclip-default` and the correction uses `paperclip-no-reasoning`.

## Verification

- `pnpm exec vitest run src/drivers/opencode/empty-response.test.ts
src/drivers/opencode/reasoning-mode.test.ts
src/drivers/opencode/interrupt-pending-tool-call.test.ts` (in
`packages/paperclip-runner`): 23 passed. Without the driver change, the
`reply` and `empty-again` cases fail (red/green).
- `node --test
tests/runner-e2e/native-completion-source-contract.test.mjs`: 29 passed.
- A paid eval run with this change and the related eval fixes is in
progress. The PR comments will have its result. One run cannot prove the
rate change, because the failure is rare.

## Risks

- Low risk. Only the correction prompt changes. A model with no
reasoning can give a shorter final answer, but the correction asks only
for the final answer and the completion report from work already done.
- Providers other than OpenRouter do not get a variant. Their behavior
does not change.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), extended thinking, 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-11 13:31:38 -06:00
DottaandClaude Opus 5.5 6154b9485e fix(authz): name the Experimental settings page when an agent is denied an instance experiment (#20894)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - People ask chat agents to turn on instance experiments, such as
Cases
> - Agents cannot read or change experimental instance settings, which
is correct
> - The denial they get names no page, so the agent cannot tell the
person where to act
> - The case API gate ("Cases are disabled") also names no page
> - This pull request makes both denials, and the API catalog entry,
name the Experimental settings page
> - The benefit is that the agent replies with a usable link instead of
a vague "ask an admin"

## Linked Issues or Issue Description

**What happened**
An agent that asks for `GET` or `PATCH
/api/instance/settings/experimental` gets the generic board-only denial.
That denial says "Link a page only if you already know its route; do not
search for one." The case API, while Cases is off, returns only "Cases
are disabled". The agent has no route to give the person.

**Expected behavior**
The denial is final and names the page where an instance admin turns the
feature on: `[Open Experimental
settings](/<prefix>/company/settings/instance/experimental)`. If the
operator hides that page, the denial does not link it.

**Steps to reproduce**
1. Use an agent API key.
2. Send `PATCH /api/instance/settings/experimental` with
`{"enableCases": true}`.
3. See a 403 with a remediation that names no page.
4. With Cases off, send `GET /api/companies/<id>/cases` with the agent
key.
5. See a 403 "Cases are disabled" with no remediation.

Refs the chat eval `instance-operations.instance-settings` (retargeted
from the removed Agent Chat toggle to Cases).

## What Changed

- `server/src/routes/authz.ts`: the board-only denial map links
Experimental settings for `/instance/settings/experimental`. It does not
link it when `instance.experimental` is hidden.
- `server/src/routes/cases.ts`: the Cases gate keeps the "Cases are
disabled" message. It adds `code: cases_disabled` and a final
remediation that names the same page (same hidden-page guard).
- `server/src/routes/openapi.ts`: the GET and PATCH catalog entries for
experimental settings name the page. They say that agent credentials
cannot read or change these settings. They give Cases (`enableCases`) as
an example.
- Regression tests in `board-access-denial.test.ts`,
`cases-routes.test.ts` and `openapi-routes.test.ts`.

## Verification

- `cd server && pnpm exec vitest run
src/__tests__/board-access-denial.test.ts
src/__tests__/openapi-routes.test.ts
src/__tests__/cases-routes.test.ts`: 107 passed.
- Red check: with the three source files reverted, 4 new assertions fail
(GET and PATCH denial links, the catalog entry, the Cases gate).
- `src/services/native-runtime/runner-api.test.ts` and
`src/__tests__/instance-settings-routes.test.ts`: pass.

## Risks

- Low risk. Only error bodies and catalog text change. Status codes and
the "Cases are disabled" message stay the same.
- The catalog text is static, so it names the page even when an operator
hides it. The denials check the hidden setting.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), 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-11 13:14:42 -06:00
DottaandClaude Opus 5.5 a589b74fbf fix(chat): name goal links, missing-name questions and component reference pages in chat guidance (#20888)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - People talk to their agents in chat, and the chat guidance tells the
agent how Paperclip works
> - Chat evals showed three gaps in that guidance
> - The agent explained a task's purpose from titles only, because the
guidance gives no goal link and no instruction to read goal descriptions
> - The agent built a full landing page with an invented product name,
because the chat guidance has no rule to ask for a missing name first
> - The agent posted a real question card or drew a text mock when the
person asked to see a card, because the guidance names the design guide
and ux-lab pages only in general
> - This pull request adds three short rules to the chat guidance
> - The benefit is grounded purpose answers with links, one necessary
question instead of placeholder work, and links to the real reference
pages

## Linked Issues or Issue Description

**What happened**
In chat evals (OpenCode runner, DeepSeek flash): (1) "Why are we doing
this website task?" got an answer from titles only, with no goal links
and without the parent goal's stated reason. (2) "Build a landing page
for my new product." produced a full site named with a placeholder; the
agent never asked for the product name. (3) "Show me the standard
question card" made the agent post a real question card. "Show me how
chat looks when a tool request is denied" made the agent draw a text
mock and probe real tool requests.

**Expected behavior**
(1) The agent reads the task, project, goal and parent goals, gives the
reason that the goal descriptions state, and links each record. (2) The
agent asks one free-text question for the missing name and uses the
answer in the brief. (3) The agent links the design guide question card
or the denial ux-lab page and says they are synthetic presentation
fixtures.

**Steps to reproduce**
1. Run the chat evals goals.explain-purpose,
questions-and-approvals.necessary-question,
developer-labs.component-reference and
developer-labs.interaction-fixture on master.
2. Read the reply and tool events of each run.

## What Changed

- Chat guidance: goal link format `/<prefix>/goals/<goalId>`, and a rule
to explain a task's purpose from its saved goal chain (GET
/api/goals/{id}, follow parentId), link every record, and say so when
the task has no goal.
- Chat guidance: ask for a missing name (for example the product name on
a landing page) with `request_human_input` (`questions`, one required
free-text question) before building or delegating; never use a
placeholder name.
- Chat guidance: when the person asks to see a component or chat state,
link the design guide (question card states) or the named `/ux-lab/...`
pages instead of creating a real card or a mock, and say they are
synthetic fixtures.
- Tests: new red/green tests in `agent-conversations.test.ts`; they also
check that the named UI routes exist in `ui/src/App.tsx`.

## Verification

- `npx vitest run src/__tests__/agent-conversations.test.ts` (62
passed); the new tests fail on master because the text is absent.
- `npx vitest run` for heartbeat-context-summary,
heartbeat-chat-task-link, board-access-denial, task-markdown,
current-wake-comments and heartbeat-list tests (119 passed).
- The affected chat evals are rerun on a temporary branch; results are
recorded in the PR conversation.

## Risks

- Low risk. Guidance text only. The chat directive grows by about 1.6
KB.
- The missing-name rule is narrow (a name the work must carry). It must
not cause extra questions for clear requests.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), 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-11 13:12:52 -06:00
DottaandClaude Opus 5.5 5c440d8889 test(evals): make scripted chat input name its referent (accept-one-proposal, share-private-task) (#20896)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The chat catalog evals check that agents in chat keep their user
promises
> - Two cases sent a scripted message whose referent was not clear in
the conversation
> - The agent then answered a different question, so the eval did not
test its promise
> - This pull request makes each scripted message name the thing it
refers to, without telling the agent what to do
> - It also replaces a reply keyword check with structural checks
> - The benefit is that the two cases test their real promises again

## Linked Issues or Issue Description

**What happened**

`questions-and-approvals.accept-one-proposal`: the person accepted a
"Create Acorn launch checklist" card. The CEO created the task once and
replied with a new offer: move the task into todo and execute it. The
eval then sent the same bare "Yes, go ahead with that." again. The CEO
read it as a yes to the new offer and moved the task to todo. An
assignment wake made a third run, over the declared bound of two.

`tasks.share-private-task`: the eval sent "Share this private task with
Riley." in the standing CEO chat. In that chat, "this" is the chat task
itself. The CEO tried to share its own chat task and never linked the
private task. The grader also matched reply words (Riley, owner/share
phrases).

**Expected behavior**

A repeated acceptance must refer to the same proposal, so that the eval
can show that one acceptance creates exactly one task. The share request
must name the private task, so that the eval can show that the agent
routes the owner to the task's sharing control and does not change its
audience.

**Steps to reproduce**

1. Run
`chat-features.runner-opencode.local.questions-and-approvals.accept-one-proposal`.
The CEO reply ends with a new offer, and the bare repeat accepts it.
2. Run `chat-features.runner-opencode.local.tasks.share-private-task`.
The CEO acts on its chat task, not on the private task.

## What Changed

- accept-one-proposal: the second human message names the accepted card
by its visible title (`Yes, go ahead with the "Create Acorn launch
checklist" request.`). The grader checks this exact scripted message.
All other checks stay: one task, still in backlog, one decision bound to
the first reply, two runs. Oracle version is now
`paperclip.chat-catalog.accept-one-proposal.v2`.
- share-private-task: the query is now `Share my private task "Private
launch task" with Riley.` The change is recorded in the manifest
`changedFields`, pinned in its test, and the inventory digest is
updated.
- share-private-task: the reply keyword check is removed. The grader
requires a rendered link to the named private task. `verifyResult` has
the owner open that link and open the task's Share control ("Who can
access this task"). Then it confirms that Riley is still denied. The
audience check also rejects a grant to any person who is not the owner.
Oracle version is now `paperclip.chat-catalog.share-private-task.v2`.
- Calibration tests: a bare repeat or a "move it to todo" repeat fails;
a todo task fails; reworded handoffs pass; a link to the chat instead of
the task fails; grants to Riley or to another person fail; public
visibility fails.

## Verification

- `pnpm exec vitest run --config tests/runner-e2e/vitest.config.ts` on
the case tests, contract, consistency and every chat-catalog-domains
test that imports the changed modules: 14 files, 285 tests pass.
- `node --test
tests/runner-e2e/chat-full-catalog-testops-manifest.test.mjs`: 10 pass.
- `node tests/runner-e2e/source-sdk-public-inputs.mjs <HEAD> --check`:
no change.
- `npx tsc --noEmit -p tests/runner-e2e`: no errors in the changed
files. Other errors come from the local worktree dependency links.
- Offline regrade of the retained share-private-task run: the run fails
the handoff check (it linked only the chat). The same run with a link to
the private task passes.
- Paid eval runs for both cases are pending.

## Risks

- Low risk. Only eval code, the scenario query and catalog metadata
change.
- The share-private-task grader no longer reads the reply. A false
success claim with a correct link and no saved grant is not caught by
code. A reviewer must read the reply in the paid run.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), tool use, extended
reasoning.

## 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-11 13:12:09 -06:00
DottaandClaude Opus 5.5 c02de922af fix(chat): deliver the person's time zone to chat agents (#20898)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - People talk to agents in chat, and agents create routines and other
scheduled work from that chat
> - Paperclip does not record the person's time zone, so a chat agent
cannot read "every weekday at nine" or "today" in the person's zone
> - The agent then saves schedules in UTC, and the person gets the work
at the wrong hour
> - This pull request saves the browser time zone with the person's
personal preferences and gives it to chat agents in the task prompt
> - The benefit is that schedules and dates from chat match the person's
local time

## Linked Issues or Issue Description

**What happened**
A person in America/Chicago wrote in chat: "Every weekday at nine in the
morning, summarize Launch for me." The agent saved the routine schedule
`0 9 * * 1-5` with the UTC time zone. It replied that it used UTC
because the person's time zone is not recorded.

**Expected behavior**
The agent knows the person's time zone. It saves one enabled `0 9 * *
1-5` schedule with the time zone America/Chicago. Its reply names the
zone.

**Steps to reproduce**
1. Open the board in a browser with the time zone America/Chicago.
2. Open a chat with an agent.
3. Send "Every weekday at nine in the morning, summarize Launch for me."
4. Look at the routine trigger that the agent creates. Its time zone is
UTC.

## What Changed

- Database: add the nullable `time_zone` column to
`user_workspace_preferences` (migration `0468`). This is an
instance-wide personal preference by user, as AGENTS.md section 5
permits.
- Shared: `WorkspacePreference` has `timeZone`.
`updateWorkspacePreferenceSchema` accepts an optional IANA `timeZone`
(max 64 characters, checked with `Intl.DateTimeFormat`).
`coordinatorWorkspaceEnabled` is now optional. The request must contain
at least one field.
- Server: the preference service returns `timeZone` and updates only the
fields in the request. The activity log entry stays the same. The
OpenAPI entry uses the shared schema.
- UI: a new `useBrowserTimeZoneSync` hook in `Layout` saves the browser
time zone one time when it is different from the saved zone. It is
silent and best-effort. It uses the selected company as the audit
context.
- Prompt: for a conversation (any adapter), the task context reads the
chat owner's saved zone. Recovery keeps the zone that the first turn
admitted. The chat block adds one line only when the zone is known:
`The person's time zone is America/Chicago (from their browser). Read
times and dates they give, such as "nine in the morning" or "today", in
that zone, and save schedules such as routine triggers with that
timezone; say the zone when you report a schedule.`

## Verification

- `cd server && npx vitest run
src/__tests__/workspace-preferences.test.ts
src/__tests__/workspace-preferences-routes.test.ts
src/services/heartbeat/task-markdown.test.ts
src/__tests__/heartbeat-task-context.test.ts
src/__tests__/openapi-routes.test.ts
src/services/native-runtime/runner-api.test.ts`
- `cd ui && npx vitest run src/hooks/useBrowserTimeZoneSync.test.tsx
src/hooks/useCoordinatorWorkspaceEnabled.test.tsx
src/components/Layout.test.tsx`
- Red/green: without the task-context and task-markdown changes, the two
new prompt tests fail. With the changes, they pass.
- `pnpm check:token-gates` is clean. `tsc --noEmit` passes for
`packages/shared` and `packages/db`.
- Manual: open the board in a browser with a non-UTC zone. Then ask a
chat agent to create a weekday routine at nine in the morning. The
trigger uses the browser zone.

## Risks

- Low risk. The migration adds one nullable column.
- The PUT body is now more flexible: `coordinatorWorkspaceEnabled` is
optional. An empty body still gets a 400 response.
- The prompt grows by one line only for conversations with a known zone.
- The saved zone changes when the person uses a browser in a different
zone. The latest browser wins.

> 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`), 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-11 13:11:57 -06:00
DottaandClaude Opus 5.5 5a332517da fix(evals): start the owned dev instance with a supported run command (#20885)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The chat eval catalog has a case that checks how chat handles a
request to set up a throwaway Paperclip instance
> - In this case, a scripted operator creates an owned development
instance with the supported CLI after the agent answers
> - The operator step always failed, so the eval never measured the
agent follow-up
> - The fixture started `paperclipai run` with an unsupported `--yes`
flag, and it discarded the child output, so the cause was not visible
> - This pull request starts the owned instance with a supported
command, keeps the child output for diagnosis, and replaces reply
keyword checks with structural checks
> - The benefit is a valid eval that measures the case promise, and a
readable cause if the operator step fails again

## Linked Issues or Issue Description

**What happened**
The `cli-operations.owned-dev-instance` chat eval failed every time with
"Stopped waiting for Actual owned development target readiness: Owned
dev server exited before readiness". The agent reply was correct. The
scripted operator phase failed. The fixture ignored the child stdio, so
the evidence did not contain the cause.

**Expected behavior**
The fixture starts the owned development instance with a supported CLI
command. The instance becomes healthy. The eval then grades cleanup,
isolation and the agent follow-up. If the child exits, the error names a
bounded and redacted reason.

**Steps to reproduce**
1. Start a disposable source instance with an embedded database.
2. Copy the checkout like the fixture does, then run `paperclipai
worktree init --instance X --home H --from-config <source config>
--server-port P --db-port D --seed-mode minimal`.
3. Run `paperclipai run --config <checkout>/.paperclip/config.json
--yes`. It exits 1 with `error: unknown option '--yes'`.
4. Run the same command without `--yes`. It exits with `TS2688: Cannot
find type definition file for 'node'` because the copied plugin SDK has
no linked `node_modules`.

## What Changed

- Remove `--yes` from the owned `paperclipai run` command. `run` has no
such flag. It onboards only when the config is missing, and `worktree
init` writes the config.
- Link the installed `node_modules` of every workspace package into the
copied checkout. Before, the fixture linked only six packages, so `run`
could not compile the copied plugin SDK.
- Write the owned CLI output to a private `0600` log under the attempt
temp root, outside the owned root that cleanup deletes.
- Add a bounded reason (last 12 lines, 1200 characters at most, terminal
codes removed, known secrets and secret shapes redacted) to readiness
and CLI failure errors.
- Replace the `operator-dev-guidance` reply keyword check with
`initial-chat-left-host-unchanged`. The check compares the digests of
every Paperclip installation config under the attempt root (agent HOME,
workspace and runtime directories) before and after the initial turn.
- Change `native-reports-real-target` to require only the exact
`host:port` that the human receipt supplied (bare, linked or with the
scheme). It no longer matches outcome words.
- Bump the case `oracleVersion` to
`paperclip.chat-catalog.privileged.owned-dev-instance.v2` in code and in
the descriptor.

## Verification

- Local reproduction with a disposable source instance: the old command
exits with `unknown option '--yes'`. Without `--yes` and with the old
links, `run` exits with TS2688. With this change, the owned target
reports healthy in 16 to 19 seconds. The seeded canary task reads back
from the target. `worktree:cleanup` removes the instance.
- `pnpm exec vitest run --config tests/runner-e2e/vitest.config.ts
tests/runner-e2e/chat-catalog-domains/privileged-operators.test.ts
tests/runner-e2e/chat-catalog-contract.test.ts
tests/runner-e2e/chat-catalog-consistency.test.ts`: 17 passed.
- Calibration: the retained CI reply and reworded receipt reports pass.
A changed host installation, a missing target, a longer port, a
different port, an unattributed reply and an empty initial reply fail.
- `node tests/runner-e2e/source-sdk-public-inputs.mjs <HEAD sha>
--check`: no change.
- `npx tsc --noEmit -p tests/runner-e2e`: no errors in the changed
files. Remaining errors come from a local dependency mismatch in
unrelated packages.
- The paid eval
`chat-features.runner-opencode.local.cli-operations.owned-dev-instance`
is not run yet.

## Risks

- Low risk. The change is in one eval fixture and its unit test.
- The installation inventory walks the attempt root. It skips
`node_modules` and `.git`, and it stops with an error after 1,000,000
entries.
- The initial-turn check does not grade the reply wording. A reply that
falsely claims to have created an instance, without host changes, needs
human review of the retained reply.

> 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`), agentic coding with
tool use and code execution.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 13:04:22 -06:00
DottaandClaude Opus 5.5 683fd1e9a5 test(restart-chaos): cells for staging's no-restart failures (re-wake, Nth recovery step, long turn, K15 without volume) (#20893)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart-chaos harness (`tests/restart-chaos/`) tests that agent
work survives control-plane faults
> - In one 8-hour staging window, 20 of 25 failed runs happened with no
restart at all, so the harness did not see them
> - The harness had blind spots: one short task per agent, no wake right
after a successful run, no fault in a later recovery step, short turns,
and a redeploy that always keeps its instance home
> - This pull request adds cells for those shapes, so the harness
reproduces these failure classes on master
> - The benefit is that fixes for these classes can be proven red and
green with the harness, without staging

## Linked Issues or Issue Description

**What happened?**
On staging, an agent ran 10–13 tasks at once on reused sandboxes. 30–60
s after a long run succeeded, an ordinary wake (a comment or an issue
monitor) started the next run of the same task. That run or its recovery
failed before any provider work, with errors such as
`runner_remote_process_identity_unavailable`, `Controller journal drain
timed out` and `journal_tool_input_unanswered`. Other failures happened
in a later recovery step (a held agent-directory lock, a missing runtime
asset after a deploy without a volume).

**Expected behavior**
The chaos harness can reproduce each of these shapes on demand, and
grades them: no failed attempt, every task `done`, and no task left
blocked without a working Retry.

**Steps to reproduce**
See the commands in the README sections "Re-wake after success", "Fail
the Nth recovery step", "Long turn" and "Redeploy without a volume, with
overlap".

Related: #20844 (session fault SR1). W2C is the configuration change
after a successful run; SR1 is the same change while a question is
parked. This PR does not add or change `session-faults.ts`.

## What Changed

- New workloads W2, W2Q, W2C (`rewake.ts`): phase 1 ends with the task
left open; the harness, as the board, wakes the same task the seeded
0–60 s after its run settled. W2 uses a comment or a board-scheduled
issue monitor (by nonce). W2Q parks on an unanswered
`request_human_input` question and is woken by a comment before the
answer. W2C changes the agent's `adapterConfig.idleTimeoutMs` before the
wake. Oracle: phase 1 succeeded, the wake was served by a run that
succeeded, `no_failed_attempt`, `done`, markers.
- `--faults K0`: cells with no fault, for workloads whose shape is the
fault.
- `--fail-recovery-step <step>[:<nth>]` (`recovery-step-faults.ts`):
when the wave's first restart fault starts its replacement, the harness
arms one crash point in a new fail mode; the nth hit throws the error of
the real step, once. Steps: `runnerd-start` (`spawn paperclip-runnerd
EAGAIN`), `teardown` (provider destroy did not finish), `directory-lock`
(`Agent directory operation is already active`). Crossed tasks get
`recovery_step_failed_once` and `no_failed_attempt`.
- Crash points (`packages/shared/src/crash-points.ts`): fail mode in the
control file (`<token> <name>:<nth> fail [pid=<n>]`), with an optional
call-site error; `pid=` binds the line to one server process. The
harness arms the step for the replacement only and disarms it once its
receipt lands. New point
`agent_directory.operation_lock.before_acquire`. The teardown point
moved into its `try`, so fail mode behaves like a failed provider
destroy; kill mode is unchanged. All points stay disarmed under
`NODE_ENV=production`. The runner package's own crash-point copy is not
changed (runner image content ID unchanged).
- `--long-turn-minutes <n>` (`long-turn.ts`, 15–60, opt-in): every
task's first turn runs rounds of real tool work for n minutes before its
workload. Checks `long_turn_rounds` and `long_turn_run_observed`.
- K15 `--k15-fresh-home` (Daytona) and `--k15-overlap-seconds
<min>-<max>` (`redeploy.ts`, `supervisor.ts`, `edge-proxy.ts`): each
deploy generation gets a new, empty instance home and no volume
variables; the next generation boots beside the old one behind an edge
proxy on the wave's port, takes traffic once ready, and the old one gets
SIGTERM after the overlap.
- `tests/runner-e2e/source-sdk-public-inputs.json`: refreshed pins for
the changed shared crash-point files.
- README: one section per new cell, with what it covers.

## Verification

- `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts`
(48 files, 581 tests after rebasing on master, includes the new
`shape-cells.test.ts`).
- `pnpm exec vitest run packages/shared/src/crash-points.test.ts
server/src/__tests__/crash-point-call-sites.test.ts` (fail mode, call
sites).
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`.
- Chaos waves on master `1d67e309e0` server code (Daytona image
`ghcr.io/paperclipai/paperclip-daytona-runner@sha256:99fc2671fc320816407f63cffd3acbd27471621febcd26494fe25bb5d7c5dd77`,
content ID `1836c515…`), all RED unless noted:

| Cell | Wave | Result on master | Staging class |
| --- | --- | --- | --- |
| W2/W2Q/W2C, 10 tasks on one Codex agent, `--daytona-warm`, K0 |
`w-20261111T163206-s5201` | 9/10 fail. 3 tasks (W2 t02, W2C t03, t06):
first fault `native_runner_process_exited`, same-run reattach,
`native.remote_runner_recovery_failed` with `stage:
process_verification`, `reason: process_not_running`, `runnerLifecycle:
ready`, `runnerSettled: false`, terminal
`runner_remote_process_identity_unavailable`, "Workspace repair
required". 3 tasks (t04, t08, t10): `native_restart_recovery_blocked`
(`native_retry_source_superseded`) while its successor ran (R4
`concurrent_runs`). 3 W2Q tasks: the answer's continuation was cancelled
`issue_not_in_progress` (status `blocked`), and the task needed Retry. 2
W2C tasks then looped: 60+ succeeded runs in 20 minutes on an
`in_progress` task. | D′/T terminal step reproduced with no restart, no
governed wait and no PRP timeout (same error code and refusal fields).
C+ retry outcome (ownership ambiguous) reproduced as
`native_restart_recovery_blocked`. Not reproduced: `Controller journal
drain timed out`, `did not durably suspend`,
`journal_tool_input_unanswered`. |
| Same, local `--lifecycle warm` | `w-20261111T160103-s5101` | 4/10
fail. 3/3 W2Q: continuation cancelled `issue_not_in_progress`, Retry
needed. 1 W2C: the same `in_progress` run loop (39+ runs). | The
remote-runner classes need a remote runner; locally they do not show. |
| `--fail-recovery-step runnerd-start:1`, K1 at 10 s, local A1 |
`w-20261111T162432-s5303` | Fail. After K1 the first runnerd start of
recovery failed (`spawn paperclip-runnerd EAGAIN`); the run ended
`native_provider_terminal_failed` ("previous turn is no longer active",
"Automatic recovery stopped") and needed Retry. Control, same seed and
K1 without the step fault (`w-20261111T171114-s5303`): pass. | A later
recovery step that fails once ends the run instead of trying again (the
A-chain shape). |
| `runnerd-start:2`, local | `w-20261111T170909-s5304` | Step never
reached: recovery started the runner once and succeeded. Grade fails
`recovery_step_failed_once` (tested nothing). | — |
| `teardown:1`, K1, Daytona A1 | `w-20261111T172048-s5312` | Pass. The
teardown failed once and the sweep settled it. | Not reproduced. |
| `directory-lock:1`, K2, Daytona A1 | `w-20261111T171247-s5311` | Step
never reached by A1 after K2. | Not reproduced; A1 recovery does not
collect remote agent files. |
| K15 `--k15-fresh-home --k15-overlap-seconds 30-150`, `--storage s3`,
Codex and OpenCode, T1/C1 | `w-20261111T173618-s5322` | 1/4 fail
(OpenCode T1): `native_continuation_requires_reconciliation` with "Agent
directory belongs to a different execution environment"; the Retry runs
failed `adapter_failed` "Configured instruction entry is missing"; the
task rests `blocked` on a board recovery action, and two sandboxes were
left (R6). One Codex T1 task finished before its fault
(`fault_crossed_tool`). | A-deploy: a controller-local file missing
after a deploy without a volume. Here the missing file is the agent's
instruction bundle, not a runtime-context asset; whether staging keeps
instruction bundles outside the instance home is not verified. |
| `--long-turn-minutes 15`, K1 at 600 s, local A1 |
`w-20261111T175505-s5401` | Task pass (10/10 rounds, 992 s span, run
1019 s, K1 crossed mid-turn). The wave exited 2 only because I switched
branches in the worktree during the wave. | Shape check only. |


## Risks

- Product code changes are test instrumentation only: three `crashPoint`
call sites take a fail-mode error, and one new call site was added.
Disarmed (always in production) each call is one comparison. Moving the
teardown point into its `try` does not change kill mode: the process
dies at the same instant.
- The new cells are opt-in. Default waves plan the same tasks as before
for the same seed.
- The W2 cells depend on model behaviour (phase 1 leaves the task open).
The oracle records the task status at the wake.
- `--k15-fresh-home` drops everything under the instance home. Whether
staging keeps agent instruction bundles outside the instance home is not
verified (see the K15 result).

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, agentic coding with
tool use (shell, file edits, test and chaos runs) 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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 12:58:41 -06:00
DottaandClaude Opus 5.5 b96967a97b fix(authz): name the environment settings page when an agent is denied an environment route (#20880)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Execution environments are the computers that agents run on. Only a
board user can list, inspect, test or change them.
> - An agent in chat can be asked "Which computer can run Morgan with
Claude Code?". Its first environment read returns 403.
> - The 403 says the denial is final, but it names no page. The agent
cannot link the person to the control that answers the question.
> - Other board-only areas (secrets, plugins, budgets) already name
their page in the denial.
> - This pull request adds the environment settings page to the agent
denial for every environment route, and says that testing and changing
computers are board-only too.
> - The benefit is that the agent can send the person straight to the
correct control, and it does not search for a probe route that does not
exist.

## Linked Issues or Issue Description

**What happened**

An agent asked which computer can run a coding harness called the
environment list or capabilities route. The route returned 403 "Board
access required" with a remediation that named no page ("Link a page
only if you already know its route; do not search for one"). The agent
had no route to give the person.

**Expected behavior**

The denial names the environment settings page as a Markdown link with
the `<prefix>` placeholder, like the denials for secrets and plugins. It
also says that the other environment steps (capabilities, probes,
changes) are board-only, so the agent replies instead of trying them.
When an operator hides environment settings
(`PAPERCLIP_HIDDEN_SETTINGS=instance.environments`), the denial does not
link the page.

**Steps to reproduce**

1. Use an agent API key.
2. Call `GET /api/companies/<companyId>/environments/capabilities`.
3. Read the 403 body. Before this change, `remediation` contains no
link.

## What Changed

- `server/src/routes/authz.ts`: board-only denials on
`/companies/:id/environments...`, `/environments/...` and
`/environment-leases/...` now link `[Open environment
settings](/<prefix>/company/settings/instance/environments)` and add one
sentence that says execution environments are board-only throughout. The
link is left out when the operator hides `instance.environments`.
`/companies/:id/project-environments` (agent-readable) is not matched.
- `server/src/__tests__/board-access-denial.test.ts`: regression tests
for the list, capabilities, probe and lease routes, the scope sentence,
the project-environments exclusion, and the hidden-settings case.

## Verification

- `cd server && pnpm exec vitest run
src/__tests__/board-access-denial.test.ts`: 33 passed. With the old
`authz.ts`, 5 of the new tests fail (red), and they pass with the change
(green).
- `cd server && pnpm exec vitest run
src/__tests__/environment-routes.test.ts
src/__tests__/agent-permissions-routes.test.ts`: 99 and 126 passed.

## Risks

- Low risk. Only the agent-facing 403 remediation text changes. Status
codes, the error code and board behavior do not change.
- The scope sentence is environment-specific, so other denials are
unchanged.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), 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>
canary/v2026.1111.0-canary.13
2026-11-11 12:49:19 -06:00
DottaandClaude Opus 5.5 854b4bcac4 fix(opencode): report a provider 401 from the hello probe as missing authentication (#20878)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Each agent has a runtime readiness test ("test environment") that
runs a short hello probe through its harness
> - The OpenCode probe sorts failures into model, timeout,
authentication and generic errors
> - OpenRouter rejects an empty or unknown key with HTTP 401 and the
messages "Missing Authentication header" or "User not found."
> - The probe matched authentication only by wording, so these 401
replies became a generic `opencode_hello_probe_failed` error
> - The server maps `*_hello_probe_auth_required` to the missing-account
state, so the user saw a generic failure instead of "connect an account"
> - This pull request classifies the structured APIError status 401 as
`opencode_hello_probe_auth_required`
> - The benefit is that a member without a working account shows the
missing-account state and its fix

## Linked Issues or Issue Description

**What happened**
An OpenCode (native runner) agent with an empty OpenRouter key failed
its readiness test with the generic error `opencode_hello_probe_failed`.
The chat eval `teams.configure-team` could not form its "one member is
missing an account, one is ready" fixture because of this.

**Expected behavior**
The readiness test reports `opencode_hello_probe_auth_required` (warn)
when the provider rejects authentication with HTTP 401, whatever the
provider's wording.

**Steps to reproduce**
1. Configure a native runner OpenCode agent with model
`openrouter/deepseek/deepseek-v4.1-flash-1017` and `OPENROUTER_API_KEY`
set to an empty string.
2. Run the agent's environment test.
3. OpenCode 1.18.34 prints
`{"type":"error","error":{"name":"APIError","data":{"message":"Missing
Authentication header","statusCode":401}}}`.
4. The result is `opencode_hello_probe_failed` (error), not the
missing-authentication check.

## What Changed

- `parse.ts`: add `openCodeErrorStatusCodes`, which reads the HTTP
status from OpenCode error events.
- `test.ts`: treat a 401 status on the hello probe as
`opencode_hello_probe_auth_required`, in addition to the existing
wording match.
- `test.remote.test.ts`: regression tests with the two recorded
OpenRouter 401 outputs, and a negative control (500 and 429 stay generic
failures).

## Verification

- `pnpm exec vitest run
packages/adapters/opencode-local/src/server/test.remote.test.ts
packages/adapters/opencode-local/src/server/parse.test.ts` passes (28
tests).
- Red check: without the classifier change, the two new 401 tests fail.
- `npx tsc --noEmit -p packages/adapters/opencode-local` passes.
- I recorded the probe output from the pinned OpenCode 1.18.34 binary
against OpenRouter with an empty key and with an unknown key.

## Risks

- Low risk. Only a 401 status changes classification. A 401 means the
provider rejected the credential. Other statuses keep their current
result.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), 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-11 12:44:09 -06:00
Nicky LeachandPaperclip 5dfbb04b85 Poll asynchronous workspace provider preparation (#20865)
## Thinking Path

> - Paperclip starts agent runs through a workspace provider when a
project uses remote storage.
> - The provider must prepare the workspace before the agent starts.
> - One long worker call ties the checkout to its transport.
> - Some checkouts take longer than the transport allows.
> - This change lets an opted-in provider accept work and report its
deadline.
> - Core waits for completion through short status calls and stops at
the provider deadline.

## Linked Issues or Issue Description

**What happened?**

A workspace provider can lose its reply when preparation takes longer
than a single transport request. This stops run bootstrap before agent
execution.

**Expected behavior**

Core should wait for a provider-confirmed workspace through short
requests. The provider's deadline must bound the wait.

**Steps to reproduce**

Use a workspace provider that takes longer to prepare a checkout than
its transport can remain open. Start a run that requires a new checkout.

## What Changed

- Add an optional workspace preparation method to the plugin SDK.
- Validate pending receipts and keep the original binding checks on
completed replies.
- Poll only during active preparation. Retry a lost status reply within
the advertised deadline.
- Preserve the original operation for providers that do not offer
asynchronous preparation.
- Reject deadline offers that exceed the existing lifecycle lock budget.
Honor accepted offers without shortening them.
- Checkout verification uses the existing synchronous RPC with a
10-second deadline and a 12-second transport timeout. [Core verification
PR #20876](https://github.com/paperclipai/paperclip/pull/20876)

## Verification

- All 63 workspace SDK and Core integration tests passed with temporary
local PostgreSQL. The tests cover lost status replies, deadline expiry,
oversized offers, and synchronous verification with an async-capable
worker.
- All 58 runner SDK input, evidence, and public type tests passed with
the regenerated source pins. The local server typecheck passed.

## Risks

- This change activates only when the provider advertises the method.
Existing providers keep their current behavior.
- Polling uses one-second intervals during active preparation. It stops
at completion, failure, or the provider deadline.
- No database migration is required.

## Model Used

- OpenAI GPT-6 Codex with local code execution and tests.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [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-11 10:33:02 -08:00
DottaandClaude Opus 5.5 e601f9d2fc fix(runner): answer each OpenCode MCP tool call on its own connection (#20886)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The Paperclip runner runs native OpenCode agents. OpenCode calls
Paperclip tools through a local MCP bridge in the OpenCode proxy.
OpenCode runs on Bun and uses Bun `fetch` for each MCP request.
> - In one chat eval, the model made two parallel tool calls. The bridge
produced and wrote both results in about 0.55 s. OpenCode received only
one of them.
> - The other call waited until Bun's default fetch timeout fired, about
five minutes later. The chat turn lost five minutes, and the eval missed
its deadline.
> - The most likely trigger is the reuse of an idle keep-alive socket.
We could not reproduce the loss locally, so this pull request is
hardening that is based on a hypothesis.
> - This pull request makes the bridge answer every request on its own
connection. It also records how each tool result left the bridge, so a
recurrence can be attributed from the run log.
> - The benefit is that a pooled socket can no longer strand a tool
result. If a call is stranded again, the run log shows whether the
bridge wrote the answer.

## Linked Issues or Issue Description

No public issue exists. Related: Refs #18751 (draft native client
acceptance diagnostic), Refs #19546 (abandoned queued calls), Refs
#20862 (transport event delivery; it does not touch this path).

**What happened**

In CI run `gha-42925998380` (chat-features, runner-opencode,
`cases.create-case`), the CEO chat run used OpenCode 1.18.34 with an
OpenRouter DeepSeek model. At 00:01:12.16 the model issued
`list_workflows` (MCP id 3) and `list_projects` (MCP id 4) in parallel.

The runner emitted the dynamic tool results for id 4 at 00:01:12.468 and
for id 3 at 00:01:12.714 (ok, HTTP 200). The bridge writes the HTTP
response directly after that event. There is no queue between them.

OpenCode completed `list_projects` at 00:01:13.113. It failed
`list_workflows` only at 00:06:01.761 with `The operation timed out.`

We read the bundled OpenCode source. Its MCP tool call has no timeout
that produces that message: the MCP request timeout gives `-32001
Request timed out`, and the configured value is 1 h for durable calls.
With the same binary in Bun mode (Bun 1.3.14), a fetch on a reused
keep-alive socket that never gets an answer fails after 298 s with
`TimeoutError: The operation timed out.` So Bun's default fetch timeout
ended the call, and the client never consumed the written answer.

The previous call (`list_agents`) answered at about 00:01:06.89, so its
socket was idle for about 5.27 s when the parallel pair started. Our
thesis is that call 3 reused that pooled socket, and call 4 opened a new
one.

**Expected behavior**

When the bridge has the result of a tool call, the MCP client gets it.
The client must not wait for a five-minute transport timeout.

**Steps to reproduce**

We did not reproduce it locally. This is the only occurrence in the
retained evidence. A macOS stress test (Bun 1.3.14 client, Node 26
server with the bridge's default keep-alive, one call, then an idle time
of 4.8–7.2 s, then two parallel calls, 8 pairs in parallel) completed
343 trials and about 1,029 requests with 0 lost results. The failing run
was on Linux CI with Node 24.

## What Changed

- `mcp-bridge.ts`: every bridge response sends `Connection: close`. The
MCP client cannot reuse a pooled keep-alive socket for a later request.
A loopback connect per request costs much less than the failure mode.
- `mcp-bridge.ts`: each `tools/call` records one closed delivery
diagnostic. This includes rejected calls (unsupported tool, invalid
input, duplicate identity), which record the outcome `error`
(`paperclip.opencode.tool-delivery-diagnostic/v1`) when its response
closes. The fields are the call id SHA-256, the socket request index,
the socket idle ms, the handler ms, the outcome (`result`, `error` or
`unanswered`) and `written` (`writableFinished`). It holds no tool
names, inputs or results. The limit is 256 records per bridge.
- `opencode-proxy-finish-diagnostics.ts`: the proxy forwards these
records to the run log as `warning` frames after session binding. It
validates the exact key set and value types and drops anything else.
- Tests: a new `mcp-bridge-delivery.test.ts` uses a pooling keep-alive
client with the incident's call shape (one call, then two parallel
calls). New forwarder cases cover the record validation. The existing
finish diagnostic assertion now filters by its own schema.

## Verification

- Red: with the `Connection: close` line removed,
`mcp-bridge-delivery.test.ts` fails with `expected 'keep-alive' to be
'close'`. The pooling client then reuses the socket.
- Green: `cd packages/paperclip-runner && pnpm exec vitest run
src/drivers/opencode/mcp-bridge-delivery.test.ts
src/drivers/opencode/mcp-bridge-diagnostics.test.ts
src/drivers/opencode/mcp-bridge.test.ts
src/cli/opencode-proxy-finish-diagnostics.test.ts
src/cli/opencode-app-server-proxy.test.ts
src/cli/opencode-proxy-abandoned-tool-call.test.ts` gives 6 files and 45
tests passed.
- The new test asserts that all three results arrive with the correct
JSON-RPC ids, that no socket is reused, and that each call records
`socketRequestIndex: 1`, `outcome: "result"`, `written: true` without
tool names or raw ids. A second test asserts that a client that leaves
before its answer records `outcome: "unanswered"`, `written: false`. A
third test asserts that a rejected call records `outcome: "error"`,
`written: true`.
- `tsc -p tsconfig.json --noEmit` for `packages/paperclip-runner`
reports no errors in the changed files. Seven existing errors in
`codex-runtime-adapter.ts` and `eval/workflow-harness.ts` come from this
worktree's linked dependencies. Required CI typecheck is the source of
truth.
- This does not prove the production fix. A recurrence on CI will show
socket reuse, idle time and `written` for the stranded call in the run
log.

## Risks

- Low risk. Each MCP request now opens a new loopback TCP connection.
That adds well under 1 ms per call, and there is no TLS.
- The `notifications/cancelled` handling and the abandoned-call abort do
not change. The abort still reads `writableFinished` on response close.
- One extra run-log warning for each tool call (at most 256 for each
bridge). The record holds only a hash, counters and timings.
- Previous-release story (upgrade-safe run state): no persisted run,
session, launch or pack state changes. A bridge from the previous
release keeps its behavior until the proxy restarts.
- If the thesis is wrong, the five-minute stall can recur. The new
records will then show `written: true` on a fresh socket and point the
next investigation at OpenCode or Bun.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), 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-11 12:29:59 -06:00
DottaandClaude Opus 5.5 8eabb13ebd fix(recovery): retry a system-woken run once when it lost its runner before any turn (#20873)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A native run on a remote sandbox can lose its Runner process before
its same-run retry reattaches. The run then fails with
`runner_remote_process_identity_unavailable`
> - After saved-work recovery, the server requests one Retry by itself,
but only for user wakes. System wakes (monitors, automatic assignments)
rest on the visible Retry
> - On staging, a monitor wake's run lost its Runner before it started
any provider turn. Nothing it did could be repeated, but the task stayed
blocked until a person clicked Retry
> - This pull request lets that one case continue automatically, once,
under the run's responsible user
> - The benefit is that a system-woken task whose run never reached a
provider turn continues without a person

## Linked Issues or Issue Description

**What happened?**
On staging, a monitor wake (`issue_monitor_due`) started a remote native
run. Its `session.snapshot` command timed out before any turn. The
same-run retry found the Runner process gone (`process_not_running`,
lifecycle `ready`) and the run failed with
`runner_remote_process_identity_unavailable`. The run had no turn event
and no tool event. Saved-work recovery recovered the files, but
`requestInterruptedNativeTurnRetry` refused with `not_user_request`, so
the task stayed blocked on a manual Retry.

**What did you expect to happen?**
When the run proves that it did no provider turn, the server requests
the Retry once by itself, as it does for user wakes.

**Steps to reproduce**
1. A system wake starts a remote native run.
2. The run fails before any turn event, and the same-run retry finds the
Runner gone.
3. Saved-work recovery completes. No Retry is requested; the task waits
for a person.

Refs #20872, #20853

## What Changed

The rule that changed, exactly: in `requestInterruptedNativeTurnRetry`
(`server/src/services/heartbeat/restart-interrupted-turn.ts`), a run
whose wake was not requested by a user used to stop with
`not_user_request` in all cases. Now it requests the task Retry only
when all of these are true:

- the run failed with one of the existing reattachment-loss codes
(unchanged gate);
- a positive durable record proves that no turn was ever issued: the
run's last `native.remote_runner_recovery_failed` event carries
`remoteRunnerRecovery` with `stage: process_verification`, `reason:
process_not_running` and `providerTurnIssued: false`;
- the run also recorded no turn event and no tool event;
- the run has a `responsibleUserId`; the Retry runs under that user;
- the run is not itself a `task_retry` run;
- the task is still open and assigned to the same agent (unchanged
gate).

An absence of run events is never enough: a turn can reach the provider
before its events are stored (Greptile, first review).

- New `withRemoteRunnerTurnProof` (`native-session-executor.ts`): when a
same-run reattachment is refused because the sandbox proved the Runner
process gone (exit 45, not an unreachable sandbox), the executor reads
the run's controller journal. The controller records each turn command
there before it sends it. Only when
`runnerdStateProvesNoRemoteProviderTurn` holds (just
`run.prepare`/`session.open` and maintenance commands such as
`session.snapshot`, and only session-open events) does it add
`providerTurnIssued: false` to the diagnostic. This is the same proof
that restart recovery already uses for `bootstrap_incomplete`.
`RemoteRunnerRecoveryDiagnostic` keeps the field only as `false`.
- Which staging shapes this covers: a run whose journal holds no turn
command. Whether c14bab19 (the `session.snapshot` timeout) qualifies
depends on its controller journal, which staging does not expose; if its
journal held a `turn.start`, it stays on the manual Retry floor.
32376b3a (`turn.submitted`, then a `turn.start` timeout) always stays on
the floor.
- Everything else about system wakes is unchanged: without the journal
proof, with any turn or tool event, or without a responsible user, a
system wake still rests on the visible Retry.
- Bounded: the Retry uses the existing idempotency key
`restart-interrupted-turn:<runId>`, so it is requested at most once per
run. Its successor is a user-attributed `task_retry` run, so if that run
also loses its Runner, the existing `automatic_retry_used` rule leaves
it on the visible Retry. No loop is possible.
- Attribution: `requestTaskRetry` records `issue.retry_requested` with
actor `restart-recovery` on behalf of the run's responsible user.
Admission keeps every gate (saved-work recovery, stop proof, approvals,
pause, budget, ownership).
- Tests in
`server/src/services/workspace-recovery-retry-copyback.test.ts`: a
positive case (monitor wake, Runner proven gone, journal proof),
including the second-loss `automatic_retry_used` check. Negative cases:
no run events but no journal proof (the Greptile shape), a settled turn,
a tool event, no responsible user.
- New
`server/src/services/native-runtime/remote-runner-turn-proof.test.ts`:
the proof is recorded only for a journal of session commands; not for a
`turn.start` command, a committed turn event, an unreadable journal, or
an unreachable sandbox (`probe_transport_failed`).
- `doc/execution-semantics.md`: describes the rule.

## Verification

- `cd server && pnpm exec vitest run
src/services/workspace-recovery-retry-copyback.test.ts`: 83 passed.
- Red on master (product hunk removed): the positive case fails with
`expected { requested: false, … } to match object { requested: true }`.
- Red on the first head (a244ad3b): the "no run events but no journal
proof" case is requested automatically; CI's `run-completion.test.ts`
("replays saved user intent … system_wake") failed for the same reason.
Both pass now.
- `pnpm exec vitest run
src/services/native-runtime/remote-runner-turn-proof.test.ts`: 4 passed.
`native-session-executor.test.ts`, `run-completion.test.ts`,
`runtime-execution.test.ts`: 1571 tests passed (one local `afterAll`
hook timeout under load).
- `cd server && pnpm exec tsc --noEmit -p tsconfig.json`: passes.

## Risks

- Low to medium. A system-woken task can now start one new run without a
person. This happens only when the controller journal proves that no
turn command was ever issued, so no provider work or tool side effect
from that run can be repeated. The Retry starts a fresh provider
session, like a manual Retry.
- The Retry runs with the run's responsible user's authority. That user
already authorized the failed run. If the user lost access,
`requestTaskRetry` refuses and the visible Retry remains.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), with tool use and code execution
in an agent harness.

## 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-11 12:22:51 -06:00
DottaandClaude Fable 5.2 f9171ef621 fix(native): retry a throttled post-turn checkpoint for real; wait out storage throttling while saving the provider session (#20871)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - After a native turn in a remote sandbox, closing the session saves
the provider session (the harness backup) to object storage. The next
attempt resumes from it.
> - On staging, object storage throttled that save ("Please reduce your
request rate", S3 `SlowDown`). The close failed, was retried 3 times 1 s
apart, and the session was quarantined. The next attempt of the same run
was refused with `native_session_cleanup_quarantined`.
> - The retries could never succeed: the Runner transport memoizes
`close()`, so every retry returned the first rejected promise and never
ran the checkpoint again. Quarantine recovery and admission recovery
call the same `close()`.
> - Also, the backup upload did not wait out a throttle or outage longer
than one request's own retries.
> - This pull request makes a close that failed only in its checkpoint
run that checkpoint again on the next `close()`, and makes the backup
upload wait out storage unavailability inside the existing bounded
storage wait.
> - The benefit is that a transient storage throttle no longer ends in a
quarantine that refuses the next attempt of the same run.

## Linked Issues or Issue Description

Refs #20772 (made the backup upload resendable after `SlowDown`). Refs
#20165 (per-request throttle backoff). Refs #20833 (earlier steps of the
same failure chain).

**What happened?**

1. A staging Codex run on Daytona finished its turn. The post-turn close
ran the remote checkpoint.
2. The checkpoint failed: `remote checkpoint failed: Please reduce your
request rate.`
3. The Runner retried the close 3 times 1 s apart (`native session
cleanup recovery after close failure (1..3)`), then 3 quarantine
recoveries. All failed within about 5 s.
4. The next attempt of the same run was refused:
`native_session_cleanup_quarantined: prior session cleanup remains
incomplete`.

**Expected behavior**

A throttle slows the save down. A retry of the close saves the session
again. The next attempt of the same run proceeds once the save succeeds.

**Steps to reproduce**

1. Make a transport close whose checkpoint throws once.
2. Call `close()` again.
3. On master the second call returns the first failure without running
the checkpoint again.
4. Separately, answer each harness-backup `PUT` with 9 `503 SlowDown`
responses. On master `persistNativeHarnessBackup` throws `SlowDown:
Please reduce your request rate.`

**Paperclip version or commit**

master at 381b862135.

## What Changed

- `packages/paperclip-runner/src/live/runnerd-codex-transport.ts`: when
a close fails because its checkpoint failed (and the control-route
release did not fail), the transport keeps the checkpoint and the rest
of the close. The next `close()` runs the checkpoint again and then
finishes the close. All other close failures (for example a missing
suspension proof) stay memoized as before.
- `server/src/services/native-runtime/native-harness-backup-storage.ts`:
`persistNativeHarnessBackup` runs inside the existing storage outage
wait (`waitOutStorageOutages`, purpose "saving the provider session"). A
storage-unavailable failure (outage, throttle, rejected credentials)
that outlasts one request's retries is retried with the bounded
exponential storage wait (2 s doubling to 30 s, 5 minutes in total).
Each request still uses the jittered per-request throttle backoff from
#20165.
- Restart chaos: storage fault `ST8` answers the next 9 to 12 (seeded)
harness-backup `PUT`s with `503 SlowDown`. The storage audit reports it
when it never changed a request. The README documents it (for `--env
daytona --storage s3`).

## Verification

- RED on master (fix reverted, new tests kept):
- `runnerd-codex-transport-suspension.test.ts` "runs a failed close
checkpoint again on the next close": the second `close()` returns the
first `SlowDown` error.
- `native-harness-backup-throttle.integration.test.ts` "waits out a
throttle that outlasts one request's backoff": `SlowDown: Please reduce
your request rate.`
- GREEN: both pass. `native-harness-backup-throttle.integration.test.ts`
3 passed. `runnerd-codex-transport-suspension.test.ts` 12 passed.
`runnerd-codex-transport-release-resume.test.ts` 22 passed (twice).
`vitest --config tests/restart-chaos/vitest.config.ts`: 46 files, 554
tests passed, including the new ST8 planning and proxy test.
- `tsc --noEmit` clean for `packages/paperclip-runner`, `server` and
`tests/restart-chaos`.
- `runnerd-codex-transport-settlement.test.ts` has one case that also
fails on unchanged master in this worktree.

## Risks

- A checkpoint retry runs the same server checkpoint callback again
after the runner process was stopped. It reads the settled state from
the sandbox. If the sandbox is gone, it fails as before and the
quarantine stays.
- A close attempt can now take up to the 5-minute storage wait during a
long outage. This is on the post-turn path only, not before
`turn.start`.
- No change to the quarantine admission rules: admission still
accelerates one bounded recovery batch and refuses if it fails. Those
recoveries now really retry the checkpoint.

## Model Used

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

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] 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 Fable 5.2 <noreply@anthropic.com>
2026-11-11 12:11:16 -06:00
DottaandClaude Opus 5.5 566a97b882 fix(native): an unretirable predecessor sends the next attempt to a fresh sandbox (#20863)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs on a reused sandbox keep the previous run's warm Runner,
and a new run must retire that owner before it starts
> - #20844 retires the owner when Paperclip can prove it stopped. Some
owners can never be proven stopped: the controller journal drain
outlives the close grace, the Runner never confirms suspension, or a
parked question leaves a tool input unanswered
> - Then the new run failed before any provider work, and the task
stayed blocked with no automatic continuation
> - The general answer is to leave the old Runner on its sandbox and
start the next attempt on a fresh sandbox restored from the saved
workspace
> - This pull request adds that fallback for every path that retires a
predecessor, and settles a parked question's tool input once its Runner
is gone
> - The benefit is that an unprovable predecessor no longer fails a task
that did no work

## Linked Issues or Issue Description

**What happened?**
An automatic wake started a new run 30 to 60 seconds after a long
successful run on the same task, on the same reused sandbox. The new run
had to retire the previous run's warm session first. The retirement
failed and the run failed before any provider work, with one of:
- `Controller journal drain timed out; preserve the session before
replacing its writer.` (during instruction preparation, before runtime
selection, or in the executor);
- `provider_transport_failed: runner did not durably suspend before
checkpoint` (during instruction preparation);
- `runner_state_identity_mismatch:
prior_owner_terminal_state_indeterminate;
retirement_refused=journal_tool_input_unanswered` (the previous run had
parked on a question).
The tasks rested `blocked` with a reconciliation hold or with nothing
scheduled. One bounded retry reused the same sandbox and ended
`execution_finalization_deadline_exceeded`, then its recovery refused
with `allocation_ownership_ambiguous`.

**Expected behavior**
A run that did no provider work does not fail the task because its
predecessor cannot be retired. The next attempt drops the reused sandbox
and starts on a fresh one restored from the saved workspace. Unsaved
work is never dropped: when the workspace or the session backup is not
durably saved, the sandbox is kept and today's behavior applies.

**Steps to reproduce**
1. Complete a warm native run on a Daytona sandbox.
2. Wake the task again so that the next run must retire the warm owner
(configuration change, new session, changed agent files).
3. Make the owner's close fail (journal drain timeout, unconfirmed
suspension).
4. The new run fails before provider work and the task is not continued.

Refs #20844, #18373.

## What Changed

- `native-runtime/predecessor-retirement.ts`: a typed pre-provider error
`NativePredecessorRetirementUnprovenError`
(`native_predecessor_retirement_unproven`) and the `replaceBeforeReuse`
lease marker.
- Native executor: every path that retires a predecessor for a new run
(instruction-copy claim, instruction-directory reservation, session
reset, configuration change) throws the typed error when the close
fails. The owner stays registered for its own sandbox, so no run on that
sandbox launches over it. A run on a different sandbox forgets that
owner instead of replaying its cached rejected close. The executor does
not settle this error as a provider failure.
- A prior-owner fence that refuses a terminal owner on a resumed sandbox
(`prior_owner_terminal_state_indeterminate`) also throws the typed
error.
- Retirement of a finished remote owner accepts an unanswered tool input
when the owner's committed result is a governed-wait park
(`response_wake` on a task interaction) and its Runner's sandbox lease
ended elsewhere.
- Heartbeat (`heartbeat/predecessor-retirement-fallback.ts`): on the
typed error with no execution-start evidence and no result, mark this
run's reusable lease on that sandbox `replaceBeforeReuse` and log
`environment.lease_replacement_requested`. After the run ends, `failRun`
hands the prepared workspace on only through the shared unchanged-files
proof (`proveRemoteWorkspaceUnchanged`; a provisioning command or any
runner command refuses it). With the proof it records
`predecessorRetirementFallback`, closes the run's coordinator without a
result and re-queues the run in the uncharged `interrupted_preparation`
lane (native and legacy runtime modes); a run that answered a task
interaction keeps its own wake in that uncharged lane, so the retry
still carries the answer. Without the proof the workspace stays dirty,
heartbeat runs today's failed-workspace preservation and no retry is
scheduled.
- Environment runtime: a marked lease is replaced instead of resumed,
under the same rule as a failed resume
(`reusableSandboxReplacementIsSafe`: run ended, clean workspace with a
committed save, verified harness backup). The old lease goes to
`pending_cleanup` for the sweep. Otherwise the sandbox is resumed as
before.
- Chaos: session fault SR1U, the SR1 variant where the retained owner
cannot be retired. The server gets a test-only fault
(`PAPERCLIP_TEST_WARM_RETIREMENT_FAULT=unconfirmed`, armed only when
`NODE_ENV=test`). The oracle allows only the fallback failure and
requires the succeeding follow-up on a different sandbox.
- Docs: `doc/plugins/PLUGIN_RUNNER_EXECUTION.md`, chaos README.

## Verification

- RED on master before this change (with #20844), then GREEN:
- `native-session-executor.test.ts -t "predecessor cannot be retired"`:
base fails with `Error: Controller journal drain timed out…` instead of
the typed error.
- `native-session-executor.test.ts -t "unretired predecessor"`
(instruction preparation): same base failure.
- `native-recovered-owner-followup.test.ts -t "unacknowledged"`: base
fails with `runner_state_identity_mismatch:
prior_owner_terminal_state_indeterminate;
retirement_refused=journal_tool_input_unanswered`.
- New contract tests: `heartbeat-native-predispatch-retry.test.ts`
(claimed native run: lease marked, activity logged, uncharged
`interrupted_preparation` retry, no recovery action, task not blocked,
retry succeeds) and
`environment-runtime-reusable-resume-replacement.test.ts` (a marked
lease is replaced without a resume; with unsaved work or no verified
backup it resumes as before),
`remote-workspace-unchanged-release.test.ts` (an untouched workspace is
handed on; a setup-written one stays dirty) and `retries.test.ts` (an
answered interaction re-queued in the uncharged lane stays a
resolved-interaction continuation with no failure retry spent).
- Existing suites pass: the native executor, recovered-owner follow-up,
instruction working copy, run completion, run failure cause and resume
replacement test files. `pnpm exec vitest run --config
tests/restart-chaos/vitest.config.ts question-handoff scheduler config`
passes. Server and chaos harness `tsc --noEmit` pass.
- Not run: a Daytona SR1U wave. It needs a runner image that matches
this checkout.

## Risks

- A sandbox marked for replacement is resumed when it cannot be replaced
safely. That run can meet the same unretired owner again, fail again in
the bounded lane, and then rest with Retry, as today.
- The replacement session restores the latest verified harness backup.
When the old Runner's final checkpoint was never saved, a same-session
continuation resumes from that backup.
- An unanswered tool input is accepted only for a committed
governed-wait park whose Runner's sandbox lease ended on another
sandbox.
- Previous-release story: the marker and the run record are additive
JSON. An older server ignores the marker and resumes the sandbox as
before.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, agentic coding with
tool use (shell, file edits, test runs) 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-11 12:10:24 -06:00
DottaandClaude Fable 5.2 ffe05b1b7c fix(heartbeat): retry every pre-provider lease acquisition failure and rest it on Retry (#20857)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A task run takes an environment lease (for example a Daytona
sandbox) before the agent starts. No provider work happens before that
lease exists.
> - Only a provider HTTP status or a connection errno counted as a
transient lease failure. #20828 (merged while this was open) added the
Daytona acquisition deadline. A database error while the lease was
recorded, a plugin RPC deadline, and an unfinished provisional creation
still did not count.
> - Those failures became a generic `setup_failed` with no bounded
retry. And when the bounded retries of any
`environment_provider_unavailable` failure were used up, recovery still
put the task on a board-owned `stranded_assigned_issue` action.
> - A hosted instance saw this for six runs in 45 minutes when
concurrent runs rose from about 8 to about 20. Two tasks stayed blocked
and waited for the board.
> - This pull request classifies every lease acquisition failure that
has structured transient evidence as `environment_provider_unavailable`.
When the bounded retries are used up, the task rests on a system-owned
action and a working Retry.
> - It also adds the restart-chaos fault EP1, so the harness covers
lease acquisition failures from now on.
> - The benefit is that a busy or slow sandbox provider no longer
strands tasks on the board.

## Linked Issues or Issue Description

No public issue exists.

**What happened?**

Five runs failed with `Failed to acquire lease for environment
"Paperclip Sandbox" (sandbox): Daytona lease acquisition timed out;
allocation cleanup is pending` after 5 to 6 minutes in `preparing`. One
run failed with `Database query failed (lease_acquire_failed)` within 10
seconds. Every run recorded `executionRecovery: { kind: "bootstrap",
providerWorkStarted: false }`, but the error code was `setup_failed`. No
run got a bounded retry. Two tasks were left `blocked` with a
board-owned `stranded_assigned_issue` recovery action.

**Expected behavior**

A lease acquisition failure happens before any provider work. The run
must retry with a bounded, visible backoff. When the bound is used up,
the task must rest with a Retry that works, and no board-owned recovery
action.

**Steps to reproduce**

1. Make lease acquisition fail with the provider's acquisition deadline
(the validated acquisition diagnostic that the environment runtime
records), or with a database driver error.
2. Let the run start.
3. Before this change, the run fails with `setup_failed`, no retry is
scheduled, and recovery creates a board-owned `stranded_assigned_issue`
action.

## What Changed

**How this PR and #20828 divide the problem.** #20828 (merged) makes the
Daytona plugin end a stuck create early and classifies the
acquisition-deadline envelope as `environment_provider_unavailable`.
This PR does not change the Daytona plugin or that deadline path. It
adds only what #20828 leaves out:
- a lease-acquisition failure that is not a create timeout: the plugin
RPC deadline (`-32003`) and an unfinished provisional creation recorded
for this run;
- a transient database error while the lease is recorded;
- the exhausted state: a system-owned recovery action and Retry instead
of a board-owned `stranded_assigned_issue`, for every
`environment_provider_unavailable` cause (including #20828's);
- the chaos fault EP1 (EP1, EP1N, EP1D).

- `readTransientLeaseAcquisitionFailure`
(`server/src/services/environment-provider-unavailable-error.ts`)
classifies a lease acquisition failure as transient when it has
structured evidence: the provider status or errno (as before), the run's
own validated acquisition diagnostic (deadline or unfinished provisional
creation), the plugin RPC deadline, or a transient database error (a
postgres.js connection code, or SQLSTATE class 08 or 53, 57P01-57P03,
57014, 55P03, 40001, 40P01). A deterministic SQL error (syntax, schema,
constraint) is not retried. A definite non-transient status (for example
HTTP 401 or 404) is still `setup_failed`. Message text is not parsed.
- `environment-run-orchestrator.ts` uses this classifier, so these
failures get `environment_provider_unavailable` and the existing bounded
bootstrap retry (30 s, then 60 s, with jitter). The task shows "retry
scheduled" while it waits.
- When the retries are used up, recovery uses the cause
`environment_provider_unavailable` with a system-owned action and no
board routing. The notice says "Retry when ready" and does not ask for a
board decision. The task's Retry starts a new acquisition.
- Restart-chaos fault EP1: a test-mode server seam
(`environment-lease-fault-injection.ts`, read from
`PAPERCLIP_CHAOS_LEASE_FAULT_CONTROL`, ignored in production) throws the
staging error shapes before any provider call.
`tests/restart-chaos/env-provider-wave.ts` runs EP1 (deadline twice,
then success), EP1N (deadline forever, then Retry after disarm) and EP1D
(database error once). The oracle fails on any `setup_failed` run, any
board-owned recovery action, a wrong retry count, or a task that does
not complete.
- Docs: `doc/execution-semantics.md` and
`tests/restart-chaos/README.md`.

## Verification

- RED on `origin/master` (`1d67e309e0`, before #20828): the three new
tests in `heartbeat-process-recovery.test.ts` ("transient sandbox
provider refusal during lease acquisition") fail with the staging
result: `expected [ [ 'failed', 'setup_failed' ] ] to deeply equal [ [
'failed', 'environment_provider_unavailable' ], [ 'scheduled_retry',
null ] ]`. On current master the deadline now retries (#20828), but the
exhausted case still rests on a board-owned `stranded_assigned_issue`
action (the existing HTTP 502 exhaustion test showed `ownerType:
"board"` before the recovery change), and the database error is still
`setup_failed`.
- GREEN: `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/heartbeat-process-recovery.test.ts -t "lease acquisition"`
passes 5 of 5. This includes the deadline and the database error that
retry once and then complete, and the deadline that never clears. That
task rests `blocked` with a system-owned action, `canRetry: true`, no
execution blocker, and Retry completes it.
- Unit tests: `environment-provider-unavailable-error.test.ts` +
`environment-lease-fault-injection.test.ts` (56),
`tests/restart-chaos/env-provider-faults.test.ts` (6) pass.
- Related suites pass: `environment-run-orchestrator`,
`task-interruption-query`, `issue-recovery-actions`,
`issue-recovery-deadlines`, `stranded-notice`,
`heartbeat-native-resume-lease-failure`.
- `server` and `tests/restart-chaos` typecheck pass.
- Chaos waves (local, Codex `gpt-6.1-sol`, head `1bd387e98f`; the later
head changes only database classification and the notice), all pass:
- EP1 `w-20261111T171803-s71-ep`, 2 tasks: each had exactly 2
`environment_provider_unavailable` runs, then succeeded with no user
action, no board-owned recovery action, brief oracle pass.
- EP1D `w-20261111T172850-s73-ep`, 1 task: 1 failed run on the database
error, then succeeded.
- EP1N `w-20261111T173346-s74-ep` (and `w-20261111T162752-s72-ep` before
the rebase): 3 failed runs, the task rested `blocked` with `canRetry:
true` and no board-owned action; after disarm, Retry completed it.
- No Daytona wave was run. The fault is injected in the server before
the provider call, so a local instance reaches the same retry and
recovery code.

## Risks

- More failures now retry, all bounded. Only transient SQLSTATEs retry.
The staging database error recorded no SQLSTATE. If it was a
deterministic SQL error, it still fails `setup_failed` and goes to the
board as before; that is intended, because a retry cannot fix it.
- Each retry is a new creation attempt with its own creation record. A
provisional allocation whose cleanup is pending stays with its record
and the creation reconciler, as before.
- The retry bound is the shared bootstrap bound (two retries). A
provider outage longer than about 15 minutes still rests on Retry, but
no longer on a board action.
- The fault seam reads a file only when the environment variable is set,
and never when `NODE_ENV=production`.
- Saved state from the previous release: no persisted shape changes. A
task already stranded on a board action keeps it
(`preserveExistingOwner`). New failures use the new cause.

> 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 (Anthropic) through Claude Code, model ID `claude-opus-5-5`,
with tool use and code execution.

## Checklist

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

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

---------

Co-authored-by: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 12:05:54 -06:00
Nicky LeachandPaperclip b2edea5754 fix(workspaces): verify existing worktrees before fresh retries (#20876)
## Thinking Path

> - Paperclip manages work for AI agents.
> - Retry checks whether the task can keep its existing workspace.
> - A provider can complete checkout before Core receives the result.
> - Recreating a completed worktree costs time and can change its base
commit.
> - Core uses the same ownership and writer checks for local and
provider workspaces.
> - The provider verifies its checkout before Core creates a fresh
workspace.

## Linked Issues or Issue Description

**What happened?**

A retry can allocate a new provider worktree even though the original
checkout completed.

**Steps to reproduce**

Complete workspace checkout through a storage provider. Fail Runner
startup or lose the prepare reply. Retry the task.

**Expected behavior**

Reuse the same worktree when the provider confirms checkout and the
prior run can no longer write to it.

Related: #20865 addresses timeouts while the original prepare is in
progress. This PR checks an earlier checkout when the user retries.

## What Changed

- Check the prior task binding and writer state before asking a storage
provider to verify a checkout.
- Extend the existing prepare operation with an optional verification
mode. Providers opt in through their manifest.
- Return through normal workspace reuse after verification. Keep the
fresh retry fallback when verification cannot establish checkout.

## Verification

- Core workspace preparation, plugin workspace ownership, retry
continuity, and SDK workspace-provider suites passed locally (213
tests).
- Focused cases pass for provider verification, retry through heartbeat
dispatch, and real local Git checkout verification with no plugin
worker. The heartbeat test confirms that ordinary preparation and
adapter dispatch keep the original workspace, branch, and base commit.
- Core server typecheck passed before the final audit and timing edits.
GitHub CI covers the final revision and broader regressions.

## Risks

Provider verification adds one bounded call before a fresh retry. Older
providers keep their current behavior. Verification must preserve the
workspace, branch, and recorded base commit. The normal execution
preparation checks still run before admission. No database migration is
needed.

## Model Used

OpenAI GPT-6 (Codex), with source inspection and local 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 linked existing issues or described the issue in-PR
following the relevant issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links
- [x] My branch name describes the change and contains no internal issue
details
- [x] I have run targeted 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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-11 18:04:18 +00:00
DottaandClaude Opus 5.5 a52f622be8 fix(native): retry a remote provider startup failure once its runner exit is proven (#20874)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs on a remote sandbox (Daytona) start a Runner in the
sandbox, and the Runner opens the provider session
> - When the provider rejects `session.open` at startup, no provider
work began
> - #20808 ends every such remote rejection at once with Retry, because
restart recovery could not prove that the failed runner was gone. A
fault that clears by itself therefore needs one human click
> - The Runner transport already settles the failed runner on close and
observes its exit. That is the missing proof
> - This pull request records that proof on the failure and lets the
same-run bootstrap retry use it. It stops a startup error that repeats,
so an image fault that a retry cannot fix does not loop
> - The benefit is that a transient remote startup failure recovers
without a person, and a permanent one still rests at once with a working
Retry that names the error

## Linked Issues or Issue Description

Refs #20808
Refs #20832
Refs #20820

**What happened?**
A remote (Daytona) native run whose provider rejected `session.open` at
startup ended as `native_provider_startup_rejected` with Retry. This
happened even when the Runner had settled and exited, and no provider
session, event or checkpoint existed. A person had to click Retry for a
fault that the durable record proves did no provider work.

**Expected behavior**
With a proven runner exit and no provider work, the run retries its
bootstrap on the same run automatically. A startup error that repeats on
the next attempt stops at once with Retry and names that error. Failures
saved without the proof keep today's behaviour.

**Steps to reproduce**
1. `cd server && npx vitest run
src/services/native-runtime/native-startup-rejected-retry.integration.test.ts
src/services/native-runtime/native-runner-restart-recovery.integration.test.ts
-t "remote startup rejection|session.open rejection"`
2. On master, 3 cases fail: the proven remote rejection is
`terminal_failure`, the repeat does not name its error, and restart
recovery holds the proven failure on
`ambiguous_remote_recovery_evidence`.

**Paperclip version or commit**
master at 3ded7bbd91.

## What Changed

- Runner transport (`runnerd-codex-transport.ts`): new
`runnerExitConfirmed()`. It is true only after a completed close settled
the runner (provider stopped, runner suspended) and the runner process
exit was observed.
- Codex driver (`codex-app-server-driver-impl.ts`): after it closes the
transport for a failed session open, it sets `runnerExitConfirmed` on
the error when the transport proves the exit.
- Executor (`native-session-executor.ts`):
- A remote `provider_session_open_rejected` with a startup
`providerFailure` keeps the same-run `bootstrap_retry` only with that
proof. The saved failure records `runnerExitConfirmed: true`.
- Each failed attempt's recovery history entry records a startup failure
signature: provider, category and message, with numbers ignored. When
the next attempt fails with the same signature, it stops as
`native_provider_startup_rejected`. Its next action names the startup
error and keeps "Retry starts a new run".
- Deterministic rejections (`requires_action` authentication, permission
or invalid request) stop at once as before.
  - Local targets are unchanged.
- Restart recovery (`native-restart-recovery.ts`):
`remoteRunnerExitedBeforeProviderWork` also accepts a settled
`provider_session_open_rejected` with `runnerExitConfirmed: true`. All
its other checks still apply: no provider session, no provider event, no
checkpoint, no runner-sent event, no successor.
- Docs: `doc/execution-semantics.md` and
`packages/paperclip-runner/docs/durable-recovery.md`.

## Verification

- Red on master, green here:
- `native-startup-rejected-retry.integration.test.ts`: proven remote
rejection → `retryable_failure` with `runnerExitConfirmed`; unproven →
terminal; repeat → terminal, the next action names `bwrap: loopback`,
and Retry is offered.
- `native-runner-restart-recovery.integration.test.ts`: proven failure →
`bootstrap_incomplete` (`remote_runner_exited_before_provider_work`); an
older release's failure without the proof stays held.
- `codex-app-server-driver.lifecycle.test.ts`: the driver records the
proof only when the transport reports it after close.
- `native-session-executor.test.ts`: unit cases for the rule, the
`cause` chain, the repeat signature and deterministic rejections.
- Local runs: the 3 server files above pass (1380 tests); restart
recovery `-t remote` 75 pass; driver lifecycle 25 pass.
- `tsc --noEmit` clean for `server` and `packages/paperclip-runner`.
- Not run: a Daytona chaos wave. There is no in-sandbox startup fault
seam yet (see Risks).

## Risks

- The proof depends on the transport's close settling the runner after a
failed `session.open`. The Rust runner keeps serving after a failed
`session.open` (lifecycle `Continue`) and accepts `runner.suspend`, so
close should settle. If it does not settle, there is no proof and the
run ends at once with Retry, which is today's behaviour. It is never
less safe than today.
- Retries stay bounded: the startup retry budget (3 attempts) and the
repeat rule both apply. A repeated identical error stops after one
automatic retry.
- Saved state: failures saved by older releases have no
`runnerExitConfirmed` and keep the old rule. History entries without a
signature never match a repeat. A mid-run deploy is safe: an old
executor never writes the proof.
- Not covered by chaos: a remote "Codex fails at startup N times" fault
needs a hook inside the sandbox Runner or Codex. DF1's seam is
server-side only, and no open chaos PR adds a seam inside the sandbox.

> 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 (Anthropic) Opus 5.5, model ID `claude-opus-5-5`, run in Claude
Code with tool use (shell, file edit) and extended reasoning.

## 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-11 11:55:15 -06:00
Nicky LeachandClaude Opus 5.5 14cb69245f fix(projects): do not offer a single project storage option (#20875)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A project uses one workspace storage owner. The user chooses it when
they create the project.
> - The **Available in** selector shows the storage owners that the user
can choose.
> - Sometimes the default environment cannot be used, and only one
storage owner is available. In that case, the dialog shows a selector
with only one option. The user must select it before they can create the
project.
> - A selector with only one option is not a real choice.
> - This pull request uses the only available storage owner as the
default. The selector appears only when the user has two or more
options.
> - The benefit is that project creation asks for a choice only when
there is one.

## Linked Issues or Issue Description

Refs #20835

**What happened?**
An example is an instance in managed-sandbox-only mode that has the
agent-computer plugin but no managed sandbox. The local environment is
hidden, and the managed-only rule finds no default. The
project-environments endpoint returns one storage group and an error.
The new project dialog shows **Available in** with only one option and
disables **Create project** until the user selects it. An API request
without `environmentId` fails with `project_environment_unavailable`.

**What did you expect?**
When only one storage owner is available, the project uses it. No
selector appears.

**Steps to reproduce**
1. Turn on `enableManagedSandboxOnly`. Install the agent-computer
plugin. Do not add a sandbox environment.
2. Open **Create project**.
3. See an **Available in** selector with one option, and a disabled
**Create project** button.

## What Changed

- `projectEnvironmentOptions`: when the configured default is
unavailable and only one storage group exists, the first environment of
that group becomes `defaultEnvironmentId`, and `error` is null. When two
or more groups exist, an unavailable default still produces the error.
The server does not guess.
- Project create uses the same default, so an API request without
`environmentId` now succeeds in the single-group case.
- `NewProjectDialog` shows the selector only when `groups.length > 1`.
- Tests:
- A new server test covers managed-only mode with only the
agent-computer environment.
- A new server test covers an unavailable default with two storage
groups (the server still refuses to guess).
- The archived-default test now expects the server to use the only
remaining storage.
  - A new UI test checks that the dialog never shows one option.
- Docs: `doc/project-environment-compatibility.md` and
`docs/api/goals-and-projects.md`.

## Verification

- `pnpm vitest run src/__tests__/project-environments.test.ts` in
`server/`: 16 passed.
- `pnpm vitest run src/components/NewProjectDialog` in `ui/`: 12 passed.
- `tsc --noEmit` in `ui/`: clean.
- Server `tsc` reports 9 errors on this machine. None are in the changed
files. They are in `packages/adapter-utils` and a native-runtime test,
because local dependencies are stale. CI typechecks with a fresh
install.

## Risks

- Low risk. The change applies only when the default is unavailable and
exactly one storage group exists. In that case, the server picks the
group that the user would have had to choose anyway. Run placement does
not depend on the project, so this does not change where runs execute.
- No migration.

## Model Used

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

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [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-11 09:49:33 -08:00
DottaandClaude Fable 5.2 76da2fec4f perf(runner): deliver transport events when they arrive instead of polling every 5 ms (#20862)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The server runs each native agent session through a runner
transport. The transport reads the events that the runner commits to the
session's control plane and delivers them to the driver.
> - The transport found those events with a timer. Every live transport
ran a pass every 5 ms, for as long as the session was open, whether or
not an event had arrived.
> - A CPU profile of the staging server shows the cost. The main thread
was at 96% of one core, and this pass used 5.6% of it with about a dozen
live sessions. The cost grows with the number of sessions and never
stops.
> - The same timer also delayed every event. An event waited up to 5 ms
for the next pass before the driver saw it. Agent boot time and time to
first token are measured requirements.
> - This pull request makes the control plane tell the transport when a
reader can see a change, and the transport delivers in the next task.
The fixed timer is gone.
> - The benefit is that an open session with nothing to deliver costs
nothing on the main thread, and an event reaches the driver sooner than
before.

## Linked Issues or Issue Description

No public issue exists. The problem is described here.

**What happened?**

The server's main thread on a hosted instance stayed at 96% of one core,
so every request and every agent turn waited behind it. A 30 second CPU
profile of the live server (Node 24, about a dozen live native agent
sessions) attributes 5.6% of the main thread to `#pumpEventsSafely` and
4.7% self time to `#pumpEvents` in
`packages/paperclip-runner/src/live/runnerd-codex-transport.ts`. Three
places started `setInterval(() => this.#pumpEventsSafely(), 5)`. Each
pass scans the session's committed-event window, so the cost is
`sessions x 200 passes per second x window size`, with nothing to
deliver.

**Expected behavior**

An open session that has no event to deliver does no periodic work. An
event that arrives is delivered at once, not at the next timer tick.

**Steps to reproduce**

1. `cd packages/paperclip-runner`
2. `PAPERCLIP_TRANSPORT_PUMP_BENCHMARK=1 pnpm exec vitest run
src/live/runnerd-transport-pump.benchmark.test.ts`
3. Read the `TRANSPORT_PUMP_BENCHMARK` lines.
`stateReadsPerSessionPerSecond` counts how often an open, idle transport
reads the control plane. `deliveryWaitMs` is the time from the moment a
reader can see a committed event to the moment the transport delivers
it.

**Paperclip version or commit**

Present on master at `c8ae02e5ce`. The profile is from `556b249897`.

**Deployment mode**

Hosted server with native agent sessions. The same code runs in local
development.

## What Changed

- `DurablePrpControlPlane` has a new `onReadableStateChange(listener)`.
It fires when the state that a reader of `store` can see may have
changed. The control plane calls it after every stored-state replacement
or save, after a saved checkpoint releases events that were waiting for
it, and after an authenticated runner moves the recovered cursor.
- The transport subscribes when it creates its control plane. A change
wakes one delivery pass in the next task (`setImmediate`). The pass
never runs inside the control plane's own commit, and several changes in
one task share one pass. This keeps the existing order: the control
plane finishes its commit before a reader reacts.
- The three `setInterval(..., 5)` timers are gone. `#startPump` and
`#stopPump` replace them and keep the same lifecycle: the pump runs from
session start, and it stops on release, controller retirement, failure,
and close.
- The delivery pass now reports what it left behind. Events that the
transport holds back itself (a pending turn start, a pending recovery
binding, a trace write that must be retried) are retried every 5 ms, as
before, and only while they are held. This is the only timer that
remains.
- A pass that stops after it exposes `turn/started` (so the driver can
bind the turn first) now continues in the next task. Before, the events
after it waited for the next 5 ms tick.
- `attachRun` wakes the pump when it moves the transport to the new run
identity.
- New regression test `src/live/runnerd-codex-transport-pump.test.ts`,
new opt-in benchmark
`src/live/runnerd-transport-pump.benchmark.test.ts`, a paced provider
fixture, and a test-only probe that records when each committed event
becomes readable.

How this meets `rules/sleep-aware-background-work.mdc`:

| Item | Answer |
| --- | --- |
| Trigger | The control plane's readable-state change. No timer. |
| Source of truth | The control plane's committed-event window and the
transport's delivery cursor. A notification is a hint to look at them. |
| Pending-work predicate | The pass returns `held` or `next_task`, or a
trace write is waiting. |
| Next deadline or retry | 5 ms, only while the predicate is true.
`next_task` uses one `setImmediate`. |
| Quiet condition | Nothing is held. Then the transport has no timer and
no scheduled task. Release, retirement, failure, and close stop it
completely. |

## Verification

**Reproduction and before/after measurement**

Run from `packages/paperclip-runner`:

```sh
PAPERCLIP_TRANSPORT_PUMP_BENCHMARK=1 pnpm exec vitest run src/live/runnerd-transport-pump.benchmark.test.ts
```

The benchmark opens 12 real transports against `runnerd` and a
credential-free provider fixture. It measures the open sessions while
nothing arrives. Then all 12 sessions stream one turn at the same time
(40 events each, one every 15 ms). "Before" is the unchanged source at
`e2cf9a670f`, which was the base of this branch when the measurements
were taken. "After" is this change on that base. Each cell shows two
runs, and the runs alternate between before and after. The host was a
shared macOS arm64 machine with other work running (load average 8 to
10), so absolute numbers carry that noise.

External checkpoint persistence. This is the mode that the server uses.

| Measure | Before | After |
| --- | --- | --- |
| Idle: control-plane state reads per session per second | 364 / 365 | 0
/ 0 |
| Idle: event-loop utilization, 12 new sessions | 1.5% / 1.5% | 0.0% /
0.0% |
| Idle: event-loop utilization, 49 retained events per session | 2.1% /
2.2% | 0.0% / 0.0% |
| Idle: process CPU, 49 retained events per session | 12.8 / 11.8 ms per
second | 0.05 / 0.04 ms per second |
| Delivery wait, mean (480 events) | 3.48 / 4.22 ms | 1.91 / 1.81 ms |
| Delivery wait, p50 | 3.16 / 3.97 ms | 0.83 / 0.58 ms |
| Delivery wait, p95 | 7.78 / 8.49 ms | 6.36 / 7.47 ms |
| Delivery wait, max | 8.84 / 9.23 ms | 9.07 / 8.86 ms |
| Delivery wait of the turn's terminal event, p50 | 2.37 / 2.58 ms |
0.08 / 0.05 ms |

The idle cost grows with the retained event window, as the profile
predicts. With `PAPERCLIP_TRANSPORT_PUMP_DELTAS=400` (409 retained
events per session, one run each): idle event-loop utilization 3.4%
before and 0.0% after, idle process CPU 21.5 ms per second before and
0.08 after. Delivery wait in that run: mean 5.93 ms before and 3.55 ms
after, p95 16.27 ms before and 8.03 ms after, max 66.94 ms before and
14.04 ms after.

Delivery wait is the time from the moment a reader can see a committed
event to the moment the transport hands it to the driver. It is equal or
better at every point. The p95 and the max did not move much. They
belong to the first events of each turn, which wait for the turn start
response. That wait is a separate 10 ms poll (`waitForRunnerCommand`),
and this pull request does not change it.

File persistence (the local journal, which writes and syncs on the main
thread):

| Measure | Before | After |
| --- | --- | --- |
| Idle: state reads per session per second | 363 / 364 | 0 / 0 |
| Delivery wait, 12 sessions, mean | 10.33 / 13.76 ms | 11.88 / 12.73 ms
|
| Delivery wait, 12 sessions, p50 | 6.04 / 6.87 ms | 6.34 / 6.20 ms |
| Delivery wait, 4 sessions and 400 events each, mean | 4.75 ms | 2.39
ms |
| Delivery wait, 4 sessions and 400 events each, p50 / p95 | 0.07 /
22.99 ms | 0.06 / 11.80 ms |

With 12 sessions the file journal saturates the main thread with its own
synchronous writes, and the wait is the time behind those writes. Three
more before-runs of that case gave means of 9.0, 14.0 and 20.2 ms, so
the two columns are the same within the noise. With 4 sessions the
journal does not saturate and the after column is better.

**Regression test (red, then green)**

```sh
pnpm exec vitest run src/live/runnerd-codex-transport-pump.test.ts
```

- `stops looking for events while an open session has none to deliver`
(file and external persistence). On the unchanged source: `expected 708
to be less than 100` and `expected 732 to be less than 100` state reads
in a 2 second idle window. With the fix: pass.
- `delivers each committed event in the next task, without a timer tick`
(file and external persistence). The test freezes every interval timer,
runs a full turn, and checks that each event is delivered before a task
that the test queues at the moment the event becomes readable. This is
an order check and does not depend on the speed of the host. On the
unchanged source: `the turn's events were delivered without a periodic
poll: expected false to be true`. With the fix: pass.
- Result: 4 of 4 failed on the unchanged source, 4 of 4 passed three
times with the fix, and 4 of 4 passed again after the rebase.

**Other local checks**

- `node ./node_modules/typescript/bin/tsc -p tsconfig.json --noEmit` and
`-p tsconfig.test.json --noEmit` in `packages/paperclip-runner`: pass on
the rebased commit.
- `pnpm exec vitest run src/live/runnerd-codex-transport` (15 files, 455
tests). These suites cover attachment, recovery, warm transition,
suspension, and release and resume on real `runnerd` processes. Before
the rebase, on a quieter host: all passed. On the rebased commit, at a
host load average of 15 to 20: 454 passed and 1 failed. The failure is
the terminal-replay row that is described below.
- `pnpm exec vitest run src/live src/control-plane
src/native-session-runtime.test.ts src/backends
src/drivers/dot/runnerd-dot-driver.test.ts
src/drivers/muse/runnerd-muse-driver.test.ts
src/drivers/codex/codex-protocol-integrity.test.ts --maxWorkers=3` (81
files): 1566 passed, 32 failed, 6 skipped. The failures:
- 30 failures in `runnerd-acpx-permission-ordering.test.ts` and
`runnerd-acpx-permission-forwarding.test.ts`. Both replace the control
plane with a fixture that appended events in place and relied on the 5
ms poll to notice them. The fixture now implements
`onReadableStateChange` and notifies its readers when it emits. Both
files pass (30 of 30).
- 1 failure in `runnerd-codex-transport.test.ts`, `stops a lost
runnerd's provider and escaped tool job from its lineage journal
(journal=true)`: `runner did not durably suspend before checkpoint`. It
passed 3 of 3 when run again alone.
- 1 failure in `runnerd-codex-transport-settlement.test.ts`, the
terminal-replay row of `settles only retained control authority without
starting another provider turn`: `native_cleanup_maintenance_unproven`,
with events still in the runner outbox. It failed again in 2 runs of
that test group and in the rebased run above, each time while the host
load average was 11 to 20. At a load average near 8 it passed 3 of 3
alone and 1 of 1 as a group with the fix. The unchanged master source
(`c8ae02e5ce`) gives the same result: it passed 2 of 2 alone and 1 of 1
as a group at a load average near 8, and the same row failed with the
same error (`native_cleanup_maintenance_unproven`, 27 events still in
the runner outbox) in a group run at a load average of 11 to 15. So this
row is sensitive to host load with or without this change. The failing
step is the retained-session maintenance path. That path builds its own
control plane, has no transport pump, has no listener on the new
notification, and has a fixed 30 second deadline.
- `pnpm verify` returns exit 2 (broader verification required, because
of the new fixture file). CI supplies the broad coverage.
- Not run locally: the server test suites, the repository-wide
typecheck, and the build. CI ran them on `4bce83f50a`: 59 checks passed
and 4 were skipped (Storybook deploy, Storybook visual regression,
Sandbox provider tests, Select Vitest files).

## Risks

- **A change that does not notify.** With no periodic pass, an event is
delivered only when something wakes the pump. Every wake comes from one
of a small, closed set: the five places where the store replaces or
saves its state, the checkpoint publication, the recovered-cursor
update, and the transport's own identity and start points. A new code
path that changes readable state without one of these would leave an
event undelivered until the next change. The regression test runs a full
turn with every interval timer frozen, in both persistence modes, so a
missing wake on the normal path fails the test. The existing transport
suites (attachment, recovery, warm transition, suspension, release and
resume) now run with no timer that could hide a missing wake.
- **Delivery in the next task, not later.** An event now reaches the
driver up to 5 ms earlier. The pass still runs in its own task after the
commit, and the same gates hold a terminal event until the turn start
response is released. The order of events does not change.
- **No protocol, schema, Rust runner, or provider pack change.** The
contract fingerprint inputs are untouched. Saved state from an older
release resumes the same way.

> 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 Fable 5.2 (`claude-fable-5-2`), Anthropic, in Claude Code. Tool
use and local code execution.

## Checklist

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

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

---------

Co-authored-by: Claude Fable 5.2 <noreply@anthropic.com>
canary/v2026.1111.0-canary.12
2026-11-11 11:08:50 -06:00
DottaandPaperclip cda23f08ac fix(db): preserve legacy project reads during rolling upgrades (#20860)
## Thinking Path

> - Paperclip controls agent work while web servers change versions.
> - The old and new servers can share a database during that change.
> - Older servers still read and write the project environment column.
> - Migration 0466 removes that column before those servers stop
serving.
> - A later repair alone leaves a committed missing-column window and
loses values.
> - This pull request applies the exact drop and repair in one
transaction.
> - Old reads and saved values survive while current placement behavior
stays unchanged.

## Linked Issues or Issue Description

Refs #20835 and #20852.

**What happened?**

Migration 0466 drops `projects.environment_id`. The previous server
still selects that column and reads it during admission. Applying a
separate repair afterward leaves a window in which those requests fail.
The drop also removes saved values.

**Expected behavior**

A database that has not yet applied 0466 must keep the column and its
values visible to concurrent old servers throughout the upgrade. Both
published migration entries must be recorded truthfully. Current project
responses must keep the new workspace placement contract.

**Steps to reproduce**

1. Use a database before migration 0466 with nonempty project
environment values.
2. Keep an old project writer transaction open and start the pending
migrator.
3. Start an old project read, then commit the writer.
4. Without this repair, the saved values disappear. With this repair,
the reader sees the preserved values and both migrations commit
together.

## What Changed

- Add migration 0467 to restore the legacy nullable UUID column and its
original foreign key. Leave the published 0466 SQL unchanged.
- Apply only the exact SHA-bound 0466/0467 pair in one transaction. Lock
the project table, retain nonempty values in a temporary table, execute
both real SQL files, restore values, and record both real journal
entries before commit.
- Apply the additive 0467 repair normally when 0466 was already
committed.
- Keep a deprecated database-only field and omit it from current project
API responses. Current placement logic stays unchanged.
- Add PostgreSQL regressions for concurrent old reads and writes,
idempotence, rollback, and an already-applied drop. Document the overlap
contract and retain the five newest generated snapshots.

## Verification

- Node 24 cached agent preflight passed.
- `pnpm exec vitest run
packages/db/src/project-environment-overlap-migration.test.ts
server/src/__tests__/project-environments.test.ts -t 'project
environment rolling migration|saves storage, not'`: 4 passed in 32.57
seconds against real disposable PostgreSQL.
- Before the owner fix, the regression had two failures: saved values
disappeared and the restore-failure case committed. The fixed case
proves that both DDL and journal entries roll back together.
- `pnpm --filter @paperclipai/db typecheck` and `pnpm --filter
@paperclipai/db check:migrations`: passed.
- `git diff --check`: passed. Migration 0466 bytes are unchanged. The
generated snapshot set is 0463 through 0467.
- Review found a raw DELETE response that still exposed the
database-only field. The delete projection now omits it; a real
authorized HTTP DELETE passed with successful cleanup. The concurrency
test now observes the old reader waiting inside PostgreSQL before
releasing the old writer. All three migration cases passed with that
barrier. Earlier HTTP fixture setup/cleanup failures remain recorded as
failed attempts; they were not product-regression passes.
- Candidate `047178538c0df3a9e382259a51635a6e46da5646` has fresh
Greptile 5/5 with no open findings. [Required final-head
CI](https://github.com/paperclipai/paperclip/actions/runs/43062354612)
passed, including build, typecheck, migration checks, all test shards
and the aggregate verification gate. The separate Superagent security
scan ended inconclusive/neutral; it did not produce a passed scan.
Deployed acceptance remains pending. No hosted database or app
deployment was changed by this branch.

## Risks

The pair takes a table lock for its transaction. Operators must retain
their normal lock and statement timeouts. Both SQL files and their exact
hashes must match before the pair runs. A failed restore rolls back the
drop and both journal entries.

If 0466 was already committed, 0467 restores the column and foreign key.
It cannot recover values that were already deleted by the earlier
release. This repair does not claim to recover those values.

## Model Used

OpenAI Codex (GPT-6), with tool-assisted implementation and real
PostgreSQL qualification. 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 and contains no internal
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-11 11:08:30 -06:00
DottaandClaude Opus 5.5 e0e6462a73 fix(heartbeat): keep an agent's explicit resume comment that a stale continuation dropped (#20856)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Agents coordinate on tasks through comments. A comment posted with
`resume: true` is an explicit request for more work; on a finished task
it reopens the task, for a person or for another agent.
> - A comment that arrives while the assignee's run is still settling
queues its own run. If the settling run then closes the task, that
queued run is cancelled before dispatch as a stale continuation
(`continuation_task_ownership_changed`).
> - For a person's comment, the outbox saves the unread message first
and the release drain reopens the task (chaos s9401/t72). For another
agent's explicit `resume: true` comment, nothing was saved. The request
was dropped and the task stayed `done`.
> - This pull request saves that agent comment too, with its explicit
resume intent. The existing release-drain rule for an agent's explicit
resume reopens the task and delivers the comment once.
> - The benefit is that a parent agent's "do this next turn" request is
not lost to a timing race. An agent comment without `resume: true` stays
communicative, as before.

## Linked Issues or Issue Description

**What happened?**
On a hosted staging instance, a parent agent posted a comment with
`resume: true` on its child's task: "a changed requirement for your NEXT
turn". The child's provider turn had finished 90 s earlier, but its run
was still settling (workspace copy-back). The comment queued a new run.
40 s later the settling run applied its `done` decision. The queued run
was then cancelled with `continuation_task_ownership_changed`. The task
stayed `done` and no run ever read the comment. The cancelled run itself
is not shown as a failure (the task is terminal, so task chat hides it,
and Sentry skips expected cancellations), so nothing told anyone the
request was lost.

**Steps to reproduce**
1. Run an agent on a task. While its run is still settling, post a
comment from another agent with `resume: true`.
2. Let the run close the task (`done`).
3. The comment's queued run is cancelled as a stale continuation. The
task is not reopened and the comment is never delivered.

**Expected behavior**
The task reopens for the explicit resume request, and the comment is
delivered exactly once, as for a person's comment.

## What Changed

- `deferUndeliveredCommentsOfStaleRun` finds, per owed comment, the
comment's own `issue.comment_added` activity record: written by its
author (another agent, not the assignee) with `resumeIntent: true`.
Merged runs, whose wake is rewritten as comments join, therefore keep
each comment's own intent.
- `saveUndeliveredUserComments` also admits those comments
(`explicitResumeCommentIds`). When only such comments remain, the
deferred receipt records the agent as requester and carries
`resumeIntent`/`followUpRequested` in its payload and wake context. The
release drain's existing rule for an agent's explicit resume on a `done`
task then reopens and delivers it. Person-comment behaviour is
unchanged.
- `doc/run-log-events.md` describes the rule.

## Verification

- RED on `origin/master` (`1d67e309e0`): `pnpm --filter
@paperclipai/server exec vitest run
src/__tests__/heartbeat-held-comment-wake.test.ts -t "explicit resume
comment"` fails: `expected [] to have a length of 1 but got +0` (the
comment is never delivered after the stale cancellation).
- GREEN: the whole file passes (29/29), including two-requester
merged-run tests in both orders, the new negative test (`does not reopen
a finished task for another agent's comment without resume intent`) and
the existing person-comment and exactly-once tests.
- Related suites `ownership-reconciliation-report`,
`execution-continuation`, `chat-wake-conversation-batch` plus this file:
118/118 tests pass locally. One `afterAll` cleanup hook in
`ownership-reconciliation-report` timed out under local machine load; CI
runs the suite fresh.
- `pnpm --filter @paperclipai/server exec tsc --noEmit` passes.
- Not done: no restart-chaos cell. The harness posts follow-ups as the
board user, which s9401/t72 already covers; an agent-authored `resume:
true` comment needs a two-agent workload the harness does not have.

## Risks

- Low risk. Only a comment whose own wake was an agent's explicit
`resume: true` qualifies, only on a task an agent run closed, and only
when no run read the comment. The assignee's own comments never qualify.
- The ownership reconciler's durable backstop
(`checkStaleDroppedComments`) still covers person comments only. If the
fast path fails, an agent's resume comment is not recovered later.

> 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, via Claude Code with
tool use (shell, file edits, read-only staging API reads).

## 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-11 10:57:47 -06:00
Nicky LeachandClaude Opus 5.5 a219dd7499 fix(runner): keep orphan-leaving tests apart from the verifying lineage reap (#20849)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Paperclip Runner stops a dead runnerd's provider and tool processes
from its process lineage journal
(`process_lineage::journal::reap_journals`)
> - The reap refuses its proof (`unattributed_orphan`) while any live
orphan (parent PID 1) of this user started after the dead runnerd's last
sample, whoever started it. This rule is deliberate
> - `cargo test` runs the runner-core lib tests in parallel in one
process, and several of those tests leave orphans of this user on
purpose
> - When one of those orphans starts during the reap in
`reaping_stops_a_dead_runners_provider_and_its_escaped_tool_job`, the
reap correctly refuses, and `ci / Verify Paperclip Runner (rust)` fails
> - This pull request keeps the orphan-leaving tests apart from that
reap with a test-only gate, and fixes a related zombie race in the
supervisor integration tests
> - The benefit is that the Runner Rust job stops failing on unrelated
pull requests

## Linked Issues or Issue Description

Refs #20834. Refs #20770.

**What happened?**

`ci / Verify Paperclip Runner (rust)` failed intermittently in
`process_lineage::journal::tests::reaping_stops_a_dead_runners_provider_and_its_escaped_tool_job`
(journal.rs:649), for example in run 43044317310 on #20834. The panic
was `ReapReport { verified: false, reason: Some("unattributed_orphan"),
journals: 1, signalled: 4, remaining: [4178] }`. The test's own thread
was TID 4142, so the leftover process started while the test ran. The
same test failed once on #20770 and passed on re-run.

**Expected behavior**

The reap proves the test's provider and escaped tool job gone, and no
other test can make it refuse.

**Steps to reproduce**

1. Build the runner-core lib tests for Linux (`cargo test --release -p
paperclip-runner-core --lib --no-run`).
2. Run the binary 40 times in `docker run --init --cpus 2 -e
RUST_TEST_THREADS=8`.
3. See `reaping_stops_a_dead_runners_provider_and_its_escaped_tool_job`
fail with `unattributed_orphan` (3 of 40 runs here).

## What Changed

- Root cause: the reap's orphan rule covers every process of this user
on the host. The reaping test and the orphan-leaving tests run in the
same test process at the same time. The test's own processes are always
attributed through the recorded group and session, so the leftover was
another test's orphan.
- `process_lineage::host_orphans` (test-only): a gate. A test that can
leave an orphan of this user holds `may_orphan()` until the orphan is
gone or a zombie. A test whose reap must verify holds
`no_foreign_orphans()`. Holders of `may_orphan()` never wait for each
other, so a test can hold several without a deadlock.
`wait_until_group_gone()` waits until a killed group has exited, because
a signal takes effect after `kill` returns.
- `reaping_stops_a_dead_runners_provider_and_its_escaped_tool_job` holds
`no_foreign_orphans()`.
- These lib tests now hold `may_orphan()`:
- `reaping_refuses_an_orphan_born_after_the_last_sample` (deliberate
orphan)
- the three `process_supervisor`
`*_stops_a_tool_job_that_left_its_session` tests (escaped tool job)
-
`orphaned_startup::restart_keeps_the_fence_while_a_child_survives_its_exited_leader`
(`sleep 30` outlives its leader)
- `exited_child_reader_deadline_bounds_continuous_descendant_stdout`
(flooding subshell outlives the Codex provider)
- `a_timed_out_turn_start_never_establishes_a_resumable_rejection`
(`sleep 10` can be orphaned for a moment during shutdown)
- the five `provider_backend` tests that use
`disconnected_receipt_limit_fixture` (`while :; do sleep 1; done`,
killed through `fence_local_receipt_limit`). The fixture returns its
guard first, so the guard is released after the executor stops the
provider
- the two `late_park_observation_*` tests (`cat >/dev/null` runs until
shutdown)
-
`a_held_cancellation_that_cannot_reach_the_provider_keeps_the_completed_turn`
(shutdown during `sleep 2`)
- New test
`a_verifying_reap_waits_for_held_orphans_and_holds_new_ones_back` checks
the gate's ordering.
- `tests/process_supervisor.rs`:
`forced_process_group_cleanup_stops_harness_and_worker` and
`natural_harness_exit_also_cleans_up_workers` slept 20 ms and then used
`kill -0` on a worker that was re-parented to init. `kill -0` succeeds
for a zombie, and init reaps it on its own schedule. The tests now check
that the process is not running (gone or a zombie), with no sleep.
Cleanup already proves that before it returns.
- No product code changes. The reap's orphan rule is unchanged.

Audit of the other tests in the job (`check:runner`):

- Only the runner-core lib binary can overlap the reap. `cargo test
--workspace` runs test binaries one after another.
- A second sweep, after review, checked every `/bin/sh` launch and every
external command (`sleep`, `cat`, `perl`, `nohup`, `touch`, `mv`) in
runner-core's `src/`, including `src/tests/*.rs`. The first sweep had
missed tests that start a provider through `CodexProvider::start` or
that live in `src/tests/`.
- Other lib tests that start processes are not affected. They run shells
that use only builtins (`configured_environment`, the other
`process_supervisor` and `codex_provider` tests,
`acpx_provider_session`), or run a short foreground `touch` that
finishes long before teardown
(`disconnected_codex_output_is_durable...`), or they start direct
children that the test waits for (`oom_score`, `exited_writer`,
`process_group_leader`). `publisher_crosses_real_codex_tool_boundary...`
is `#[ignore]`.
- The other `reap_journals` tests refuse before the orphan rule applies,
or (the orphan test) require only their own orphan in `remaining`.
- The `kill -0` checks in `tests/codex_provider.rs` and
`tests/acpx_park_preparation.rs` target direct children that runner-core
waits for before it returns, so they are deterministic.

## Verification

Linux runs used Colima on macOS (linux/arm64), `rust:1.97.1-bookworm`,
`--release`, and `docker run --init`, so orphans go to an init at PID 1,
as on CI.

- Full runner-core lib suite, `--cpus 2`, `RUST_TEST_THREADS=8`, 40
runs: before the fix, 3 runs failed with the CI signature. After the
fix, 0 runs failed, and no other test failed.
- The reaping test beside a temporary test that keeps leaving
short-lived orphans (not committed), `--test-threads 2`, 10 runs: before
the fix, 10 failed. After the fix, with the temporary test holding
`may_orphan()`, 0 failed.
- An orphan-leaving shell outside the test binary made the unfixed test
fail in 9 of 10 runs with the same signature. This confirms the
mechanism.
- After the review fix: macOS `cargo test --release -p
paperclip-runner-core --lib -- receipt_limit late_park_observation
a_held_cancellation process_lineage process_supervisor orphaned_startup
exited_child_reader_deadline a_timed_out_turn_start`: 65 passed. Linux
full lib suite (same settings) ×20: 0 failures.
- macOS (darwin-arm64): `cargo fmt --all -- --check` passes. `cargo test
--release -p paperclip-runner-core --lib -- process_lineage
process_supervisor orphaned_startup exited_child_reader_deadline
a_timed_out_turn_start`: 51 passed. `cargo test --release -p
paperclip-runner-core --test process_supervisor`: 7 passed.
- Worker cleanup tests: with `tini` as init, 60 unfixed runs on 0.5 CPU
with busy loops all passed, because `tini` reaps at once. In a container
whose PID 1 never reaps (`sleep infinity`), the unfixed tests fail at
`assertion failed: !process_exists(worker_pid)` because the killed
worker stays a zombie. The fixed tests pass. The unfixed tests depend on
how fast init reaps.

## Risks

- Low risk. All changes are in tests or `#[cfg(test)]` code.
- The gate covers the orphan producers that exist now. A new lib test
that leaves an orphan of this user must hold `may_orphan()`. The
`host_orphans` doc comment says so. Otherwise, it can make the verifying
reap refuse again.
- A process outside the test binary that leaves an orphan of this user
during the reap still makes the test refuse. This is correct product
behavior. CI runs no such process during `cargo test`.
- `no_foreign_orphans()` waits until every `may_orphan()` guard is
released, so the reaping test can wait a few seconds behind those tests.

> 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 editing, Docker). Extended reasoning was used.

## 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-11 08:55:30 -08:00
DottaandClaude Opus 5.5 e6b538efd2 fix(native): retire a superseded warm owner instead of failing the new run (#20844)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs keep a warm Runner between turns of the same task, so
the next run can start fast
> - When the next run needs a new provider session (for example after an
agent setting or skill changes), it first retires the previous run's
warm Runner
> - That retirement uses the previous owner's close. When the close
cannot prove checkpoint suspension, the new run failed before any
provider work, with the previous owner's error
`provider_transport_failed: runner did not durably suspend before
checkpoint`
> - A governed-wait park (a saved question or review request) always
leaves such an owner: its question callback stays unsettled by design. A
failed Launcher stop RPC leaves one too
> - Nothing retried the run, and the task rested as "recovery needed"
with no provider work done
> - This pull request lets the new run retire the old owner when that
owner is contained, and forces the containment when the close left it
unproven
> - The benefit is that a fresh run never fails on its predecessor's
close, and the chaos harness now covers this class

## Linked Issues or Issue Description

**What happened?**
A new run on a task whose previous run left a warm Runner retained
failed within two minutes, before any provider turn. The error was
`provider_transport_failed: runner did not durably suspend before
checkpoint`. The run row recorded `executionRecovery: { kind:
"bootstrap", providerWorkStarted: false }`. Two runs of the same agent
on two tasks failed this way 47 seconds apart, after a configuration
change made both start new provider sessions. One predecessor had parked
on a `request_human_input` question; its callback never settled.

**Expected behavior**
A run that has done no provider work does not fail because its
predecessor's Runner could not prove suspension. The new session never
resumes the old checkpoint. Core must only prove that the old owner is
contained, and then start.

**Steps to reproduce**
1. Run a warm native task that parks on a saved question (chaos workload
Q1).
2. After the asking run settles, change the agent's adapter
configuration, so the next run cannot resume the saved session.
3. Answer the question. The follow-up run retires the parked owner and
fails with the error above.

Related: #18373 added the stopped-owner reset path that this change
extends.

## What Changed

- `closeWarmNativeSession` (server native executor): during retirement
for a different normalized session, accept an unrecoverable close when
the old owner is contained. A plugin task owner must show its
`executionStopped: true` receipt and a completed handoff. Without a task
owner, the Runner's own durable suspension
(`settlement.runnerSuspended`) is the proof. Unsettled Core tool
callbacks of the discarded conversation no longer block the reset.
- With a plugin task owner, any close error qualifies once the Launcher
receipt exists. This includes a failed `stop` that
`releaseRunnerProcessOwnership` rethrows, and "Controller journal drain
timed out": the discarded session's journal lives in its own state root.
Owners without a task keep the narrower rule. `--matrix` now rejects
session faults.
- `createPluginRunnerTaskOwner`: add `retireSupersededTask()`. When
close left the receipt unproven, the new run sends one confirmed `stop`
(not resent after a confirmed stop), takes the credential snapshot, and
calls `complete`. It never reads the old state.
- Same-session continuation, a handoff that still fails, other close
errors and failed instruction collection still block replacement.
- Chaos harness: add session fault SR1
(`tests/restart-chaos/session-faults.ts`). With Q1 and `--lifecycle
warm` (or `--daytona-warm`), the board changes
`adapterConfig.idleTimeoutMs` after the asking run settles and before it
answers. `--tasks 2 --concurrency 2` is the two-concurrent-runs variant.
Q1's oracle applies, plus `session_rotated_before_answer` and
`follow_up_new_session`.
- Docs: `doc/plugins/PLUGIN_RUNNER_EXECUTION.md` and the chaos README
describe the new boundary and cell.

## Verification

- RED on master: `native-session-executor.test.ts -t "permanently
rejected plugin close during reset"` fails the `pending-semantic` and
`stop-unconfirmed-at-close` cases with
`NativeSessionCloseUnrecoverableError: provider_transport_failed: runner
did not durably suspend before checkpoint`. GREEN with this change: all
identity-transition cases pass (49).
- Chaos SR1 local, master server code (`--env local --lifecycle warm
--workloads Q1 --faults SR1`, seeds 301 with 2 tasks and 303 with 1
task): all tasks pass, and the follow-up started a new provider session.
The local parked owner closes cleanly, so the local cell does not
reproduce the failure. It needs a Daytona task owner.
- Chaos SR1 local with this change (seed 302, 2 concurrent tasks on one
agent): 2/2 pass, `follow_up_new_session` true for both.
- Not run: the Daytona SR1 variant. No runner image matches this
checkout's runner content ID, and this change does not touch runner
inputs.
- `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts
question-handoff scheduler config` passes. Server and harness `tsc
--noEmit` pass.

## Risks

- The reset no longer waits for unsettled Core tool callbacks of the
discarded session. Their run is already terminal and their provider is
stopped, so no result can reach a provider, and the new session never
replays them. Core-side tool executions keep their own receipts under
the old run.
- A local owner without a task needs the Runner's durable suspension. A
local owner that did not suspend still blocks replacement, as before.
- Previous-release story: this changes no persisted state. Warm owners
live only in controller memory, so a deploy drops them as before.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, agentic coding with
tool use (shell, file edits, test runs) 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-11 10:53:50 -06:00
DottaandClaude Opus 5.5 3ded7bbd91 fix(native): do not claim a server restart for a held same-run retry (#20832)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs on a remote sandbox (Daytona) retry provider startup on
the same run
> - A due remote same-run retry re-enters the restart-recovery
classifier
> - When that classifier holds the run, the hold note says "Automatic
recovery stopped after a server restart". No server restarted, so the
user reads a false cause
> - This pull request records the trigger of the hold and words a
same-run-retry hold as a stopped automatic retry
> - The benefit is that a held task names its real cause

## Linked Issues or Issue Description

Refs #20808
Refs #20820

**What happened?**
Two Codex runs on one Daytona sandbox failed in `session.open` (`bwrap:
loopback: Failed RTM_NEWADDR: Operation not permitted`). The executor
saved a `bootstrap_retry`. Thirty seconds later, the due retry entered
restart recovery and stopped with `ambiguous_remote_recovery_evidence`.
The task said "Automatic recovery stopped after a server restart". No
server restart happened. The second run was the board's Retry of the
first, and it failed the same way.

**Expected behavior**
A hold that a same-run retry creates must not claim a server restart.

**Steps to reproduce**
1. `cd server && npx vitest run
src/services/native-runtime/native-runner-restart-recovery.integration.test.ts
-t "same-run retry"`
2. On master the recovery action has no `trigger`, and its note contains
"server restart".

**Paperclip version or commit**
master at 5f38226290.

## What Changed

- Restart recovery records `trigger: "same_run_retry"` in the recovery
action evidence when the held run was a saved `retryable_failure`. It
reads this from the coordinator, so a deferred check or a check after a
real restart records the same trigger.
- `executionRecoverySettlementNote` says "Automatic retry of this run
stopped" for such a hold. It does not name a cause, because the
classifier can also stop it for `execution_recovery_budget_exhausted`.
Holds without the field keep the old text.
- Tests: one integration case with the staging signature and one unit
case for the note.

## Verification

- Red on master: the integration case fails because `evidence.trigger`
is absent; the unit case fails because the note says "server restart".
- Green here: `npx vitest run
src/services/execution-recovery-resolution.test.ts
src/services/native-runtime/native-runner-restart-recovery.integration.test.ts
-t "server restart|same-run retry|exited remote runner|remote bootstrap
retry held"`: 19 pass on the rebased head.
- `npx tsc --noEmit -p server/tsconfig.json`: clean.

## Risks

- Low risk. The change adds one optional evidence field and one note
branch. Recovery decisions do not change.
- Saved state: holds that an older release wrote have no `trigger` and
keep the old note. A mid-run deploy reads the field only when present.
- Not fixed here: the Codex Planning startup fault itself (`bwrap:
loopback: Failed RTM_NEWADDR`). #20820 changes the external-sandbox
planning profile so Codex no longer uses its network-isolated filesystem
helper.

> 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 (Anthropic) Opus 5.5, model ID `claude-opus-5-5`, run in Claude
Code with tool use (shell, file edit) and extended reasoning.

## 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 (no
doc change needed)
- [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-11 10:44:27 -06:00
DottaandClaude Fable 5.2 464e1de9b1 perf(issues): decide task reads for a list in one batch (#20858)
The task routes that filter several rows for one actor (summaries, lists, blockers, relation edges and the chat bootstrap) asked for one authorization decision per task, all at the same time. Each decision sent a task-row read and a copy of the task-privacy statement, which is more than 1 KB of SQL, so a request for 50 summaries sent 127 to 279 statements on every pool connection at once.

The routes now ask the authorization service for all decisions in one batched read. It reads the task rows and applies the same privacy predicate to all of them in one statement, and reads membership, agent and run policy once. Within a batch, the authorizing user's membership is read once, and the boundary checks of a low-trust agent, which stay per task, run at most eight at a time.

A request for 50 summaries now sends 22 to 28 statements (65 for a low-trust agent). Tests compare the batch with one check per task for 24 actors.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 10:42:21 -06:00
DottaandClaude c8ae02e5ce fix(native): start a fresh session when an accepted run's provider state was not saved (#20811)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A native run in a per-turn remote sandbox saves a provider state
backup after its result is accepted. The sandbox is then released, so
the backup is the only copy of the provider session.
> - When that backup fails to save (for example, #20772: storage
throttling), the run keeps its accepted result. Nothing records that its
checkpoint can no longer be resumed.
> - The next run on the session rebinds that checkpoint and asks the
Runner to resume. The Runner has no state and refuses (#20757). With the
normal policy, the run opens a replacement session after that failed
attempt. With a governed-wait or unfinished-goal checkpoint, the run
fails with `native_provider_state_incompatible`, and only Retry or the
next message starts fresh.
> - The server already knew the state was gone when the backup failed.
The failed resume is wasted work.
> - This pull request marks the accepted run's checkpoint as not
resumable at the moment the backup fails. The next run then starts a
fresh provider session directly.
> - The benefit is that a lost backup no longer costs a failed run or a
failed resume attempt before the task continues.

## Linked Issues or Issue Description

Refs #20757 (the missing-state refusal that the next run reaches today).
Refs #20772 (the throttled backup upload that loses the backup). Refs
#20809 (a related fix for governed-wait follow-ups that reach the
missing state; this PR stops most of them from reaching it).

**What happened?**

1. A native run in a per-turn sandbox finished, and its result was
accepted.
2. `harness_state.backup.persist` failed. The run recorded
`native.post_result_failure`. Its run row kept a normal
`sessionCheckpoint`.
3. The sandbox was released.
4. The next run on the same session selected that run as its resume
source, and `rebindNativeSessionCheckpoint` returned the checkpoint.
5. The Runner found no saved state and refused with `PRP provider resume
state is unavailable`. The run then opened a replacement session after
the failed attempt, or (governed-wait or unfinished-goal checkpoint)
failed with `native_provider_state_incompatible`. Retry or the next
message then started fresh.

**Expected behavior**

The next run starts a fresh provider session at once, because the server
already knows the saved state is not there. The accepted result and the
task outcome stay as they are.

**Steps to reproduce**

1. Give a run row an accepted result and a normal `sessionCheckpoint`.
2. Make its close checkpoint fail in a per-turn sandbox
(`reusableLeaseConfigured: false`).
3. Select the next run's resume source with `findNativeSessionResumeRun`
and call `rebindNativeSessionCheckpoint`.
4. On `master` it returns the checkpoint, so the next run tries to
resume state that does not exist.

**Paperclip version or commit**

`master` at `556b249897`.

## What Changed

- `server/src/services/native-runtime/native-provider-state-unsaved.ts`
(new): `markAcceptedRunProviderStateUnsaved` adds
`nativeProviderStateUnsaved: { code:
"native_provider_state_incompatible", reason: "harness_backup_not_saved"
}` to the run's `runnerProfileJson`. One conditional `UPDATE` writes it
only when the run's coordinator holds a result with `schema_status =
'accepted'`. `clearRunProviderStateUnsaved` removes it.
- `server/src/services/native-runtime/native-session-executor.ts`: the
remote close checkpoint (`checkpointRemoteRunner`) now wraps the
existing body. When the checkpoint throws in a sandbox without a
reusable lease, it calls the marker, writes one run-log line, and throws
the original error. A marker or log failure cannot replace that error.
If a later checkpoint of the same run saves, it clears the mark.
- `server/src/services/native-runtime/native-session-resume.ts`:
`rebindNativeSessionCheckpoint` returns `null` for a source with the
mark, as it already does for `errorCode =
native_provider_state_incompatible`. `findNativeSessionResumeRun`
projects the new key.
- `doc/execution-semantics.md`: one additive paragraph.
- Tests in `native-session-resume.test.ts` and
`native-session-executor.test.ts`.

Guard conditions. The mark is written only when all of these are true:

- The close checkpoint threw before the durable backup was stored (after
the upload's own bounded retries). A later local publication, stamp or
cleanup failure does not mark the run.
- The target is a remote `sandbox` with `reusableLeaseConfigured !==
true`. A retained sandbox or an SSH host keeps the live state, so its
resume path is unchanged.
- The run holds an accepted result. A failure before acceptance changes
nothing.

Saved state from the previous release: run rows without the key keep
today's resume path. An older server that reads a row with the key
ignores it, and so it behaves as it does today. No schema change.

User-visible: the next run's transcript shows "Paperclip session
started" instead of "resumed", with a new session id. The source run's
log shows `provider state was not saved after the accepted result; the
next run starts a fresh provider session`. The fresh session gets the
full task context, as for other fresh starts.

## Verification

- `cd server && npx vitest run
src/services/native-runtime/native-session-resume.test.ts -t "not
resumable only after|provider state was not saved"`: 9 pass (6 pure, 3
embedded Postgres).
- `cd server && npx vitest run
src/services/native-runtime/native-session-executor.test.ts -t "marks
the provider state unsaved|uses the bundled Linux image authority for
remote opencode-new-durable"`: 8 pass. They include a durable store
failure (marked) and a local publication failure after the store (not
marked).
- Red on `master` (source files reverted, tests kept): the 3 "starts the
next run in a fresh provider session directly" cases fail (`expected {…}
to be null`), the DB "accepted" case fails the same way, and the
executor per-turn case fails (`mark` called 0 times).
- Guard removal checks:
- Sandbox guard replaced with `true`: the "retained sandbox" case fails
(`mark` called once).
  - `!checkpointBackupStored` removed: the "publish-failed" case fails.
- `schema_status = 'accepted'` removed from the marker: the "rejected"
case fails (`expected true to be false`).
- Negative cases that pass: no result, rejected result, retained
sandbox, records without the key or with another code (all three
recovery policies), and a later save that clears the mark.
- `cd server && npx tsc --noEmit -p .`: clean.
- Not related to this change and failing on `master` in this worktree
too: `runtime-selection.test.ts` connector skill cases (missing
`.claude/skills/*/SKILL.md`) and `native-session-resume.test.ts`
"archives a damaged prior epoch" (`ENOENT … authority-epochs`). Required
CI is the authority for those suites.

## Risks

- Low. The change only skips a resume that cannot succeed. The fresh
session uses the same workspace and full task context, like a Retry
after `native_provider_state_incompatible`.
- If the backup failed but the provider state were still somewhere
recoverable, the next run would lose provider context. For a per-turn
sandbox without a reusable lease, the restore path uses only the backup,
so this does not apply. Retained sandboxes and SSH hosts are excluded.
- One extra conditional `UPDATE` runs only on the failure path. The
normal path before `turn.start` has no new waits or work.
- `doc/execution-semantics.md` is also edited by #20809. The paragraphs
are at different places; whichever merges second rebases.

> 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, via 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 <noreply@anthropic.com>
2026-11-11 10:36:00 -06:00
DottaandClaude Opus 5.5 1b39cf1794 fix(native): retry a contended provider-pack admission write (#20850)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A native OpenCode run on a remote sandbox must save a provider-pack
admission receipt on its run row after the provider session opens.
Artifact retention refuses to continue without it.
> - The admission write locks the coordinator row
(`native_run_finalizations`) and then the run row, each with a 1000 ms
`lock_timeout`. The runner's durable checkpoint writes lock the same
coordinator row for the whole checkpoint transaction.
> - On a loaded database one checkpoint transaction can outlast one lock
wait. The admission write then fails with `55P03`, the state stays
`pending`, and artifact retention fails the run as
`native_provider_artifacts_admission_unverified` before the first turn.
The task rests blocked.
> - #20682 fixed the same symptom for a run-event append on the run row.
This is the coordinator-row half.
> - This pull request retries a contended admission write a bounded
number of times, each in a new transaction that re-proves the claim. It
also names the database cause in the run log and adds a chaos option
that makes every fresh sandbox take the pack upload and admission path.
> - The benefit is that a healthy fresh sandbox is no longer failed and
blocked by ordinary lock contention.

## Linked Issues or Issue Description

Refs #20682 (same symptom, run-row lock; this is the coordinator-row
lock).

**What happened?**
On a hosted staging instance, an OpenCode run on a fresh Daytona sandbox
found no matching preinstalled provider pack (`provider_pack.select`
reason `cached_packs_unverified`). It uploaded and verified the pack (61
s), started the runner, and the runner authenticated to the durable
control plane. 3.4 s later the run log shows `native provider-pack
admission receipt could not be saved`.
`nativeProviderPackAdmissionState.state` stayed `pending`. The run
failed with `native_provider_artifacts_admission_unverified` before any
provider turn. The task rests `blocked` with no Retry.

**Steps to reproduce**
1. Start a native run that waits for its claim (`waitForClaim: true`, as
a fresh run with artifact storage does).
2. Hold its `native_run_finalizations` row `FOR UPDATE` in another
transaction for 1.6 s, as a checkpoint write does.
3. Call `recordNativeProviderPackAdmission`. It fails with `canceling
statement due to lock timeout` (`55P03`).

**Expected behavior**
The admission waits out the checkpoint write within a bound, re-proves
the claim, and saves the receipt.

## What Changed

- `recordNativeProviderPackAdmission` retries a write that waits for its
claim when it fails with `55P03`, `40P01` or `40001`, up to 5 attempts
with 100–400 ms back-off. Each attempt is a new transaction with the
same checks (claim, run, epoch, receipt). A stale claim still returns
`false`. Other errors still throw. A write that does not wait for its
claim keeps its `NOWAIT` refusal.
- New `nativeProviderPackAdmissionFailureCode` reads the SQLSTATE from
the error or its cause.
- The executor's run-log line now ends with `(cause=<SQLSTATE or
native_* code>)`, so a lost admission is diagnosable. It carries no
paths or secrets.
- Restart chaos: `--daytona-pack-mismatch <image@sha256:…>` starts
sandboxes from another image while the controller keeps
`--daytona-image`'s runnerd and pack. Every fresh sandbox then takes the
upload and admission path, which the matched-image check made
unreachable. Config validation and README updated.
- `doc/execution-semantics.md` describes the bounded retry.

## Verification

- RED on `origin/master` (`1d67e309e0`): `pnpm --filter
@paperclipai/server exec vitest run
src/services/native-runtime/native-provider-pack-admission.test.ts -t
"checkpoint write holds"` fails with `canceling statement due to lock
timeout` (`55P03`), the staging error.
- GREEN: the same file, 39/39 pass, including the existing fence tests
(`cannot publish while another transaction holds the coordinator claim`,
revoked-claim recheck) and the new bound test (`gives up after the
bounded retry window while the claim stays held`, under 15 s).
- `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts
tests/restart-chaos/scheduler.test.ts`: 34/34 pass, including the new
option test.
- `pnpm --filter @paperclipai/server exec tsc --noEmit` and `tsc
--noEmit -p tests/restart-chaos/tsconfig.json` pass.
- Not run: a Daytona chaos wave with `--daytona-pack-mismatch`. It needs
a runner image built at this revision. The staging cause is inferred
from the timing and the code path; the original error was swallowed,
which is why this PR adds the cause to the run log.

## Risks

- Low risk. The normal path has no contention and makes one attempt.
Under contention the write waits at most about 11 s before the first
provider turn. Without this change that case failed the run.
- A retry cannot widen authority: every attempt re-checks the
coordinator claim, lease expiry, run status and admission epoch.
- When admission still fails after the retries, the run fails as before.
This PR does not change that recovery path.

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

## Model Used

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

## 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-11 10:28:55 -06:00
DottaandClaude Fable 5.2 381b862135 fix(native): let a finish through after one stale-result refusal; rebuild a missing instruction bundle (#20833)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A native Codex run ends its task with a Paperclip terminal call
(`paperclip_finish` or `paperclip_block`)
> - The Runner refuses a terminal call that the agent decided before it
saw earlier tool results (`terminal_call_before_results`). This protects
against stale calls held across a restart.
> - On staging, Codex cancelled a nested exec cell. Codex never sent
`item/completed` for one Paperclip tool call in that cell. The Runner
then refused every later finish and block in the turn, and the turn
ended with no semantic result.
> - The same-run recovery then resumed the run on a new controller
container. The run's admitted instruction bundle is stored on the
controller's local disk, so it was missing there, and runnerd failed
with ENOENT.
> - This pull request makes a refusal name a returned call once, and
rebuilds a missing admitted instruction bundle before the Runner backend
starts.
> - The agent-directory lock part of the same failure chain ("Agent
directory operation is already active") is fixed separately by #20829.
> - The benefit is that the agent can finish after one refusal, and a
resumed run does not fail because a deploy moved it to a host without
the bundle.

## Linked Issues or Issue Description

Refs #20829 (the agent-directory lock wait and dead-owner release; this
PR does not change that lock).

**What happened?**

Staging Codex runs on Daytona sandboxes failed after about 30 minutes of
work:

1. Early in the turn, the agent called `paperclip_search_assigned_tools`
from a nested exec cell. Codex cancelled that cell. The controller
returned the result, but Codex never sent `item/completed`, so the
Runner never committed `tool.execution.completed` for it.
2. At the end, the agent called `paperclip_finish` four times and
`paperclip_block` once. The Runner refused each one with
`terminal_call_before_results` ("Those calls have now returned"). The
old call stayed "returned but unseen" for the rest of the turn.
3. The turn completed with no semantic result
(`native_finalization_missing`). Same-run recovery scheduled another
attempt.
4. On the new controller container, the attempt failed with
`runner_ingress_unavailable: runnerd process failed: ENOENT ... scandir
'/paperclip/instances/default/runtime-context-assets/bundles/<sha>'`
(one run) or `ENOENT ... lstat` of the same kind of path (a run during a
rolling deploy). In both, `<sha>` is the run's admitted instruction
bundle. The asset root is controller-local, and the new containers had
no volume.
5. The last attempt failed with "Agent directory operation is already
active" (fixed by #20829).

**Expected behavior**

The agent finishes after one refusal. A resumed run rebuilds its exact
admitted instruction bundle on the new host.

**Steps to reproduce**

1. Run a Codex task that calls a Paperclip tool, then
`paperclip_finish`.
2. Drop Codex's `item/completed` for the first tool call (new chaos
fault `--faults PV2 --provider-fault-message tool_completed`).
3. On master, every `paperclip_finish` is refused and the run ends with
`native_finalization_missing`.
4. Separately, delete `runtime-context-assets/bundles/<instruction
digest>` and resume a run that pinned it: runnerd fails with ENOENT.

**Paperclip version or commit**

master at 1d67e309e0.

## What Changed

- `packages/paperclip-runner/src/live/terminal-call-freshness.ts` and
`runnerd-codex-transport.ts`: when the Runner refuses a terminal call
with `terminal_call_before_results`, it records the refused call id with
the returned-but-unseen call ids it named. A later terminal call ignores
those calls only when the refused call's `tool.execution.completed` (the
refusal reached the provider) is committed before the later call starts.
A terminal call queued before the refusal reached the provider, a call
that was pending at the refusal, and a call made after it are judged as
before. The record is in memory, so after a Runner restart the agent can
get one more refusal.
- `server/src/services/native-runtime/runtime-context.ts`: new
`rehydratePinnedInstructionAsset`. When the admitted instruction bundle
is not present on this host, it rebuilds it from the agent's instruction
revision whose content has the admitted digest (single-file bundles), or
from the current instructions when they still produce the admitted
digest. It never substitutes different content. When nothing matches, it
leaves the context unchanged.
- `server/src/services/native-runtime/native-session-executor.ts`:
`createRunnerdBackendWithinSessionClaim` calls it before the backend
starts. When the bundle is present this is two local file reads.
- Restart chaos: new provider message `tool_completed` for the Codex
shim (PV1/PV2/PV3). It targets Codex's `item/completed` for the first
Paperclip tool call that is not a finish or block. Config accepts it
with A1 and C1 on Codex only. The README documents it.

## Verification

- RED on master (fix reverted, new tests kept): `vitest run
src/live/terminal-call-freshness.test.ts` in `packages/paperclip-runner`
fails with `expected [ 'paperclip_search_assigned_tools' ] to deeply
equal []`.
- GREEN: `terminal-call-freshness.test.ts` 13 passed; the transport
tests for terminal calls pass, including a new test where the first
finish is refused and the second is dispatched, and tests where a queued
stale terminal call and a late-returning pending write are still
refused.
- `server/src/__tests__/runtime-skill-preservation.test.ts`: 9 passed,
including the new test that deletes an admitted instruction bundle and
its manifest, then rebuilds the exact bundle from the revision history
while a newer revision exists.
- `vitest --config tests/restart-chaos/vitest.config.ts
provider-faults.test.ts`: 22 passed.
- Chaos wave `w-20261111T150212-s301` (`--env local --adapters codex
--workloads A1 --faults PV2 --provider-fault-message tool_completed
--tasks 1 --seed 301`): 1/1 PASS, `provider_fault_applied` applied=1,
R1–R6 and the A1 oracle pass.
- `pnpm verify` asks for broader verification (a restart-chaos README
change is a global CI input); required CI supplies it.

## Risks

- The terminal-call change lets a later finish through when a returned
call never reaches the agent. The earlier refusal named that call and
reached the provider first, so the agent decides with that knowledge.
- The instruction rebuild adds two local file reads before the Runner
backend starts. It writes only when the bundle is missing, and only
bytes whose digest equals the admitted digest.
- Not covered here: a multi-file (legacy) instruction bundle whose
content changed since admission still cannot be rebuilt; a skill bundle
with neither a version id nor a preservation receipt is not rebuilt; the
runner-side quarantine after an S3 `SlowDown` during the post-turn
harness backup.

## Model Used

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

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] 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 Fable 5.2 <noreply@anthropic.com>
2026-11-11 10:25:37 -06:00