Commit Graph
2464 Commits
Author SHA1 Message Date
DottaandClaude Opus 5.5 814fe5d20f fix(runner): issue the remote bootstrap ticket at dispatch and retry unauthenticated starts (#21073)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs on a remote sandbox start `runnerd` in the sandbox, and
Paperclip connects to it through provider ingress and authenticates it
with a one-use bootstrap ticket
> - The ticket lives for 60 seconds, but Paperclip issued it before the
remote launcher staged its files in the sandbox
> - Under load, staging took 60 to 81 seconds, so `runnerd` presented an
expired ticket and every connection was closed. The run failed with
`runner_ingress_auth_failed`, and the task stopped behind a manual Retry
> - This pull request issues a fresh ticket right before dispatch,
retries these failures automatically when the controller checkpoint
proves no runner ever authenticated, and adds spans that attribute the
slow startup window
> - The benefit is that a slow but healthy start authenticates, a failed
start recovers on its own, and the remaining startup delay becomes
measurable

## Linked Issues or Issue Description

**What happened?**

On a hosted instance, 9 runs of one Codex agent on Daytona failed in
about 70 minutes with `native_session_interrupted` and the message
`runner_ingress_auth_failed: Runner ingress did not complete PRP
authentication before the deadline.` In the same window only 6 runs
succeeded company-wide. The run log showed Paperclip reconnecting for 60
seconds while `runnerd` reported that its peer closed each WebSocket. In
294 remote runs with timing spans, the time between the bootstrap ticket
issue (end of `runner.transport.connect`) and the process dispatch
(start of `runner.process.launch`) decides the outcome without
exception:

- 60 s or more: 23 of 23 runs failed authentication.
- Under 60 s: 271 of 271 runs authenticated, in 0 to 1 s.

The tasks did not recover by themselves, because the failure was
classified as ambiguous state.

**Expected behavior**

A runner that starts slowly but correctly authenticates. If ingress
authentication still fails before any runner authenticated, Paperclip
retries the start automatically. The time between the start request and
the runner connection is visible in run spans.

**Steps to reproduce**

1. Run a Codex agent on a Daytona sandbox environment that uses provider
ingress.
2. Make remote staging (`runner.runtime.stage`, inside the remote
launcher) take 60 seconds or more. Many concurrent runs of the same
agent do this on staging.
3. See that `runner.prp.authenticate` fails after 60 seconds and the run
ends with `runner_ingress_auth_failed`.
4. The regression test `runnerd-bootstrap-ticket-refresh.test.ts`
reproduces this locally with a 75-second staging delay.

**Paperclip version or commit**

`4e519bd4` (deployed), reproduced on `origin/master` at `4228bcf89c`.

**Deployment mode**

Hosted (cloud), controller on Railway, sandboxes on Daytona.

**Agent adapter(s) involved**

`paperclip_runner` with the Codex app-server provider (`gpt-6.1-sol`),
remote sandbox target, provider-ingress transport.

## What Changed

- `RunnerProcessLaunchSpec` and `spawnRunner` accept
`refreshBootstrapTicket`. A restart does not inherit it.
- The runnerd Codex transport passes a refresh that issues a new
bootstrap ticket and flushes the controller checkpoint. It does not do
this for a warm-transition launch.
- The remote sandbox launcher calls the refresh after `ensureArtifact`
and immediately before the launch RPC, and puts the new ticket in the
launch environment.
- `inspectUnauthenticatedBootstrapCheckpoint` proves from a controller
checkpoint that no runner authenticated: no connections, no leases, no
used tickets, no events, and only pending commands.
- `nativeSessionFailureSourceCode` classifies the outbound ingress
startup failures as `runner_ingress_startup_failed`.
`nativeProviderRecoveryEvidence` treats it as pre-session
(`bootstrap_retry`) only when that proof holds for the exact run
identity and there is no provider session or provider event. Restart
recovery and failed-run retry accept the new code with the same rules as
similar startup codes.
- Remote startup now records `runner.control_plane.open`,
`runner.launch.prepare`, `runner.checkpoint.register` and
`stage.asset.codex_skills.verify`. The 6 to 11 minute gap between
`native.process_start_requested` and `runner.transport.connect` had no
spans before.

## Verification

- Red/green:
`packages/paperclip-runner/src/live/runnerd-bootstrap-ticket-refresh.test.ts`
starts a real `runnerd` with a 75-second staging delay. Without the fix
it fails with `runner never connected`. With the fix it opens the
session.
- Red/green: `native-session-executor.test.ts` "launches runnerd with a
bootstrap ticket issued after remote staging". Without the fix the
launch RPC gets the stale ticket.
- Red/green: `native-controller-recovery.integration.test.ts` "retries a
fresh remote start whose launched runner never authenticated through
provider ingress" (embedded Postgres). Without the fix the failure is
`native_session_interrupted` and `ambiguous_state`.
- `durable-prp-control-plane.test.ts`: the new checkpoint proof, its
negative cases, and the restart rule for the refresh.
- `native-run-trace.test.ts`: remote startup spans are recorded without
duplicate launcher spans.
- Focused suites pass: runner package 5 files and 235 tests; server 9
files and 1,607 tests, including `native-session-executor`,
`native-runner-execution-driver`, `remote-launch-assets`,
`native-restart-recovery`, `runner-prp-outbound` and the recovery
integration tests.
- `tsc --noEmit` passes for `packages/paperclip-runner` and `server`.

## Risks

- A launch now issues two bootstrap tickets. The first one is never sent
to `runnerd` and expires in 60 seconds. Bootstrap counters already allow
more than one issued credential.
- The new retry path needs a checkpoint proof that no runner
authenticated, so a run that did provider work cannot be retried this
way. The existing attempt budget still bounds the retries.
- This change does not remove the 6 to 11 minute controller-side delay
before `runner.transport.connect`. The new spans make it attributable
for a follow-up change.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with tool use
(shell, browser, code search) 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-12 05:02:30 -06:00
DottaandClaude Opus 5.5 b7693e2076 fix(runner): decode native inputs saved under ACPX 0.13.1 (#21062)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Paperclip Runner defines the native execution input that every
native run saves and every later run reads back
> - The ACPX upgrade to 0.22.0 (#20770) changed the qualified
`acpxVersion` in each ACPX profile
> - The input parser accepts only the current `acpxVersion`, so it
refuses every input that was saved under ACPX 0.13.1
> - Run preparation reads the previous run's saved input for the same
task, so each ACPX agent with an older task session fails before its
provider starts
> - This pull request lets the parser decode inputs saved under ACPX
0.13.1 and normalizes them to the current qualified profile
> - The benefit is that saved ACPX agents and tasks work again after the
upgrade, with no manual edit

## Linked Issues or Issue Description

Refs #20770

**What happened?**
Every run of an ACPX agent (Claude through ACPX) failed about 20 seconds
after it started. The run error was `input.provider.profile does not
match the qualified ACPX v1 profile` with the failure cause
`native_execution_input_invalid`. The run log showed no runner output,
only the preparation messages. Five runs on five different tasks failed
this way in 40 minutes. The previous successful run on one of those
tasks saved this provider profile:
`{"agent":"claude","acpxVersion":"0.13.1","agentProfileVersion":1,...}`.
New runs saved `acpxVersion` `0.22.0`. Run preparation reads the
previous run's saved input (for example, the instruction working copy
step parses the input of the task session's last run). The parser
refused `0.13.1` and the error stopped the run.

**Expected behavior**
A run on a task whose earlier run was saved under ACPX 0.13.1 prepares
and starts normally. The ACPX upgrade carries saved sessions forward.
The session identity already pins `0.13.1` for this reason
(`recovery-identity.ts`). The saved input must decode too.

**Steps to reproduce**
1. Run an ACPX agent on a task with Paperclip before #20770, so the run
saves `provider.profile.acpxVersion: "0.13.1"`.
2. Deploy a build that includes #20770.
3. Wake the agent on the same task.
4. See the run fail in preparation with `input.provider.profile does not
match the qualified ACPX v1 profile`.
5. The new unit test reproduces this with the exact saved profile shape.

**Paperclip version or commit**
master at 424e2c6488. The failure is on a deployed build at 3c058c37e6,
which includes #20770.

**Deployment mode**
Hosted cloud (staging), Daytona remote workspace, `paperclip_runner`
adapter.

**Agent adapter(s) involved**
`paperclip_runner` with provider `acpx`, ACPX agent `claude`. All ACPX
agents with inputs saved before the upgrade have the same failure.

## What Changed

- `profile-compatibility.ts`: add `isDecodableAcpxVersion`. It accepts
the current qualified ACPX version and `0.13.1`, the only ACPX version
that was qualified before.
- `native-execution.ts`: the ACPX profile check uses
`isDecodableAcpxVersion` instead of a strict equality with the current
version. The parser continues to write the current qualified version
into the parsed profile, so all later checks and digests see the current
profile.
- `native-execution.test.ts`: a regression test with the exact saved
Claude profile shape. It confirms that the saved input decodes, that the
result is the current profile, that the parse is idempotent, and that
other versions (`0.13.0`, `0.21.9`, `0.23.0`, empty, null, number) are
still refused.

## Verification

- Red: the new test failed on master with `input.provider.profile does
not match the qualified ACPX v1 profile`, the same error as the failed
runs.
- Green: `npx vitest run src/contracts/native-execution.test.ts` in
`packages/paperclip-runner`: 115 passed.
- `npx vitest run` on the ACPX `profile-compatibility`,
`cursor-cloud-candidate-profiles`, `kimi-installation`,
`pi-installation`, `copilot-installation` and `recovery-identity` tests:
140 passed, 1 skipped.
- Server: `native-execution-input.test.ts` (42 passed) and
`heartbeat/instruction-working-copy.test.ts` (14 passed).
- `tsc --noEmit` for `packages/paperclip-runner`: clean.
- `pnpm verify --explain` selects the full suite because the runner
package changed. CI supplies that coverage.

## Risks

- Low risk. The change only widens decoding from one ACPX version to two
named versions. Unknown versions are still refused.
- A decoded 0.13.1 input becomes the current qualified profile. This
matches the upgrade design: the current ACPX loads sessions that 0.13.1
wrote, and the session identity already uses 0.13.1 for all versions.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with tool use
(shell, browser for read-only staging API reads) 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-12 03:35:32 -06:00
DottaandClaude Opus 5.5 4228bcf89c fix(opencode): never save the model's reasoning as its chat reply (#21051)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents that use OpenCode models reply in chat through the Runner's
OpenCode driver. The driver settles one final response, and Paperclip
saves it as the reply comment
> - In chat-eval cohort 5 (OpenCode with
`openrouter/deepseek/deepseek-v4.1-flash-1017`), four saved replies were
the agent's reasoning, for example "Confirmed down. Now report honestly.
... I'll finish with paperclip_finish."
> - The run events show the cause. The model wrote a reasoning part,
then wrote the same text again as an ordinary text part, and ended the
step without `paperclip_finish`. The driver took that copy as the final
answer. This is not the #20999 path: no completion call happened
> - A second, related problem is in the #20999/#21025 path. When the
model writes an answer, calls `paperclip_finish`, and then writes its
own new answer, the driver can save both texts, or a status note
together with the answer
> - This pull request drops the repeated reasoning, lets the existing
empty-response correction ask for the real answer, and saves the
post-completion message alone unless it is a short line that refers back
> - The benefit is that people never see the agent's planning text or
tool names as its reply, and the eval catalog now fails when they do

## Linked Issues or Issue Description

Refs #20999
Refs #21025

**What happened?**
The saved chat reply held the agent's reasoning, not an answer. Cohort 5
cases: `connection-setup.unreachable-mcp`, `plugins.configure-plugin`,
`tasks.website-task`, `teams.install-team`. In each case the reply text
is byte-identical to the reasoning part before it, and the run result
evidence is `provider:opencode:agent-message` (no `paperclip_finish`
call). Also, `recovery.explain-stall` saved its answer twice, and
`status-cards.refresh-card` saved a "STATUS: ..." note above its answer.

**Expected behavior**
The saved reply is the model's answer to the person. It never holds
reasoning, completion tool names or report field names. A short closing
line such as "Answered above" still keeps the earlier answer.

**Steps to reproduce**
1. Use an OpenCode agent with
`openrouter/deepseek/deepseek-v4.1-flash-1017`.
2. Ask a chat request that the model finishes without a completion call
after a long reasoning step.
3. Read the saved reply with `GET /api/issues/{id}/comments`. It holds
the reasoning text.

**Paperclip version or commit**
master at 98c18784c6 (cohort 5) and 3c058c37e6 (base of this PR).

## What Changed

- `OpenCodeServerDriver` records the text of each completed reasoning
part in the run. A text part that still matches the start of a completed
reasoning part does not stream as progress. A completed text part that
repeats a reasoning part exactly is not a response candidate, and the
driver emits the `opencode_reasoning_echo_discarded` diagnostic. With no
answer left, the existing empty-response correction asks the model for a
final answer with reasoning disabled.
- After the completion call, the driver keeps the prose written between
the last work tool and that call only when the later message is a
closing reference: a short line with no link of its own that points back
to it ("Answered above.", "Commands above are ready"), or one under half
that prose's length. A later message that repeats the prose is saved
once. Otherwise the later message is the reply. The completion tool
description already names the post-call message as the final response.
This replaces the `restatesEarlierReply` heuristic from #21025, which is
removed.
- Tests: a new `reasoning-echo` mode in `empty-response.test.ts` uses
the recorded `tasks.website-task` text. Two new driver tests use the
recorded `recovery.explain-stall` and `status-cards.refresh-card` texts.
The #20999 "Answered above" test and the #21025 restatement test pass
without change. After review: the reasoning-echo mode streams the copy
with a trailing newline (it must stay held back), a new driver test
keeps "The result is 42." when the later message is "Answered above.",
and two more check that a STATUS note followed by a short complete
answer saves only the answer, and a repeated short answer is saved once.
- No eval keyword gate: a draft shared check that matched completion
tool names in replies was removed after review, because chat catalog
graders must not grade free-form replies by keyword. Replies stay saved
for human review; the driver fix is what keeps reasoning out of them.

## Verification

- `cd packages/paperclip-runner && npx vitest run src/drivers/opencode`:
17 files, 207 tests pass.
- Red/green: with the driver from `origin/master`, the `reasoning-echo`
test fails (the saved reply is the reasoning text), and both new
post-completion tests fail (two texts saved). With this change, they
pass.
- `npx vitest run --config tests/runner-e2e/vitest.config.ts
tests/runner-e2e/chat-catalog tests/runner-e2e/chat-reply
tests/runner-e2e/source-sdk`: 3903 tests pass.
- Check calibration: the new unit tests cover recorded valid replies,
reworded valid replies (`paperclip_runner` adapter type,
`PAPERCLIP_API_KEY`, `paperclip_finish_task`, plain
"summary"/"evidence"), and the four recorded broken replies. Over all
193 cohort-5 conversations the check flags exactly the four broken
cases.
- Over the 30 cohort-5 turns with prose before and after
`paperclip_finish`, the new rule changes 3: `recovery.explain-stall`
(duplicate removed), `status-cards.refresh-card` ("STATUS:" note
removed) and `assistant-connections.consent-read-only` (shorter
restatement kept, same link). The pointer-line turns
(`tasks.website-without-project`, `cli-operations.choose-cli-context`)
still keep both texts.
- `npx tsc -p packages/paperclip-runner/tsconfig.json --noEmit` passes.
- A paid chat-eval run on this change is linked in a comment below.

## Risks

- A turn that repeats its reasoning as text now takes one more model
request (the existing empty-response correction). If the corrected
response is empty again, the turn fails with `opencode_empty_response`,
as before.
- A real answer that is exactly equal to a reasoning part is dropped and
requested again. The correction recovers it.
- When a long post-completion message drops something the earlier prose
said, only the later message is saved. The completion tool contract
names that message as the final response, and the earlier prose remains
in the run transcript.
- Saved run state: no persisted run, session, checkpoint or event shape
changes. The reasoning texts are in-memory driver state per run. A run
saved by the previous release resumes without change; only a newly
settled final response differs.
- The new catalog check can fail a case that passed before. In cohort 5
it fails only the four reasoning replies.

> 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, 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-12 03:06:40 -06:00
DottaandClaude Fable 5.2 fd9985b571 fix(native): say what a Runner command waited behind when it times out (#21056)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs on remote sandboxes talk to a Runner through durable PRP
commands. The controller waits for each command's result
> - After a server replacement, the new controller reattached to live
Runners. For runs it reached later than about 115 s after detach,
`session.snapshot` got no answer within 30 s
> - The failure said only `provider_transport_failed: PRP command
session.snapshot timed out`. The run log could not tell whether the
Runner was disconnected, whether the command waited behind older
commands, or when the Runner last sent an event
> - This pull request adds that evidence to the timeout message. It
names types, counts and durations only, never payloads
> - The benefit is that the next upgrade's failures explain the stall,
so the runner-side cause can be fixed

## Linked Issues or Issue Description

Refs #21053, #21052, #21039

**What happened?**
At the 07:10Z staging swap, 9 remote native runs that the new controller
reached 114–152 s after detach failed. Each recovery read ended with
`PRP command session.snapshot timed out`. The transport had
authenticated, and in some runs the same Runner process was verified.
The Runner journal recorded no diagnostic other than "WebSocket peer
closed the connection". Runs reached within 39–116 s succeeded.

**What did you expect to happen?**
A command timeout should say what the command waited behind, so the
cause can be found from the run log.

**Steps to reproduce**
1. Start a remote native run and replace the server while a provider
turn is active.
2. Let the new controller reach the Runner more than about two minutes
after detach.
3. Read the run log. The failure says only "timed out".

## What Changed

- `packages/paperclip-runner/src/live/runnerd-codex-transport.ts`:
`waitForRunnerCommand` accepts an optional `describeStall`. When a
command times out, the message ends with a bounded description, for
example `(runner connected; pending ahead 1 [semantic_tool.result]
oldest 188s; last runner event seq 152 181s ago)`. The description
covers whether a Runner is connected or how long it has been
disconnected, the pending commands ahead of this one (types and count,
up to 6 types, and the age of the oldest), and the last committed Runner
event's sequence and age. A failing or empty description keeps the plain
message.
- The transport passes this description for every command it waits on.
The last event's age comes from the PRP payload's `emittedAt`.
- `server/src/services/native-runtime/native-session-executor.ts`: the
startup `session.open` timeout classifier accepts the optional
description, so a startup timeout stays `provider_session_open_timeout`
and keeps its bounded same-run retry. A regression case covers each
startup code with the suffix.
- Tests: the exact message for the staging shape, the plain message when
there is nothing to describe or the description throws, and a
disconnected Runner with no events.

## Verification

- `cd packages/paperclip-runner && npx vitest run
src/live/runnerd-codex-transport.test.ts -t "a Runner command that gets
no answer"` passes 3 of 3. These tests fail on `origin/master`, which
has no `describeRunnerCommandStall` export and no description in the
message.
- The full `runnerd-codex-transport.test.ts` file passes 307 of 308. The
one failure (`stops a lost runnerd's provider and escaped tool job from
its lineage journal`) is a process-timing test that passes alone and
does not touch this code.
- `npx tsc --noEmit -p packages/paperclip-runner/tsconfig.json` passes.
- Message matchers: the only matcher anchored on the end of the timeout
text was the startup `session.open` classifier. It now accepts the
suffix. `cd server && npx vitest run
src/services/native-runtime/native-session-executor.test.ts -t
"classifies a startup session.open timeout"` passes. Without the
classifier change it fails, because the suffix turns the timeout into
`native_session_interrupted`.

## Risks

- Low. Only the text of a timeout message changes, by appending text. No
control flow, persisted state or Runner behavior changes.
- The description holds command types, counts and durations only. It
holds no payloads, identifiers or provider text.
- No wait is added before a 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 (Anthropic) via Claude Code, with tool use (shell, file edits,
test runs, read-only staging API reads). Model ID as attributed in the
commit trailer: Claude Fable 5.2.

## 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
- [ ] 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-12 03:00:32 -06:00
DottaandClaude Fable 5.2 8e956da1cf fix(native): keep a live reattached Runner when its restart recovery read times out (#21053)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs on remote sandboxes (Daytona) use a detached Runner. A
server replacement detaches the controller and leaves the Runner alive.
The new server reattaches to it and continues the same turn
> - On staging, about half of the in-flight runs at each upgrade lost
their turn. The new controller reattached to the live Runner (same
process verified), its recovery read timed out (`PRP command
session.snapshot timed out`), and the failure cleanup killed the Runner
> - The next same-run reattachment then found `process_not_running`.
#21039 continues these runs in a fresh session, but the in-flight turn
is lost and its outcome is unknown
> - The cause of the kill is in the Codex driver's `recoverSession`: its
cleanup closed the transport without retention. For a reattached Runner
that means SIGKILL after the close deadline. #21023 already keeps the
Runner for a failed open session, but not for a failed recovery
> - This pull request keeps the live Runner when a recovery fails or is
aborted on its way to it. Identity refusals and protocol integrity
faults still stop it
> - The benefit is that a slow reattachment no longer destroys a turn
that was still running

## Linked Issues or Issue Description

Refs #21023, #20923, #21039, #21052

**What happened?**
Staging replaced its server at about 07:10Z with 17 remote native runs
in flight. For each, the old server logged "Server shutdown suspended
native controller ownership without cancelling provider work". The new
server reattached. Example run: the Runner process (same pid and start
time) was verified at 07:12:37 and the transport authenticated at
07:12:32. At 07:13:28 the recovery failed with
`native_session_recovery_failed: ... PRP command session.snapshot timed
out`, and a same-run resume was saved. At 07:14:12 the same-run
reattachment found `process_verification` / `process_not_running`, with
the Runner journal still `ready` and no exit record from the Runner
itself. The run failed with `runner_remote_process_identity_unavailable`
/ `native_continuation_requires_reconciliation`. 90 of 93 such failures
in two hours of upgrades had this sequence.

**What did you expect to happen?**
A recovery that times out against a live Runner is a transient failure.
The same-run reattachment should find the Runner alive and adopt it
again. Killing the Runner is only for a deliberate stop, an identity
refusal, or an integrity fault.

**Root cause**

`packages/paperclip-runner/src/drivers/codex/codex-app-server-driver-impl.ts`
`recoverSession`: the `catch` block and the abort handler call
`bootstrapCancellation(...).close()`, which calls `transport.close()`
without `retainUnsettledRemoteRunner`. In `runnerd-codex-transport.ts`
`#closeOnce`, an unresponsive adopted Runner that is not retained gets
`SIGKILL` after the close deadline. The Codex driver serves remote
Codex, OpenCode and ACPX runs, which matches the failures seen on
staging for all three driver kinds.

## What Changed

- `codex-app-server-driver-impl.ts`: `recoverSession` classifies a
thrown recovery failure. A transient one (`provider_transport_failed`, a
`PRP command ... timed out`, or the transport closing while waiting) now
detaches this controller with the transport's restart detach
(`detachControllerForRestart`). That path queues no `turn.stop` and no
`runner.suspend` and sends no signal, the same as a server restart. The
Runner and its in-flight turn stay for the same run's next reattachment.
An abort (a Stop or the recovery deadline), any message that names a
mismatch, a different session, identity, authority, integrity or
incompatibility, any unclassified failure, any returned identity
refusal, and a protocol integrity fault still close and stop the Runner.
`openSession` is unchanged.
- `codex-app-server-driver.recovery.test.ts`: the staging message
(`codex_history_read_failed: ... provider_transport_failed: PRP command
session.snapshot timed out`), and a plain snapshot timeout now detach
and never close. An abort of a hung read still closes and stops the
Runner. A thrown different-session refusal,
`native_adopted_provider_identity_mismatch`, an identity refusal that
also mentions a timeout, and an unclassified failure still close without
retention. The existing integrity-fault cases check that the close does
not retain the Runner.
- `doc/execution-semantics.md`: describes the rule.

## Verification

- Red on `origin/master` with only the new tests: `cd
packages/paperclip-runner && npx vitest run
src/drivers/codex/codex-app-server-driver.recovery.test.ts` fails 2 of
69 (the two detach cases; master calls `close()` and stops the Runner).
- Green with the fix: 69 of 69.
- `npx vitest run src/drivers/codex src/native-session-runtime.test.ts
src/backends src/live/runnerd-codex-transport-remote-retention.test.ts
--maxWorkers=4` passed except two timing tests (Codex command
resolution, background cleanup containment) that pass alone and do not
touch this code.
- `npx tsc --noEmit -p packages/paperclip-runner/tsconfig.json` passes.
- `detachControllerForRestart` is the existing restart path. It stops
the pump, disconnects the Runner, and drains the controller journal. It
queues no command and never signals the process.
- Not run: a live Daytona restart. The next post-upgrade audit should
show fewer `runner_remote_process_identity_unavailable` failures after a
`session.snapshot timed out` resume. Instead, the run log should show
"controller authority detached for restart; durable runner left alive"
and then a successful same-run reattachment.

## Risks

- Medium. A Runner that the recovery could not reach now stays alive,
with its turn, for the same-run reattachment. This is the state a server
restart already leaves. The same-run reattachment adopts it, or a Stop
cleanup or saved-work recovery stops it.
- If the Runner stays unresponsive across every same-run attempt, the
run no longer gets the `process_not_running` proof that #21039 uses for
its automatic fresh-session continuation. The run then keeps its visible
Retry. The staging data shows that reattachments reaching the Runner
sooner succeed, so a later attempt is expected to succeed. This is not
proven against a live sandbox.
- An abort of the recovery (a Stop or the recovery deadline) is
unchanged: it closes and stops the Runner.
`plugin-runner-release-resume.integration.test.ts` (14 of 14) covers a
Stop during an adopted resume.
- No turn is replayed and no second Runner is launched. Adopting an
existing Runner never launches a new one.
- Previous-release story: no persisted state changes. The decision is
made in the process that runs the recovery, so a deploy during a run
changes nothing.
- No wait is added before a normal turn start. Only the recovery failure
path changes.

> 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) via Claude Code, with tool use (shell, file edits,
test runs, read-only staging API reads). Model ID as attributed in the
commit trailer: Claude Fable 5.2.

## 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-12 02:59:46 -06:00
DottaandClaude Opus 5.5 b2cb8f8f56 test(native): cover a turn.start timeout that loses the remote Runner, end to end (#21041)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs on a remote sandbox (Daytona) use a detached
`paperclip-runnerd`. After a recoverable failure, the same run
reattaches to that Runner
> - On staging, 31 tasks that a person retried are blocked. Their last
run failed with `runner_remote_process_identity_unavailable` /
`native_continuation_requires_reconciliation`
> - In each run, the `turn.start` PRP command timed out. The close then
killed the Runner. The same-run reattachment found the Runner gone
(`process_not_running`, `providerTurnIssued: false`), and the task
waited for a Retry
> - #21023 and #21039 fix this sequence. Neither one is deployed yet. No
test reproduces this exact sequence, so a later change could break it
without a signal
> - This pull request adds two tests. One reproduces the turn start
timeout with a real `paperclip-runnerd`. The other reproduces the
staging failure record of a person's Retry
> - The benefit is that a regression of this failure class fails CI
before it reaches staging

## Linked Issues or Issue Description

Refs #21023, #21039, #20923, #20853

**What happened?**
A person retried a blocked task that runs Codex on Daytona. The Retry
started a fresh provider session and submitted a turn. The `turn.start`
PRP command timed out (`provider_transport_failed: PRP command
turn.start timed out`). The run closed. The run log shows `runner did
not prove durable suspension before checkpoint`, `runner shutdown
failed: Error: Durable recovery runner timed out.`, and then
`runner_remote_process_exited lifecycle=ready`. The log does not show
`remote runner left alive unsettled for same-run reattachment`. The
same-run retry tried to reattach 50 s later. It recorded
`native.remote_runner_recovery_failed` with `stage:
process_verification`, `reason: process_not_running`, `runnerSettled:
false`, `providerTurnIssued: false`, and an empty Runner stderr. The run
failed with `runner_remote_process_identity_unavailable`, and the task
waited for a person.

**Expected behavior**
A turn start timeout does not kill the Runner, so the same-run
reattachment adopts it. If the Runner is gone anyway, the run continues
once by itself in a fresh provider session and does not replay the turn.

**Steps to reproduce**
1. Start a remote native Codex run on a release before #21023.
2. Make the provider not answer `turn/start`, so the `turn.start` PRP
command times out.
3. The runtime cancels the session as failure cleanup. The close treats
that as a Stop and kills `paperclip-runnerd`.
4. The same-run reattachment finds `process_not_running`. Before #21039,
the completion hook refuses an automatic Retry with
`provider_turn_unsettled`.

**Paperclip version or commit**
Staging runs 8cefbe9977. That build has neither #21023 nor #21039. This
pull request is based on master after both.

**Deployment mode**
Hosted staging cloud workspace. Native runtime with a remote Daytona
sandbox.

**Agent adapter(s) involved**
Codex (native `codex_app_server` driver through `paperclip-runnerd`).

## What Changed

-
`packages/paperclip-runner/src/live/runnerd-turn-start-timeout-retention.test.ts`
(new): runs `executeNativeSession` over a real `paperclip-runnerd` with
a launcher-owned (remote-shaped) process. The fake Codex never answers
`turn/start`, so the `turn.start` PRP command times out. The test checks
that the run fails with that exact error, that the Runner process is
still alive after the close, and that its journal is `ready` for the
same-run reattachment.
- `server/src/services/runner-lost-mid-turn-continuation.test.ts`: a new
case with the staging failure record. A person's Retry (`task_retry`, a
user intent with a non-hook key, `forceFreshSession`) has only
`turn.submitted`. Its refusal has `runnerSettled: false` and
`providerTurnIssued: false`. The completion hook requests one automatic
Retry. The test checks one unknown-outcome event with `replayed: false`,
an idempotent second call, saved-work recovery first, and one queued
fresh-session successor. The fixture gets small optional inputs for the
wake, the events, and the refusal payload. Other cases are unchanged.
- No production code changes.

## Verification

- `cd packages/paperclip-runner && npx vitest run
src/live/runnerd-turn-start-timeout-retention.test.ts
src/live/runnerd-codex-transport-remote-retention.test.ts`: 6 of 6 pass.
The new test passed 4 runs in a row.
- Red without #21023: with `native-session-runtime.ts`,
`harness-driver-backend.ts`, and `native-session-backend.ts` from before
#21023, the new runner test fails with `expected 'gone' to be 'alive'`.
That is the staging failure.
- `cd server && npx vitest run
src/services/runner-lost-mid-turn-continuation.test.ts`: 9 of 9 pass.
- Red without #21039: with `restart-interrupted-turn.ts` from before
#21039, the new server case fails with `{ requested: false, reason:
"provider_turn_unsettled" }`.
- `npx tsc --noEmit` passes for `server` and
`packages/paperclip-runner`.
- `pnpm verify` exits 2 because the new runner test file is outside the
resolved test graphs. CI supplies that coverage.
- Staging evidence: read-only GET of the failed run's events and log.
- Not run: a live Daytona run. #21023 and #21039 are not deployed yet.

## Risks

- Low. Tests only. No production code changes.
- The runner test starts a real `paperclip-runnerd` and the fake Codex.
It kills both processes and removes its temporary directory in
`finally`.

> 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`), 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-12 01:08:18 -06:00
DottaandClaude Opus 5.5 148d2b629c fix(heartbeat): renew legacy controller leases on their own connection, timed from the statement (#21042)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A legacy run keeps a controller lease on its `heartbeat_runs` row.
The server renews every lease in the process with one batched statement
every 10 seconds, and stops a run when its local lease deadline passes.
> - The batched renewal ran on the 2-connection liveness pool. Native
runner lease renewals use the same pool. Each native renewal holds a
connection for a row lock, about seven notifications, and an update.
> - With 18 native runner runs renewing every 20 seconds on a slow
database, the legacy batch waited 30 to 50 seconds for a connection. The
local deadline also counted that wait, so it fired 20 to 40 seconds
before the database lease could expire.
> - Runs then stopped with `controller_lease_lost` while no other
controller owned them, and their issues looped through retries.
> - This pull request gives legacy renewal its own connection and
measures the deadline from the time the statement starts on that
connection.
> - The benefit is that a slow server no longer stops runs that still
own their lease. A real takeover still stops the run.

## Linked Issues or Issue Description

**What happened?**

On staging, after the 05:37Z deploy, 63 of about 175 new runs failed
with `controller_lease_lost` ("Legacy controller lease lost") at about 3
per minute. All were legacy runs in preparation
(`executionRecovery.kind: bootstrap`, `providerWorkStarted: false`). One
example had `controllerLeaseLoss: {cause: "renewal_deadline",
renewalCount: 7, sinceRenewalMs: 42537, renewalPendingMs: 38798,
deadlineLateMs: 134}`. Several runs that failed at the same moment had
identical timing values, so they were members of the same renewal batch.
The previous successful renewal took 17 to 44 seconds, so the deadline
that it set was only 16 to 43 seconds away. At that time 27 legacy runs
were preparing and 18 native runner runs were renewing on the same
liveness pool. The issues looped: `issue_assigned`, lease lost,
`issue_continuation_needed`, lease lost, `transient_failure_retry`,
lease lost.

**Expected behavior**

A slow server or database must not cause a lease loss while the
controller still owns the lease. Only a real expiry or a takeover by
another controller must stop the run.

**Steps to reproduce**

1. Start legacy runs and native runner runs on one server.
2. Make the database slow, so that each native lease renewal holds a
liveness connection for several seconds.
3. The legacy renewal batch waits for a liveness connection, and its
next local deadline is shortened by that wait.
4. The next renewal also waits, and the local deadline fires first. The
run fails with `controller_lease_lost` (`renewal_deadline`).

**Paperclip version or commit**

Staging at 8cefbe9977. The fix is based on master at aa94939988.

**Deployment mode**

Hosted cloud (staging).

**Agent adapter(s) involved**

Legacy controller runs (Codex and Claude local adapters). Native runner
runs share the liveness pool.

## What Changed

- `packages/db`: add `controllerLeaseDb(db)`, a private 1-connection
pool (`paperclip-legacy-controller-lease`) for legacy controller
renewal. `closeLivenessDb` also ends it, so shutdown does not change.
- `legacy-controller-lease.ts`: the batched renewal runs on
`controllerLeaseDb` instead of the liveness pool that native lease
renewals use.
- `legacy-controller-lease.ts`: each batch records the time when its
transaction starts on the connection. The watcher measures the next
deadline from that time, not from the time the request was queued. The
database sets the new expiry at UPDATE time, which is later, so the
local deadline never passes the database expiry.
- The renewal statement, its boot-id and expiry conditions, and
revocation do not change. A takeover or an expired lease still rejects
the renewal and stops the run.

## Verification

- Red/green regression tests in
`server/src/services/legacy-controller-lease.test.ts`:
- `renews without queueing behind native lease transactions that hold
every liveness connection` (embedded Postgres). Before the fix, the
renewal waited 9.8 s behind four 5-second liveness transactions. After
the fix, it completes in less than 2 s.
- `keeps a controller owned when every renewal waits thirty-five seconds
for a connection before its statement runs`. Before the fix, the lease
was lost with `{sinceRenewalMs: 25000, renewalPendingMs: 20000,
renewalCount: 1}`, the same pattern as on staging. After the fix, the
controller stays owned for four lease periods.
- Updated the private-pool tests to check the new pool and its
application name.
- `npx vitest run src/services/legacy-controller-lease.test.ts`: 40
passed.
- `pnpm verify` (affected tests), plus `typecheck` for `packages/db` and
`server`.

## Risks

- Each server process opens one more database connection, only when it
has a legacy run.
- The local deadline can now be later than before, by the time that the
batch waited for a connection. It stays at or before the database
expiry, because the database sets the expiry after the recorded start
time. Readers that act on an expired lease are not affected.
- Low risk otherwise. There is no schema change.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, 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
- [ ] 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

Refs #20924, #20936

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 00:52:06 -06:00
DottaandClaude Opus 5.5 aa94939988 fix(native): continue a run whose remote Runner died mid-turn after a restart, without replaying the turn (#21039)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs on a remote sandbox (Daytona) use a detached
`paperclip-runnerd`. After a server restart, recovery reattaches to that
Runner on the run's own sandbox lease
> - On staging, a deploy detached the controller and left the Runner
alive. The restart's reattachment then found the Runner process gone
with a provider turn in flight. The run failed with
`runner_remote_process_identity_unavailable` and the task waited for a
person
> - The run log said `startup reuse: environment (fresh) created`, so
the sandbox looked re-created. It was not. The reattachment resumed the
exact retained lease. The receipt reported the lease's original creation
> - `requestInterruptedNativeTurnRetry` refused every unsettled turn
(`provider_turn_unsettled`), and refused every system wake unless the
journal proved that no turn was issued. So this run got no automatic
continuation
> - This pull request continues such a run once in a fresh provider
session when the refused reattachment proved the Runner process gone. It
records that the turn's outcome is unknown and never replays the turn.
It also makes the environment receipt tell the truth
> - The benefit is that a restart that kills a Runner in the middle of a
turn no longer needs a Retry click

## Linked Issues or Issue Description

Refs #20853, #20923, #21023, #20278, #20627, #20828

**What happened?**
A staging deploy landed in the middle of a remote native Codex run on
Daytona. Shutdown detached the controller and logged "durable runner
left alive". The new server reattached through the run's original lease
(same lease row, same provider sandbox id, same directory). Its process
check then found the Runner gone: `process_verification` /
`process_not_running`, `runnerLifecycle: ready`, `runnerSettled: true`,
last diagnostic "WebSocket peer closed the connection", empty stderr.
The provider checkpoint still had an active turn. The run failed with
`runner_remote_process_identity_unavailable` /
`native_continuation_requires_reconciliation`, and the task went to
`blocked`.

**Root cause**
- `server/src/services/heartbeat/restart-interrupted-turn.ts`
(`requestInterruptedNativeTurnRetry`): an unsettled last turn always
returned `provider_turn_unsettled`, also when the refusal proved the
Runner process gone. A system wake (here `transient_failure_retry`)
continued only with `providerTurnIssued: false`.
- The sandbox was not re-created. The environment and lease listings
show one lease for the run, never replaced, and the final workspace
snapshot used the same provider sandbox. `environmentStartupReuse` read
`sandboxLeaseAcquisition: created` from the lease's first acquisition,
so the reattachment's receipt said `fresh / created`. The 46 s acquire
was the provider resume of that exact lease. The evidence available to
GET requests cannot say what stopped the Runner process (the resume
result is not kept). This pull request does not change that path.
- #21023 named this run as not covered. #20853 resumes a Runner only at
a settled, idle `ready` cut. Here the provider checkpoint has an active
turn, so #20853 stays fail-closed, as it should.

**Expected behavior**
The run continues once by itself in a fresh provider session over the
saved work. The turn whose outcome is unknown is recorded and is not
replayed.

## What Changed

- `restart-interrupted-turn.ts`: read the last
`native.remote_runner_recovery_failed` refusal once. When it proves the
Runner process gone, an unsettled last turn no longer blocks the
automatic Retry. Before it requests the Retry, the server records
`native.interrupted_turn_outcome_unknown` once (turn id, last turn
event, `replayed: false`, closed `runnerExit` fields). The Retry is the
existing one: saved-work recovery and stop proof first, then a fresh
provider session (`forceFreshSession: true`), once per chain.
- The same file: a system wake whose run is an automatic retry of owed
work (`retryOfRunId` set) also continues under the run's responsible
user once the refusal proved the Runner gone. Other system wakes (for
example a monitor) keep their policy.
- `packages/adapter-utils/src/startup-reuse.ts` and
`native-session-executor.ts`: a `reattach_remote_runner` recovery
reports its `environment` receipt as `reused / verified_reuse`. That
path only resumes the run's exact retained lease and never acquires one.
- Docs: `doc/execution-semantics.md` and `doc/run-log-events.md`.

Previous-release story: no persisted shape changes. The rule reads the
closed `remoteRunnerRecovery` fields that the previous release already
writes; the new test uses the staging refusal payload as written. A run
that a previous release already failed this way continues on the next
completion or restart hook call only; runs already settled stay as they
are and keep their Retry. The new event is additive.

## Verification

- Red on `origin/master` (51d77648bc), with only the new tests: `cd
server && npx vitest run
src/services/runner-lost-mid-turn-continuation.test.ts` fails 4 of 6.
The three continuation cases return `{ requested: false, reason:
"provider_turn_unsettled" }`, and the monitor case returns that reason
instead of `not_user_request`.
- Green with the fix: 6 of 6. The cases cover a user's assignment
through the restart claim, the staging shape (system
`transient_failure_retry` with `retryOfRunId`), and a same-run retry.
Each one checks one recorded unknown-outcome event, an idempotent second
call, saved-work recovery first, one queued successor with
`forceFreshSession: true`, and one Retry intent. Negative cases: a
refusal that did not prove the process gone, no refusal, and a monitor
wake. Each keeps the visible Retry and records nothing.
- `cd packages/adapter-utils && npx vitest run
src/startup-reuse.test.ts`: the new receipt case fails before (`fresh /
created`) and passes after.
- Focused suites pass: `workspace-recovery-retry-copyback.test.ts`,
`governed-wait-runner-loss.test.ts`,
`heartbeat-native-remote-failure-retention.test.ts`,
`native-runner-restart-recovery.integration.test.ts` (302 passed, 14
skipped), and `native-session-executor.test.ts -t "startup
reuse|restart|reattach"` (149 passed).
- `pnpm --filter @paperclipai/server typecheck` and `pnpm --filter
@paperclipai/adapter-utils typecheck` pass.
- `pnpm verify` exits 2 because `doc/execution-semantics.md` is a global
CI input. CI supplies that coverage.
- Not run: a live Daytona restart. The change is in the server's
recovery decision; the tests use the fixture's sandbox confinement.

## Risks

- Medium. A run whose Runner died in the middle of a turn now continues
by itself. The turn's outcome is unknown, so tool work from that turn
can be partly done. The successor is a fresh session over the saved
workspace; it does not replay the turn. Saved-work recovery stops and
keeps the sandbox before the successor starts, so the old provider
cannot still run.
- Without positive proof that the Runner process is gone, an in-flight
turn keeps the visible Retry, as before.
- An automatic retry of owed work now continues after a lost Runner.
Monitor and other system wakes without `retryOfRunId` do not. The bound
is still one automatic Retry per chain.
- The environment receipt changes only for the reattachment path. No
execution decision reads it.

## 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-12 00:13:25 -06:00
DottaandClaude Opus 5.5 15b0746340 fix(connections): tell agents a request card can only be resolved by the person it is addressed to (#21027)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - When an agent needs a person to connect a service, it creates a
connection request card. The card is addressed to one person, and only
that person can complete or decline it. The server enforces this
> - Agents learn how connection requests work from the shared connection
guidance (`CONNECTION_INTENT_AGENT_GUIDANCE`). The `get_task_context`
tool returns it as `connectionGuidance`
> - That guidance did not say who can resolve a card. In chat-eval
cohort 4 (`connection-setup.wrong-person-resolves`), another member said
"I will finish Alex's connection request for them". The CEO agent read
only the task context and replied that "completing it there is the right
move"
> - The product still refused the other member, but the agent encouraged
an action that cannot succeed
> - This pull request adds one line to the shared guidance: a request
card is addressed to one person, only that person can complete or
decline it, and the agent says whose action it waits on
> - The benefit is that agents give the correct next step instead of
sending people to a control they cannot use

## Linked Issues or Issue Description

**What happened?**
A company member told the CEO agent "I will finish Alex's connection
request for them." The request card was addressed to Alex. The agent
replied "Alex's Notion connection request is on COM-1, so completing it
there is the right move. I'll leave the connection approval to you." The
server correctly refused the other member's setup, completion and
decline requests (403).

**Expected behavior**
The agent tells the person that the request waits on Alex, and that only
Alex can complete or decline it.

**Steps to reproduce**
1. Have an agent create a Notion connection request card addressed to
Alex on a task.
2. As another company member, tell the agent in chat that you will
finish Alex's connection request.
3. Read the agent's reply.

**Paperclip version or commit**
master at f1aef0cb58.

## What Changed

- `packages/shared/src/connection-intent-guidance.ts`: one new guidance
line. A connection or access request card is addressed to one person;
only that person can complete or decline it; another member and the
agent cannot finish it for them; when someone else offers to, the agent
says whose action it waits on and points to the request.
- `packages/shared/src/connection-intent-guidance.test.ts`: a test that
the guidance states the addressee rule.

## Verification

- `cd packages/shared && npx vitest run
src/connection-intent-guidance.test.ts`: 4 tests pass. Red/green: the
new test fails without the guidance line.
- `npx vitest run --config tests/runner-e2e/vitest.config.ts
tests/runner-e2e/connection-guidance.test.ts`: 17 tests pass.
`server/src/adapters/http/execute.test.ts`: 3 tests pass.
- `packages/adapter-utils/src/server-utils.test.ts` has one local
failure ("does not crash when the child closes stdin before a delayed
write", 5 s timeout). It fails the same way on `origin/master` on this
machine, so it is not caused by this change. CI runs it.
- A paid chat-eval run of `connection-setup.wrong-person-resolves` with
this change is linked in a comment below.

## Risks

- Low risk. The guidance text grows by one line in every prompt that
includes connection guidance. The rule restates what the server already
enforces.
- Model behavior is not guaranteed by guidance. The server boundary
stays the enforcement.

## 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 23:57:01 -06:00
DottaandClaude Opus 5.5 f7d41ca5bf fix(opencode): save an answer once when the model restates it after the completion call (#21025)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents that use OpenCode models reply in chat through the Runner's
OpenCode driver. The driver settles one final response, and Paperclip
saves it as the reply comment
> - #20999 made the driver keep the prose written before the
`paperclip_finish` call together with the message written after it. That
stopped the loss of an answer when the later message only said "Answered
above"
> - In chat-eval cohort 4 (OpenCode with DeepSeek v4.1 flash), the model
often wrote its whole answer, called `paperclip_finish`, and then wrote
the same answer again in other words. The driver saved both. 13 cases
had one saved reply that held the answer twice
> - This pull request saves only the later message when it restates the
earlier prose. A short pointer line still keeps the earlier answer
> - The benefit is that people, deliveries and the next turn read the
answer once, and #20999's protection stays in place

## Linked Issues or Issue Description

Refs #20999

**What happened?**
An OpenCode chat turn wrote its answer, called `paperclip_finish`, and
then wrote the same answer again with small wording changes. The saved
reply comment held both copies, one after the other. Example
(budgets-costs.set-budget): "I can't set this from chat — changing an
agent's monthly budget is a board-only action ..." followed by "I can't
change agent budgets from chat — that's board-only ...", both with the
same two budget links.

**Expected behavior**
The saved reply holds the answer once. When the later message only
refers back to the earlier answer, the saved reply still keeps the
earlier answer.

**Steps to reproduce**
1. Use an OpenCode agent with
`openrouter/deepseek/deepseek-v4.1-flash-1017`.
2. Ask a question that the model answers before `paperclip_finish` and
again after it.
3. Read the saved reply with `GET /api/issues/{id}/comments`. It holds
the answer twice.

**Paperclip version or commit**
master at f1aef0cb58.

## What Changed

- New `restatesEarlierReply(earlier, later)` in
`packages/paperclip-runner/src/drivers/opencode/restated-reply.ts`. The
later message restates the earlier prose only when all of these are
true: it keeps every link target and fenced code block of the earlier
prose, it is at least half as long, and it repeats at least half of the
earlier prose's distinct words. Identical text is always a restatement.
- `OpenCodeServerDriver` uses it in the path that #20999 added. When the
post-completion message restates the pre-completion prose, the driver
saves the post-completion message alone. Otherwise it joins both, as
before.
- The fake OpenCode server gets an
`answer-before-finish-then-restatement` turn that uses the recorded
cohort-4 texts.
- Tests: one driver test for the recorded shape, and unit tests for the
helper (identical, reworded, pointer line, dropped link, dropped code
block, different content).

## Verification

- `cd packages/paperclip-runner && npx vitest run
src/drivers/opencode/restated-reply.test.ts
src/drivers/opencode/opencode-server-driver.test.ts`: 87 tests pass.
This includes the #20999 tests ("Answered above" keeps the earlier
answer; long commentary before a short post-tool answer keeps both).
- Red/green: with the driver file from `origin/master`, the new driver
test fails (the saved text holds both copies). With the change, it
passes.
- `npx tsc -p packages/paperclip-runner/tsconfig.json --noEmit` passes.
- I ran the helper over all 18 recorded cohort-4 turns that had prose
both before and after `paperclip_finish`. It classified the 13 reported
duplicate replies (and 4 turns whose earlier line was a short status
that the later answer covers) as restatements. It kept both texts for
`access.cli-authorization`, where the later message is "Commands above
are ready to paste."
- A paid chat-eval run on a branch with this change is linked in a
comment below.

## Risks

- The check reads text, so it can be wrong at the margin. If it wrongly
calls an earlier answer a restatement, the saved reply loses that
earlier text. The link, code-block and length conditions keep the
references and the content of a substantial earlier answer; the earlier
prose remains in the run transcript.
- No saved run state, session state or event shape changes. A run saved
by the previous release resumes without change; only the text of a newly
settled final response differs.
- During a live run, the chat can still show the earlier prose as
progress above the saved answer, as it did before #20999.

## 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 23:54:27 -06:00
DottaandClaude Opus 5.5 51d77648bc fix(native): keep a remote Runner alive when a failed turn start or event stream closes the run (#21023)
## 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
> - #20923 stopped a failed close from force-stopping an unreachable
remote Runner. It asks for retention only when the session was not
cancelled
> - The runtime cancels the session itself when a turn start or the
event stream fails ("Native session event consumption failed."). That
cleanup sets the same flag as a user Stop
> - So after a failed turn start, the close still sent `SIGKILL` to the
Runner, and the same-run reattachment failed with
`runner_remote_process_identity_unavailable`. The task waited for Retry
> - This pull request separates failure cleanup from Stop. Failure
cleanup still revokes the turn's output, but the close keeps the Runner
for the same-run reattachment
> - The benefit is that these runs continue without Retry. Stop and
protocol integrity faults still stop the Runner

## Linked Issues or Issue Description

Refs #20923, #20853

**What happened?**
After the staging redeploy, five remote native runs failed with error
`runner_remote_process_identity_unavailable` and error code
`native_continuation_requires_reconciliation`. In four of them, the
first attempt started after the deploy. Its `turn/start` failed with
`runnerd did not report the provider turn identity`. The run log then
showed `runner did not prove durable suspension before checkpoint` and
`runner shutdown failed: Error: Durable recovery runner timed out.` The
diagnostic `remote runner left alive unsettled for same-run
reattachment` from #20923 did not appear. One second later the Runner
exited (`runner_remote_process_exited lifecycle=ready`). The same-run
reattachment 20–40 s later found `process_verification` /
`process_not_running` with `runnerSettled: false`.

**What did you expect to happen?**
The failed close keeps the Runner alive, as #20923 intended. The
same-run reattachment adopts it, and the run continues without Retry.

**Steps to reproduce**
1. Start a remote native Codex run.
2. Make `turn/start` fail (for example the Runner does not report the
provider turn identity in time), or let the event stream fail or time
out.
3. The runtime cancels the session as cleanup. `HarnessNativeSession`
treats that as a Stop, so `close` asks for `retainUnsettledRemoteRunner:
false`, and the transport kills the Runner.

**Root cause**
`packages/paperclip-runner/src/native-session-runtime.ts`
`attemptOptionalSessionCancellation` calls `session.cancel`.
`HarnessNativeSession.cancel`
(`packages/paperclip-runner/src/backends/harness-driver-backend.ts`)
sets `#explicitlyCancelled`, and `close` used
`retainUnsettledRemoteRunner: !this.#explicitlyCancelled`.

The fifth run is different. A server shutdown detached its controller
and left the Runner alive. Then the sandbox environment was acquired
fresh (45.9 s) before the reattachment, and the Runner was gone in the
middle of a turn. This pull request does not change that path. See
Risks.

## What Changed

- `NativeSession.cancel` accepts an optional `failureCleanup` field.
- `attemptOptionalSessionCancellation` passes `failureCleanup: true`,
except for a `NativeSessionProtocolIntegrityError`.
- `HarnessNativeSession` tracks a Stop separately from output
revocation. `cancel` with `failureCleanup` revokes the turn's output
only. `cancel` without it, and `revokeTurnPublication`, record a Stop.
`close` asks for retention when no Stop was recorded and no protocol
integrity fault occurred.
- Tests: three runtime cases with the real Codex backend (turn start
fails, event stream times out, Stop then failure) and a new backend
case.
- Docs: `doc/execution-semantics.md` describes the rule.

## Verification

- Red, on `master` with only the new tests: `cd
packages/paperclip-runner && npx vitest run
src/native-session-runtime.test.ts -t "remote Runner retention after a
failed run"` fails 2 of 3 (`expected false to be true`): the close asked
to kill the Runner after the turn start failed and after the event
stream timed out. The Stop case passed.
- Green with the fix: the same command passes 3 of 3.
`src/backends/harness-driver-backend.test.ts -t "retain an unsettled"`
passes 3 of 3, including the failure-cleanup case. That case also checks
that the turn's result stays unpublished.
- `npx vitest run src/native-session-runtime.test.ts src/backends
src/drivers/codex
src/live/runnerd-codex-transport-remote-retention.test.ts
--maxWorkers=4`: 31 files, 814 tests pass.
`codex-protocol-integrity.test.ts` confirms that an integrity fault
still stops the Runner.
- `npx tsc --noEmit -p packages/paperclip-runner/tsconfig.json` is
clean.
- `pnpm verify` exits 2 because `doc/execution-semantics.md` is a global
CI input. CI supplies that coverage.
- Not run: a live Daytona run. The change is in the controller library
that runs on the server, so it needs no new Runner image.

## Risks

- Low to medium. After a failed turn start or event stream, an
unreachable remote Runner now stays alive instead of being killed. This
is the state #20923 already accepted for other failed closes. The
session stays quarantined, and the same-run reattachment or a Stop
cleanup owns the Runner.
- Stop is unchanged. A server Stop calls `cancel` without
`failureCleanup`, so the close still stops the Runner, also when the
stopped run later fails. A governed wait uses `revokeTurnPublication`,
which also records a Stop.
- A protocol integrity fault needs an operator and is not resumed on the
same run, so it still stops the Runner.
- Previous-release story: this change adds and changes no persisted
state. A Runner that an older release already killed keeps its current
named refusal and Retry. A deploy during a run changes nothing, because
the decision is made in the closing process.
- Not covered: a Runner that dies with its sandbox while the controller
is detached for a restart, in the middle of a turn. Its journal is a
settled `ready` cut with an active turn. #20853 resumes only idle
`ready` cuts. This needs a separate fix.
- No wait is added before `turn.start`.

## 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 23:21:05 -06:00
DottaandClaude Opus 5.5 5a7378af56 fix(heartbeat): re-drive a queued run whose dispatch signal was lost after a restart (#20986)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The heartbeat queue starts queued runs. The run-dispatch worker
re-runs the queue only when a write sends a "queued run dispatch"
signal.
> - A follow-up run can queue while its predecessor still holds its
sandbox lease. The claim then declines the run without a log line, and
the run stays `queued`.
> - After a controller restart, nothing sent a new signal once that
lease was settled. The run stayed `queued` forever. The issue had no
Retry, and the reconciler counted the queued run as a valid next action.
> - This pull request sends a dispatch signal from the native
finalization writes that can clear a predecessor's cleanup gate, logs
why a claim was declined, and bounds the wait. The dispatch worker
re-drives an undispatched queued run after 60 s as a backstop, and the
ownership reconciler reports and re-drives one still queued after 5
minutes.
> - The benefit is that a follow-up starts within seconds of its
predecessor settling, a lost signal delays it by about a minute instead
of forever, and a run that still cannot start is visible.

## Linked Issues or Issue Description

Refs #20909, #20935, #20953, #20970

**What happened?**

Restart-chaos wave `w-20261111T223714-s9512` (master `6154b9485e` +
#20907 + #20909, Daytona warm, Codex, W2, CR1 with a 360 s hold,
`--ephemeral-server`, seed 9512), task t02. Run `0b95b1cd` crossed the
outage with an accepted result. The next controller finalized it at
22:47:59Z. Its continuation run `1ad79418` was queued at 22:48:00Z
(`retryOfRunId` = `0b95b1cd`, wake `d967700c`, source
`native_status_decision`). The run stayed `queued` with `claimed_at =
null` until the wave ended 19 minutes later. The agent stayed `running`
with no running run. The task had no Retry. The grade was
`R1:no_rest_within_budget` and U1 `no_owner_no_action ... queued 485s`.

**Root cause**

`claimQueuedRun` (`server/src/services/heartbeat/queue.ts`,
`lockIssueExecutionClaim`) declines a queued run while
`hasPendingRunCleanup` (`server/src/services/execution-blocker.ts`) is
true for its predecessor. After the restart, the predecessor's warm
`reuse_by_environment` lease was still `active` when the continuation
queued, so the first claim was declined. The decline is correct, but
nothing owned the retry. `registerRunDispatchWork`
(`server/src/services/heartbeat/dispatch-work.ts`) has `next: async ()
=> null`: the queue is only re-run by a later dispatch signal. At wave
end the lease was `retained` with `cleanupStatus: success`, which does
not block a claim. A dispatch pass in that state starts the run
(verified locally). So no pass ran after the gate cleared. The ownership
reconciler listed the run as `queued_run` with owner `run_dispatch`
(always "alive"), and I7 treats any queued run as a next action, so
nothing reported it.

This is not the #20935 throttle. The wave base predates #20935, and a
throttled intent keeps its wake intent `queued` with a `nextAllowedAt`
deadline. Here the run row existed and its claim was never retried. On
current master #20935 parks this specific `response_wake` self-wake, but
any continuation, comment wake or Retry that queues behind a settling
predecessor after a restart has the same gap. The red test uses a
`same_agent` continuation, which master still enqueues.

**Steps to reproduce**

`server/src/__tests__/heartbeat-native-restart-suspension.test.ts`,
"starts a continuation that queued behind its predecessor's sandbox
release after a graceful restart".

## What Changed

- Event-driven dispatch. A lease release already signals queued-run
dispatch (`environmentsSvc.releaseLease`, `writeEnvironmentCleanup`,
stop-and-retain settlement). I enumerated every `environment_leases`
status writer: each one that moves a lease out of a blocking state
already signals. The writers that do not signal only acquire a lease or
set `pendingCleanupInFlight`, so they add a block and do not clear one.
The native coordinator side had no signal, so:
- `notifyNativeFinalizationWork` (the shared notifier of coordinator
commits and terminal transitions) now also signals queued-run dispatch
for the company.
- The coordinator writes that clear `leaseOwner` or end the phase
outside that notifier now signal in the same transaction:
`releaseLapsedCoordinatorOwner`, native startup cleanup,
observed-workspace preservation, `record_finalization_error` in the
interaction bridge, the wake-queue terminal recovery, and the
reconciler's ambiguous-state terminal failure.
- Not changed: three non-transactional pre-dispatch closes in
`run-completion.ts` and `agent-execution-stop.ts`. Their callers queue
or settle the successor after the write, so the successor's own enqueue
starts the queue.
- `claimQueuedRun` now logs the reason when it declines a queued run at
the issue execution claim (`predecessor_cleanup_pending`,
`predecessor_still_executing`, `issue_execution_owned_by_another_run`,
case handoff, task chat cleanup), when a predecessor is still settling
in this process, and when a temporary budget hold keeps it queued.
- New `server/src/services/queued-run-dispatch-liveness.ts`:
`listUndispatchedQueuedRuns` reads queued runs in active companies whose
ready, non-paused agent has no running run. It reads every page with a
keyset cursor until the set is exhausted, so older held runs cannot hide
a newer stranded run. `createQueuedRunRedriveSchedule` re-drives each
such run 60 s after it was created, then every 2, 4, 8 minutes, capped
at 15 minutes. Run ids that leave the set are dropped.
- `server/src/services/heartbeat/dispatch-work.ts`: the queued-run
dispatch worker's `next` now plans those re-drives. It re-reads only the
companies the pass just dispatched, keeps the deadlines of every other
company, and runs no query when the pass dispatched nothing. Only an
instance-wide pass (startup or explicit wake) reads every company. It
adds each due company to the dispatch scopes and returns the next due
time.
- `packages/db`: new partial index `heartbeat_runs_queued_created_idx
(company_id, created_at, id) WHERE status = 'queued'` (migration
`0470_heartbeat_runs_queued_created_index.sql`, `CREATE INDEX
CONCURRENTLY IF NOT EXISTS`, registered as a top-level migration in
`client.ts`). It returns `null` (no deadline, no SQL until a signal)
when no run is undispatched or scheduling is suppressed. Every re-drive
goes through the normal `startNextQueuedRunForAgent` and
`claimQueuedRun` gates, so it never bypasses a hold, budget, pause or
cleanup gate.
- `server/src/services/heartbeat.ts`: wires `listUndispatchedQueuedRuns`
into the worker (two lines).
- `server/src/services/recovery/ownership-invariant-report.ts`: new
check. A queued run still undispatched 5 minutes after creation is an I7
violation (`kind: queued_run`, `reason: queued_run_not_dispatched`),
written to the activity log once. A younger one sets the report's next
due time.
-
`server/src/services/native-runtime/native-finalization-reconciler.ts`:
`applyOwnershipReportBackstops` sends a queued-run dispatch signal once,
for each company with a newly found violation. Repeat passes do not
signal again, so a held run does not wake the report every 30 s.
- `tests/restart-chaos/README.md`: records the wave and the fix in the
CR results table. The existing CR1 cell (#20909) and the enforced U1
check already cover this class in the harness.

## Verification

- RED on master (`23909077ad`):
- `heartbeat-native-restart-suspension.test.ts` "starts a continuation
that queued behind its predecessor's sandbox release after a graceful
restart": the continuation is `queued` behind the predecessor's `active`
warm lease. The run-dispatch worker is registered, the lease then
settles to `retained`/`success` with no dispatch signal, and the run
stays queued: `AssertionError: expected null not to be null` after 75 s.
- `ownership-reconciliation-report.test.ts` "reports and re-drives a
queued run that no dispatch started while its agent had nothing
running": `expected [] to deeply equal [ ObjectContaining{…} ]` (no
violation for a run queued 19 minutes).
- `ownership-reconciliation-report.test.ts` "signals queued-run dispatch
when finalization work settles or a finished coordinator's lapsed owner
is released": `expected [] to include 'settled'`.
- GREEN on this branch: all pass. The restart test has two cases. When
the lease is released through `releaseLease`, the continuation starts
within 5 s. When the lease is settled by a write with no signal, the
backstop starts it within 75 s. A new report test puts three older held
runs ahead of a stranded run with a page size of 2, finds the stranded
run, and checks that the backstop signals once.
- `queued-run-dispatch-liveness.test.ts`: schedule bound, doubling, cap,
and going quiet.
- Focused suites pass (22 files, 495 tests):
recovery-deadline-acceptance, live-recovery-work,
heartbeat-run-dispatch-lifecycle, heartbeat-run-dispatch-work,
deferred-execution-receipt-work, heartbeat-native-cleanup-admission, the
ownership kinds suites, and others.
`heartbeat-native-restart-suspension` and
`ownership-reconciliation-report` pass in full. `tsc --noEmit` for the
server passes.

## Risks

- A queued run that is held on purpose (for example a temporary budget
hold or a case handoff waiting for release) is now re-checked at most
every 15 minutes after the first minute. Each re-check is one indexed
query and the normal claim gates. Such a run is also reported once as an
I7 violation.
- An instance with an undispatched queued run keeps a dispatch deadline,
so it does not idle while that run waits. An instance with no such run
is unchanged.
- Schema: one additive partial index, built concurrently. No data
change. State saved by an older version is read as is. Nothing runs
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, 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 22:04:58 -06:00
DottaandClaude Opus 5.5 601babe322 fix(chat): let agents read saved trace export status and give chat real company page paths (#20998)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - People talk to their agents in chat, and agents answer from the
company's saved settings
> - A board member can save a company trace export (an OTLP collector or
Braintrust) in Company settings
> - The settings route is for board connection managers only, so an
agent cannot read it
> - In a chat eval, the board saved and tested a collector. The agent
checked only its own config and secrets, and then told the person that
nothing was configured
> - Chat guidance also names company pages as `/<prefix>/...`. In
another eval, the agent copied that placeholder into a link, and the
link opened nothing
> - This pull request gives agents a read-only status of the trace
export, and gives chat the real company prefix in page paths
> - The benefit is that agents answer from saved settings, and the links
they write open real pages

## Linked Issues or Issue Description

**What happened?**
1. A board member saved and tested a trace export destination. The chat
agent was asked to acknowledge the saved configuration. The agent said
that no collector endpoint or credentials existed. Agents get 403 from
`GET /api/companies/{companyId}/observability`, and no other route
reports the saved state.
2. An agent reply linked `/<prefix>/company/settings/instance`. The chat
rendered it as `/%3Cprefix%3E/...`, which is not a real page. The
placeholder came from the chat directive, which names pages such as
`/<prefix>/company/settings/instance/profile`.

**Expected behavior**
1. An agent can read whether the company's trace export is configured,
enabled and credentialed, and the result of its latest test. The agent
cannot read the destination or any credential.
2. The chat directive and API search results that an agent reads give
real company paths, such as `/COM/company/settings/instance/profile`.

**Steps to reproduce**
1. As a board member, save and test an agent observability destination
in Company settings.
2. In chat, ask the CEO agent to acknowledge that the test collector
configuration is saved.
3. See that the agent cannot read the saved settings and denies them.

**Paperclip version or commit**
master at 9dfdeaad86.

## What Changed

- Added `GET /api/companies/{companyId}/observability/status`. Agents of
the company and all members can read it. It returns `configured`,
`enabled`, `provider`, `credentialsConfigured`, `updatedAt` and the
latest test status. It never returns the destination or a credential.
- Added the shared `AgentObservabilityStatus` type and
`agentObservabilityStatus()` mapper, and registered the route in the
OpenAPI catalog so that `search_api` finds it.
- The chat task markdown now fills the conversation's own company prefix
(from its identifier, for example `COM-1` gives `COM`) into the chat
directives in place of `/<prefix>/`.
- `search_api` results now get the same substitution for the caller's
company. The shared catalog is not changed.
- Pinned the changed shared source in
`tests/runner-e2e/source-sdk-public-inputs.json`.

## Verification

- `cd server && npx vitest run src/__tests__/agent-observability.test.ts
src/__tests__/agent-observability-status-guidance.test.ts
src/services/heartbeat/task-markdown.test.ts
src/__tests__/openapi-routes.test.ts
src/services/native-runtime/paperclip-runner-tool-authority.test.ts`
- Red/green: with the product change removed, 10 of the new or updated
tests fail (the agent status read gets 403, search does not find the
route, and the chat markdown still contains `/<prefix>/`). With the
change, all pass.
- `npx tsc -p server/tsconfig.json --noEmit` passes.
- `node tests/runner-e2e/source-sdk-public-inputs.mjs <HEAD> --check`
reports no change.

## Risks

- Low risk. The new route is read-only and returns no destination and no
credential. The existing settings route keeps its board-only rule.
- The chat prompt text changes for chats with an identifier. The text is
constant for each company, so prompt caching is not affected inside a
conversation.

## 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 20:55:21 -06:00
DottaandClaude Opus 5.5 50b79d711a fix(runner): keep call_api writes carrying their body (#21002)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native agents use the `call_api` tool when no dedicated tool covers
an API operation
> - #20606 added an optional `saveResponse` flag to the shared
`call_api` schema for GET reads
> - After that change, OpenCode agents with DeepSeek v4.1 flash often
sent write calls without `body`
> - Body-less POST calls reached the routes and failed with 400; agents
then repeated the same call many times
> - This pull request moves the save option to its own read-only tool
and refuses body-less writes before they are sent
> - The benefit is that the write schema returns to its earlier shape,
and a missed `body` gets one clear, actionable error

## Linked Issues or Issue Description

Refs #20606

**What happened?**

In a full chat-eval cohort on master (OpenCode, DeepSeek v4.1 flash),
write calls through `call_api` arrived without `body`. The model's own
reasoning planned a body, but the emitted calls had only `operationId`,
`pathParams`, `query` and `saveResponse`. About 100 refused PATCH calls
listed exactly those arguments (the cohort before #20606 had none with
`saveResponse`). 72 POST calls reached the server with no body and got
400 (0 in the earlier cohort). One case sent the same body-less resolve
call 21 times.

**Expected behavior**

Write calls carry their request fields in `body`. When a body is
missing, the agent gets one clear error before anything is sent, and it
can correct the call.

**Steps to reproduce**

Run the OpenCode chat cases that resolve approvals, edit queued
messages, delegate with context, edit task properties or start preview
services on master after #20606. Count `call_api` writes without `body`
and POST 400 responses.

**Additional context**

The tool schema that OpenCode sends to the model is intact: `body` is
present. A short-context replay with the same schema did not reproduce
the omission (direct provider calls and local OpenCode runs: every write
carried `body`, with or without `saveResponse`). The omission appears in
long sessions, and it correlates with the added read-only flag. This
change removes that flag from the write schema and adds a safety net for
the remaining cases.

## What Changed

- Remove `saveResponse` from the advertised `call_api` schema and
description. The description now says that request fields go only in
`body`, and the `body` description says when it is required.
- Add a separate read-only `save_api_response` tool (`operationId`,
`pathParams`, `query`). It accepts only GET operation IDs, has no body
field, and uses the existing `saveResponse` export path with the same
access, sensitivity, budget and integrity rules.
- Refuse a write before dispatch when its body is missing: PATCH with a
required body (as before), and POST or PUT whose JSON request schema
names required fields. The error names the operation, the allowed
fields, the received arguments, the shape to resend, and that `query`,
`responseText` and `saveResponse` never carry request fields.
- Keep server-side acceptance of `saveResponse` on `call_api`, so saved
calls and receipts from earlier runs stay valid for recovery.
- Regenerate the runner contract, catalog, seeded fixture and manifest.
Update `doc/runner-api-tools.md`.

## Verification

- Red at master sources: the 3 replayed recorded writes (POST
resolve-from-comment, PATCH issue, POST comment, each with `query: {}`
and `saveResponse: false` and no body) were sent to the route instead of
being refused, and the schema test found no `save_api_response` tool. 4
tests failed.
- Green with this change:
`server/src/services/native-runtime/runner-api.test.ts` 2105/2105
passed; `runner-api.integration.test.ts` 61/61 passed;
`runner-bridge-tools.test.ts`, `task-document-key-contract.test.ts` and
`runner-api-artifacts.test.ts` passed; runner
`protocol-action-contracts.test.ts` 70/70 passed; server `tsc --noEmit`
passed.
- `runner-api-artifacts.integration.test.ts` now saves through
`save_api_response` and checks that a write operation ID or a body is
refused. Its download step needs Linux; it fails on macOS at master too,
so CI covers it.
- Paid E2E, OpenCode + DeepSeek v4.1 flash, [run
43138806990](https://github.com/paperclipai/paperclip/actions/runs/43138806990)
on this fix: 5/5 affected cases passed
(questions-and-approvals.reject-and-revise,
steering.edit-queued-message, task-coordination.delegate-with-context,
tasks.edit-properties, workspaces.preview-service). Body-less write
refusals went from 30 to 0, and body-less POST 400s went from 48 to 0,
compared with the earlier cohort on the same cases. The approval resolve
call was sent once (it was sent 24 times before). Every logged write
carried a body; the remaining non-2xx writes are the cases' intended
stale-edit and failing-fixture responses.
- Run-history redaction also covers `save_api_response` arguments; a
regression test fails on the earlier source and passes now.

## Risks

- Agents that learned `call_api` + `saveResponse: true` in a running
session will see a schema refusal and must use `save_api_response`.
Saved calls keep working for recovery.
- The new POST/PUT refusal applies only when the JSON request schema
names required fields, so writes whose body is optional are still sent.
- One more small read tool is advertised when API tools are enabled.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 20:46:14 -06:00
DottaandClaude Opus 5.5 5626a0299c fix(opencode): keep the answer written before the completion call in the saved reply (#20999)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents that use OpenCode models (for example DeepSeek through
OpenRouter) reply in chat through the Runner's OpenCode driver
> - The driver picks one assistant text part as the turn's final
response. Paperclip saves that text as the reply comment and delivers it
to other channels
> - In a chat eval, the model wrote its full answer with links, called
`paperclip_finish`, and then wrote "Answered above — two inbox items
need your attention ..."
> - The driver saved only the later line. The chat showed both texts
from the run stream, but the saved reply, its deliveries and the next
turn's history held only a line that pointed at nothing
> - This pull request keeps all prose written after the last work tool
in the saved reply in that case, and lets the chat show it once
> - The benefit is that the saved reply holds the answer the person saw

## Linked Issues or Issue Description

**What happened?**
An OpenCode chat turn wrote its answer before the completion call, then
a short line after it that refers back to the answer. The saved reply
comment held only "Answered above — ...". The full answer with its links
existed only as progress text in the run stream.

**Expected behavior**
The saved reply holds the answer. Other channels, notifications and the
agent's next turn read the same answer that the person saw in chat.

**Steps to reproduce**
1. Use an OpenCode agent. Ask a question in chat that the agent answers
with links.
2. The model writes its answer, calls `paperclip_finish`, and then
writes a short line that refers to "above".
3. Read the saved reply comment with `GET /api/issues/{id}/comments`. It
holds only the short line.

**Paperclip version or commit**
master at 9dfdeaad86.

## What Changed

- `OpenCodeServerDriver` settles the final response from all eligible
prose parts when it selects a later message after the completion call
and the model also wrote prose after its last work tool and before that
call. It joins the parts in order with a blank line. Other selection
paths do not change.
- The chat transcript adapter removes the latest progress messages when
they compose the saved response exactly, so the text does not show
twice.
- Added a fake-server fixture and a driver test for the "answer, then a
line that refers back" turn. Updated the existing post-tool test: the
saved reply now keeps the earlier prose too, because structure cannot
tell commentary from an answer.

## Verification

- `cd packages/paperclip-runner && npx vitest run
src/drivers/opencode/opencode-server-driver.test.ts` (80 tests pass).
- `cd ui && npx vitest run
src/components/task-chat/transcript-adapter.test.ts` (117 tests pass).
- Red/green: with the driver change removed, the new test and the
updated post-tool test fail. With the change, they pass.
- `npx tsc -p packages/paperclip-runner/tsconfig.json --noEmit` passes.

## Risks

- A turn that wrote commentary after its last work tool, then its answer
after the completion call, now saves both texts. The person already saw
both in chat. We accept this. Losing the answer is a worse failure than
keeping a short note.
- No saved run state, session state or event shape changes. A run that
the previous release saved resumes without change; only the text of a
newly settled final response differs.

## 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 20:19:38 -06:00
8818402e7a feat(inbox): route requests to people and record personal outcomes (#20959)
## Thinking Path

> - Paperclip helps people manage AI agents and their work.
> - The inbox collects requests that need a human decision.
> - Teams need to know who owns each request and who may act on it.
> - A shared close operation must differ from hiding a card for one
person.
> - This pull request adds personal routing and durable outcomes to the
existing attention service.
> - People can claim or transfer work and inspect what another person
did for them.
> - Domain permissions still decide who may resolve each request.

## Linked Issues or Issue Description

**Subsystem affected**

Server, shared contracts, and database. This is the routing foundation
for a stacked inbox UI change.

**Problem or motivation**

A company-wide attention feed does not give teams a clear personal
queue. Archive, withdrawal, transfer, and decisions by another person
need distinct scopes and records.

**Proposed solution**

Resolve a request audience from its addressee, task participants, and
configured owners. Provide personal, unassigned, company, all-assignee,
and archived views. Add claim, transfer, personal archive and snooze,
shared withdrawal, receipts, and personal history.

**Alternatives considered**

A single shared inbox leaves ownership and personal hiding ambiguous.
The service keeps one canonical request and projects its audience
instead of copying requests for each person.

**Roadmap alignment**

This extends the existing Multiple Human Users and Agent Reviews and
Approvals capabilities in ROADMAP.md.

**Additional context**

Refs #19363 and #19520 for the current inbox navigation and confirmation
replacement rules. Refs #20268 for related actionable-request work. That
PR filters actionability; this stack adds audience routing and personal
outcomes.

This is part 1 of a two-PR stack. The inbox UI PR #20961 targets this
branch.

## What Changed

- Add nullable company and project decision owners and a company budget
owner.
- Add audience views and personal badge counts to the canonical
attention projections.
- Add claim, transfer, withdrawal, personal archive, snooze, and restore
operations with permission checks and activity records. Lock addressed
transfers to the current addressee or routing administrators, and
validate the recipient against canonical resolution policies.
- Add receipts and scoped history for work handled by another person.
- Keep closed-task requests and replaced confirmations out of current
cards.
- Add an idempotent migration and a concurrent receipt-history index.
- Update typed fixtures so the existing UI remains compatible with the
new contracts.

## Verification

- Server layered typechecking passes, including all three test-tree
partitions.
- Seven routing and interaction-resolution suites pass all 249 tests
against a dedicated external PostgreSQL test database on `4caecc21d8`.
Six new regressions failed before the repair; they cover takeover
followed by approval, legitimate delegation, excluded creators/review
requesters, and unauthorized claims.
- Prior focused verification covers private approval history and
receipts, transfer visibility, audience escalation, withdrawal
governance, hidden-card counts, per-recipient confirmation replacement,
and SDK source contracts.
- A synthetic three-person company passed 37 routing and lifecycle
acceptance checks. The UI stack adds browser acceptance for the shared
controls.
- The complete [GitHub Actions CI
matrix](https://github.com/paperclipai/paperclip/actions/runs/43134997331)
passes on `4caecc21d891677323a7335f23b1a015cce24c13` (Linux/Node 24:
typecheck, build, general and serialized tests, Runner support, browser
shards, and aggregate gates). Greptile is 5/5 on that exact head with no
open findings. The fresh security scan passes after explicitly
rechecking the addressee-transfer repair.
- `pnpm verify` reports exit 2 because a CI-global script changed; full
CI supplies that broader coverage. Embedded PostgreSQL could not start
on this Mac because shared-memory slots were exhausted, so local
database checks use a separate external test database. Linux CI covers
the embedded setup.

## Risks

- Personal badge counts change to the viewer's assigned requests.
Accessible feed totals and task membership keep their existing scope.
- The migration adds nullable owner fields and a dismissal fingerprint.
Its index runs outside a transaction. The migration is classified and
rerunnable.
- Routing changes the addressee used by resolution authorization. The
current addressee or a routing administrator must authorize an addressed
transfer; the recipient must satisfy writer access, canonical resolver
exclusions, and task review policy under row locks.
- The shared validator export barrel crosses 1,000 lines because it
exports the new attention schemas. Schema implementation stays in the
dedicated attention validator module.

## Model Used

OpenAI GPT-6 through Codex. Used repository tools, code execution, and
browser checks. This session does not expose a more specific runtime
model ID or its context-window size.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have 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
Paperclip ticket id
- [x] I have run focused tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
(OpenAPI contracts and docs/api/attention.md)
- [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: Scott Tong <scott@scottsmbpm5max.lan>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-11 17:49:39 -08:00
TonioandPaperclip 9dfdeaad86 Refine agent workspaces and organization charts (#20973)
Refine agent details, identity editing, responsive controls, and organization charts with three card tiers, clearer save feedback, and stable filtering.

Guard instruction-file renames and root configuration updates in one transaction through the agent lifecycle integration, preserving conflict checks, rollback, and configuration history.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-11-12 01:23:59 +00:00
Nicky LeachandPaperclip 38501b1170 Add turn-scoped attachment downloads to native Runner (#20956)
## Thinking Path

> - Paperclip manages AI agents and their work.
> - Native agents open input images with a file reader.
> - Plugin Runner tasks use an authenticated Runner connection.
> - Runner needs a way to fetch an authorized file from the tenant app.
> - The tenant grants access to the exact attachment for the active run.
> - Runner downloads and verifies it before it returns a local path.

## Linked Issues or Issue Description

Refs #19740. Builds on the attachment authorization and command
transport added in #20939.

## What Changed

- Add the PRP v3 `runner.attachment.download` command for native
workspace providers.
- Issue short-lived grants for exact authorized wake and historical
attachments. Recheck the active run, source, size, and SHA-256 at the
tenant endpoint.
- Download current-wake inputs before provider dispatch. Download
historical files when the authorized chat reader asks for them. Preserve
SSH and sandbox command staging as a fallback.
- Run HTTPS transfers off the Runner control loop. Report completion
with one PRP event per transfer. Drop pending results on cancellation
and clear files at turn settlement, authority rotation, and restart.
- Grant restricted Codex sessions read-only access to the attachment
directory.

## Verification

- 69 targeted server attachment tests passed. They cover wake and
historical downloads, authorization failures, and command fallback.
- 22 focused historical attachment tests passed, including grant
redemption under a held database lock.
- Six focused Rust attachment tests passed, including a source-pinned
Codex sandbox check. The sandbox could read the staged file but could
not modify it or read adjacent Runner state.
- The real Runner transport test confirmed one durable command and one
completion event for a transfer. Restart-continuation tests confirmed
fresh attachment paths appear before dispatch.
- Server and Runner TypeScript checks, Rust formatting, and PRP
generated schema checks passed.
- Required GitHub CI passed on `7a33752bd7`. [CI
run](https://github.com/paperclipai/paperclip/actions/runs/43122967837)
- Greptile scored the same revision 5/5 with no unresolved review
threads.
[Review](https://github.com/paperclipai/paperclip/pull/20956#issuecomment-6808582134)

## Risks

- Saved provider and Runner identities keep their existing formats. A
previous Runner falls back to command staging where the target supports
it. A restart continuation refreshes temporary attachment paths before
dispatch.
- Plugin Runner tasks require a compatible Runner ROM and outbound HTTPS
access to the configured tenant origin.
- Live agent-computer qualification remains a release check.

## Model Used

OpenAI GPT-6 through Codex, with repository inspection, shell tools, and
local test execution. Exact variant and context window are unavailable
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: #123` /
`Closes #123` / `Refs #123` or (b) 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
- [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 16:28:02 -08:00
DottaandClaude Opus 5.5 1880526603 fix(runner): never start an OpenCode session without its Paperclip tools (#20964)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native OpenCode agents use Paperclip tools through a local MCP
bridge in the runner
> - OpenCode 1.18 connects each MCP server one time per process. It does
not retry
> - One failed `initialize` or `tools/list` at startup marks the
Paperclip server "failed" for the full session
> - The agent then works with only built-in tools. In one chat it told
the person "Switched the working design to blue", but no saved state
supported this claim
> - This pull request makes the runner confirm the Paperclip tools while
the session loads, reconnect them, restart OpenCode if necessary, and
fail the session by name if the tools stay unavailable
> - The benefit is that an OpenCode turn never starts without its
Paperclip tools, so the agent cannot silently claim actions that it
could not do

## Linked Issues or Issue Description

Refs #20886, #20921.

**What happened**
In chat eval `steering.steer-active` (private paid run 43086418488, on a
commit with #20886 and without #20921), two of three OpenCode sessions
started without Paperclip tools. One was the CEO chat (fresh session).
One was the worker task after its saved session resumed in a new
OpenCode process. The CEO agent ran `opencode mcp list` and saw
`paperclip ✗ failed — Failed to get tools`. It then sent a request to
the bridge without the bearer token and got HTTP 401. That 401 came from
its own probe, not from OpenCode. The run still succeeded with the reply
"Switched the working design to blue …", because a plain chat reply
counts as the result.

**Expected behavior**
Every native OpenCode session starts with the Paperclip MCP tools. If
the tools cannot be loaded, the session fails with a named reason, and
the turn does not start without tools.

**Steps to reproduce**
1. Start OpenCode 1.18.34 with a remote MCP server whose first
`tools/list` request fails (for example, the server resets the socket
one time).
2. `GET /mcp` returns `{"paperclip":{"status":"failed","error":"Failed
to get tools"}}`. OpenCode keeps this status for the life of the
process.
3. `POST /mcp/paperclip/connect` returns `true`, and then `GET /mcp`
returns `connected` (about 8 ms). We measured this with the real 1.18.34
binary.

This is the same class as #20886: `Connection: close` made the one-shot
startup connect fail more often (about 9 of 25 sessions), and #20921
removed the trigger. Any other transient failure at startup still
produced a toolless session. This change removes the class.

## What Changed

- `opencode-server-driver.ts`: while a session opens (fresh, resumed or
recovered), the driver reads OpenCode's `GET /mcp`. If the `paperclip`
server is not `connected`, it calls `POST /mcp/paperclip/connect` up to
two times. If the server is still not connected, it throws
`paperclip_tools_unavailable`. The existing startup retry restarts
OpenCode for this error (three attempts in total). After that, the
session fails visibly.
- The check runs in parallel with session creation or resume. The added
work on a healthy start is one local HTTP request.
- `fake-opencode-server.mjs`: the fixture now serves `GET /mcp` and
`POST /mcp/paperclip/connect`. It connects to the real bridge with the
configured token, and it can simulate failed connects.
- New `paperclip-tools-startup.test.ts`: reconnect after one failed
connect, restart after a process stays failed, reconnect on a saved
session that resumes in a new process, and a named failure when the
tools never load. All four tests fail on master (master opens a toolless
session).

## Verification

- `pnpm exec vitest run
src/drivers/opencode/paperclip-tools-startup.test.ts` (in
`packages/paperclip-runner`): 4 passed. Red on master: 4 failed.
- All OpenCode driver, proxy, live runnerd OpenCode and native-runtime
server suites that use the fixture: 29 files, 567 tests passed.
- `node --test scripts/contracts/contract-fingerprints.test.mjs`: 11
passed. `tsc --noEmit -p packages/paperclip-runner`: clean.
- Real OpenCode 1.18.34 probe (failure, then `connect`, then
`connected`) as in the steps above.
- Paid chat eval run 43117909094 on this change (steering.steer-active
and three other OpenCode chat cases). Result is in a comment on this PR.

## Risks

- Low. A healthy start adds one local `GET /mcp` request, in parallel
with session creation.
- If OpenCode cannot load the Paperclip tools after reconnects and
restarts, the session now fails with `paperclip_tools_unavailable`.
Before, it ran without tools. This is intended.
- No persisted run, session or pack state changes. A session saved by
the previous release resumes through the same open path and gets the
same check.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 18:07:38 -06:00
DottaandClaude Opus 5.5 23909077ad fix(retry): re-deliver the failed run's wake when the board retries it (#20970)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - When a run fails, the board shows a Retry button. The Retry starts a
new run with the wake reason `task_retry`.
> - A failed run can serve a wake that happens once, for example a
scheduled task monitor check (`issue_monitor_due`). The monitor is
consumed when it fires.
> - The Retry run did not tell the agent which wake it retries. The
agent saw only `task_retry` and the earlier interruption.
> - In the restart-chaos harness, the agent followed its brief ("act
only on a board comment or a scheduled monitor check"), decided that the
Retry was not the monitor check, and changed nothing. The run succeeded,
but the task stayed `in_progress` with no run, no wake and no monitor.
The person clicked a Retry that worked mechanically but lost the work it
was for.
> - This pull request records the retried run's own wake reason on the
Retry's continuation trigger (`trigger.retriedReason`) and shows it in
the wake summary.
> - The benefit is that a Retry re-delivers the same wake that failed.
Dotta's rule: if a Retry button shows up, it still needs to work.

## Linked Issues or Issue Description

**What happened?**
Restart-chaos wave `w-20261111T230537-s6402` (Claude, Daytona warm,
volume-free server mode, restart fault K1), task t03 (workload W2). The
phase-2 monitor wake started run `60d821af`. That run failed with
`native_session_interrupted`. The board Retry was accepted (HTTP 202).
Retry run `a5a3958e` started 2 s later and succeeded. Its context had
`wakeReason: task_retry` and `executionContinuation.trigger = { reason:
"task_retry", sourceRunId: "60d821af…" }`, with no reference to
`issue_monitor_due`. The agent's summary: "This wake was a task_retry
resuming the previously interrupted run, not a new board comment or
scheduled monitor check, so per instructions I changed nothing." The
issue stayed `in_progress` with `executionRunId = null`, monitor status
`triggered` and no `nextCheckAt`. The harness graded
`R3:retry_did_not_complete` and U1 `no_owner_no_action 182s`.

**Expected behavior**
A Retry of a failed run re-delivers that run's wake. A Retry of a failed
monitor check is handled as that monitor check.

**Steps to reproduce**
1. Give an agent a task that acts only on a scheduled monitor check.
2. Let the monitor fire, and make the monitor run fail (restart fault K1
during the run).
3. Click Retry on the task.
4. Before this change, the Retry run's prompt says only `reason:
task_retry`, and the agent does not do the monitor work.

**Additional context**
The same signature (`R3:retry_did_not_complete:W2C:K0`, Codex, monitor
rewake run `bac90594` failed `native_session_interrupted`, Retry run
`cfb24146` succeeded without the phase-2 marker) is in wave
`w-20261111T192658-s5201` task t06. That case was attributed to the
response-wake loop that #20935 fixed. Its shape matches this cause: the
Retry run served the monitor wake but did not do the monitor work. That
wave kept no database, so I could not read its run context directly.

Refs #20935

## What Changed

- `buildExecutionContinuation` adds `trigger.retriedReason` when the
current wake is a Retry (`task_retry` or `retry_failed_run`). The value
is the source run's own wake reason. A Retry of a Retry inherits the
first non-retry reason from the source run's saved continuation.
- `ExecutionContinuationEnvelope.trigger` gets the optional
`retriedReason` field.
- `renderPaperclipWakePrompt` adds `- retried wake: <reason> (this Retry
re-delivers the wake the failed run did not finish; handle it as that
wake)` to the wake summary for fresh and resumed sessions.
- New test in `server/src/services/execution-continuation.test.ts` on
embedded Postgres: a failed `issue_monitor_due` run, a Retry of it, a
Retry of that Retry, and a non-retry wake.

## Verification

- Red on master: `cd server && npx vitest run
src/services/execution-continuation.test.ts -t "carries the failed run"`
failed with `trigger` = `{ reason: "task_retry", sourceRunId }` and no
`retriedReason`.
- Green: the same file passes (45 tests).
- `tsc --noEmit` passes for `packages/shared`, `packages/adapter-utils`
and `server`.
- `pnpm verify` exits 2 (a shared type changed), so required CI supplies
broad coverage.
- Chaos: the existing cell W2 × K1 (Claude, Daytona warm) reproduces
this when the fault lands in the monitor rewake run. W2C (Codex) shows
the same shape. I did not rerun a wave for this PR.

## Risks

- Low risk. The change adds one optional field and one prompt line. It
changes no admission, lock, status or authority path.
- The field is computed only for Retry wakes. Other wakes are unchanged.
- Runs saved by an older version have no `retriedReason`. A Retry built
from them reads the source run's `wakeReason`, which older runs already
saved.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code with tool use (shell, file edits, 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 (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 18:04:19 -06:00
DottaandClaude Opus 5.5 d1e4109df1 fix(runner): keep a connected adopted Runner through a failed liveness probe (#20965)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - After a controller restart, the controller adopts the remote Runner
that kept running in the sandbox. A monitor then checks the Runner's
liveness every 250 ms through the environment plugin.
> - #20940 and #20945 made a stalled plugin probe "unknown" for the
launched Runner's monitor, reattachment and launch. The adopted Runner's
monitor still failed the run on the first probe that failed without an
answer.
> - A short plugin stall after adoption therefore failed a healthy run
with `native_adopted_runner_identity_unverifiable`.
> - This pull request uses the evidence that is available: while the
adopted Runner holds its authenticated connection to the controller, a
liveness check that fails without an answer does not fail the run, for
at most the reconnect grace.
> - The benefit is that a healthy adopted Runner survives a short plugin
stall. A check that reports the process gone still fails the run at
once.

## Linked Issues or Issue Description

Refs #20940, #20945

**What happened?**
`#watchAdoptedRunner` in
`packages/paperclip-runner/src/live/runnerd-codex-transport.ts` called
`failTransport("native_adopted_runner_identity_unverifiable: runner
liveness could not be revalidated")` on the first `isAlive()` rejection.
For a remote adopted Runner, `isAlive()` is a plugin
`environmentExecute` RPC. A plugin stall makes it reject, even while the
Runner holds its authenticated connection and works normally.

**Expected behavior**
A rejection is not an answer. While the Runner holds its authenticated
connection, the monitor keeps checking. The bound is the reconnect
grace. A check that returns `false` (the process is gone) still fails
the run at once.

**Steps to reproduce**
1. Adopt a remote Runner after a controller restart.
2. Stall the plugin worker, so that the liveness probe rejects.
3. Within 250 ms the run fails with
`native_adopted_runner_identity_unverifiable`.

**Paperclip version or commit**
master at ef755cecc6.

## What Changed

- `#watchAdoptedRunner`: on an `isAlive()` rejection, the monitor
records when verification was first lost. It does not fail the transport
while the control plane has an authenticated Runner connection and less
than `runnerReconnectGraceMs` has passed. A successful check resets the
clock.
- Unchanged: an `isAlive()` result of `false` fails the run at once. A
rejection with no Runner connection fails it as before. A transport with
no reconnect grace (local adoption) fails it as before. The close path
still uses `isAlive()` directly.
- `doc/execution-semantics.md` describes the rule.

## Verification

- Red on master: `npx vitest run
src/live/runnerd-codex-transport-adoption.test.ts -t "liveness probe
rejects while"` (in `packages/paperclip-runner/`). It failed with
`native_adopted_runner_identity_unverifiable: runner liveness could not
be revalidated`.
- Green: the test passes. The real adopted runnerd keeps its connection,
`isAlive()` rejects for 1.5 seconds (6 monitor ticks), the run still
answers `thread/read`, no signal is sent, and the runner is alive.
- `runnerd-codex-transport-adoption.test.ts` passes (24 tests),
including "retains authenticated adopted-runner ownership when a close
probe rejects". `tsc --noEmit` passes for `packages/paperclip-runner`.

## Risks

- A remote adopted Runner whose liveness cannot be verified, but which
stays connected, keeps running for up to 7 minutes before the run fails
closed. Its own authenticated connection is the evidence during that
time, and no exit is reported.
- The change touches `packages/paperclip-runner/src`, so it changes the
Daytona runner image content ID.

> 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 17:49:13 -06:00
DottaandClaude Opus 5.5 d9d5958d09 fix(runner): reject an out-of-order controller command instead of exiting (#20946)
## Thinking Path

- Paperclip's controller and the sandbox Runner exchange numbered
commands; the Runner refuses a command whose number is not exactly one
past the last it processed.
- Staging's most common "Runner gone" failure was a follow-up controller
whose journal was behind the Runner. #20928 removed the cause on the
server (a warm follow-up now launches from the journal its retired
predecessor saved) and added a defence that reconciles a journal behind
only by the predecessor's settled close.
- The Runner still treated an out-of-order command as fatal:
`durable/state.rs` returned an error and the process exited. A
controller with a stale journal from any other cause (state saved by an
older release, a mid-run deploy) therefore lost the Runner process and
the run could only fail as "Runner gone"
(`runner_remote_process_identity_unavailable`).
- This pull request makes the Runner refuse such a command with a
precise result instead of exiting, and makes the server rest that run
with a precise code and a Retry whose next run reconciles the journal
(the #20928 defence) before it opens the session.

## Linked Issues or Issue Description

- Refs #20928 (the server-side root-cause fix and the journal
reconciliation this relies on)
- Refs #20907 (makes runnerd exit text visible in the run log)
- Duplicate search: searched PRs for "controller sequence contiguous",
"controller_sequence_mismatch" and "out-of-order command"; none changes
the Runner's handling.

## What Changed

- `runner-core/src/durable/state.rs`: a new command whose
`controllerSeq` is not `lastControllerCommandSeq + 1` (behind or a gap)
now yields `CommandDisposition::Reject` with `status: "rejected"` and `{
code: "controller_sequence_mismatch", message, expectedControllerSeq,
receivedControllerSeq }`. The command is not executed or journaled and
the last sequence is unchanged; the process keeps running. Replay of an
already-processed command and the compacted-history rejection are
unchanged.
- `native-session-executor.ts`: a failure carrying
`controller_sequence_mismatch` is classified as
`native_controller_journal_behind_runner`, a permanent (terminal)
failure with a Retry action that says no provider work began and that
Retry reconciles the journal from the saved checkpoint or stops with a
precise error.
- Tests:
- Rust `tests/durable_recovery.rs`: out-of-order (behind and gap)
commands are rejected with the precise code, the journal is unchanged,
and the next contiguous command executes. The existing
`command_gaps_and_identifier_reuse_fail_closed` unit test now asserts
the rejection instead of an error.
- Real runnerd `src/live/runnerd-stale-controller-journal.test.ts`: a
follow-up controller with a journal missing the predecessor's close now
gets `"code":"controller_sequence_mismatch"`, the Runner process stays
alive and its durable sequence is unchanged; with the current journal it
resumes.
  - Server: the classification and terminal disposition of the new code.

## Verification

- Red (master Runner): the Rust test panics on `expect("an out-of-order
command is not fatal")`; the real-runnerd test receives `runnerd exited
unexpectedly with code 1: paperclip-runnerd: controller sequence must be
contiguous: expected 6, received 4`.
- Green: `cargo test -p paperclip-runner-core --test durable_recovery`
(4 passed); `cargo test -p paperclip-runner-core` lib suite (the only
other failure, `task_memory::tests::monitor_bounds_notices`, is a timing
test that passes alone); `npx vitest run
src/live/runnerd-stale-controller-journal.test.ts` with a runnerd built
from this branch (2 passed); server `native-session-executor.test.ts`
focused tests (71 passed).
- `pnpm --filter @paperclipai/paperclip-runner check:acpx-profiles`:
profiles match the release manifest. No ACPX profile, attestation, pin
or pack contract schema changed (`rules/acpx-profile-identity.mdc`): the
change is Runner source behaviour with an existing command-result shape,
so no profile version or `commandDigest` changes.

## Risks

- A controller that is *ahead* of the Runner (a gap) is now rejected
instead of fatal. Neither case executes the command; the Runner journal
is unchanged, so this is strictly safer than exiting.
- The rested run needs a Retry. Before this change the same run failed
after its retries as "Runner gone" with no precise cause.
- Runner source changes the Daytona image content ID; a new image is
needed for chaos waves on this revision.

## 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 16:14:31 -06:00
Devin FoleyandPaperclip 6a43890326 Fix image attachments in remote native workspaces (#20939)
## Thinking Path

> - Paperclip manages AI agents and their work.
> - Native runs must receive the files that users attach to their
messages.
> - Remote wake preparation returned file metadata without readable
bytes.
> - The historical chat reader also rejected every remote target.
> - Command-backed sandboxes and SSH targets already provide a bound
command transport.
> - This pull request stages verified files through that transport and
clears them after the turn.
> - Agents can then open attached images with their normal image reader.

## Linked Issues or Issue Description

Refs #19740. This change covers SSH and command-backed sandbox
execution, including Daytona. Plugin Runner tasks without a command
transport still need a separate versioned file-transfer contract. This
PR does not close that broader issue. The open attachment-recovery PR
#20778 concerns provider-session attachment journals, not input files.

**What happened?**

A user uploaded an image and sent it to an agent. The remote native run
received its filename and size, but no readable file. Wake preparation
reported `remote_workspace_staging_unsupported`. The historical read
fallback also refused remote execution.

**Expected behavior**

Copy the exact admitted attachment into the bound remote workspace. Let
the provider read the image. Preserve company, task, source-comment,
size, and hash checks. Clear temporary file contents when the turn ends.

**Steps to reproduce**

1. Select a native agent on a command-backed sandbox or Linux SSH
environment.
2. Attach a PNG to its current task or in-app chat message.
3. Ask the agent to inspect it.
4. Check that wake preparation supplies a readable workspace-relative
image path.

## What Changed

- Add a remote staging owner with bounded commands, stdin transfer, hash
verification, opaque paths, and safe image extensions.
- Confine Linux filesystem operations through directory descriptors.
Refuse symlinks and replacement files during cleanup.
- Bind current wake files and authorized historical external-chat reads
to the selected lease transport.
- Join active writes during cancellation. Retain failed cleanup work
until scope close.
- Tell providers to open image files with an image-capable reader.
- Handle stdin stream errors in the shared SSH transport. Failed uploads
return failure instead of raising an unhandled EPIPE or reporting
success.
- Add database, transport, cancellation, integrity, cleanup,
executor-wiring, and Linux filesystem regressions. Document support and
limits.

## Verification

- Prepared an isolated source checkout with `node
scripts/agent-preflight.mjs --skip-rust-debug`.
- Focused suites: `pnpm exec vitest run
server/src/services/native-runtime/remote-attachment-staging.test.ts
server/src/services/native-runtime/native-runner-file-handoff.test.ts
server/src/services/native-runtime/chat-attachment-read.test.ts` — 74
passed, 4 Linux-only tests skipped.
- Executor checks: `pnpm exec vitest run
server/src/services/native-runtime/native-session-executor.test.ts -t
'attachment staging to the remote|stages from the authenticated|verifies
frozen staged admission'` — 7 passed.
- Server typecheck: `pnpm --filter @paperclipai/server typecheck`,
followed by the server layered check after the final changes.
- `git diff --check` passed. The local secret, PII, and
private-reference diff scan found no matches.
- `pnpm verify` returned exit 2 because the documentation change
requires broad CI. No full local suite is claimed.
- The four Linux filesystem tests were skipped locally because this
macOS host has no available Docker daemon. Linux CI executed all 17
remote-staging tests successfully, including those four confinement
tests: [server shard
10](https://github.com/paperclipai/paperclip/actions/runs/43102035096/job/130796646266).
- SSH regression: the real pipe test reproduced two unhandled EPIPE
errors before the fix, plus false success when the peer exited with code
zero. The fixed SSH tests and existing argv tests pass (12 tests). The
remote-staging transport suite also passes (13 tests; 4 Linux tests
require CI).
- `pnpm --filter @paperclipai/adapter-utils typecheck` passed after the
SSH change.
- Final revision `1c5a9288b8d5f267ddeac84638ff6075dbbbf8ec`: all
applicable CI jobs passed, including typecheck, build, general and
serialized tests, Runner checks, canary dry run, and all eight browser
shards. [CI
run](https://github.com/paperclipai/paperclip/actions/runs/43102035096).
Linux CI also passed all 6 native SSH tests: [server shard
5](https://github.com/paperclipai/paperclip/actions/runs/43102035096/job/130796646349).
- Greptile reviewed that final revision at 5/5 with no remaining
findings. The review thread is resolved. GitHub reports the PR mergeable
with a clean merge state.
- No live model turn or deployed release is claimed.

## Risks

- Remote staging requires Linux, Node, and a command transport bound to
the selected lease. Plugin Runner tasks without that transport retain
their unavailable result.
- Each file uses bounded independent commands. Slow transports can delay
preparation or cleanup. File bytes stay out of command arguments and
provider-facing error messages.
- An abrupt controller or remote-host failure can bypass cleanup.
Ephemeral sandbox disposal remains the backstop. Retained files are
never blindly overwritten or deleted.
- Previous-release provider sessions and checkpoints retain their format
and identity. New turns receive newly staged input files. Existing
restart-admission checks still apply. No migration, runner-image change,
or account-credential transfer is required.

## Model Used

OpenAI `gpt-6.1-sol` through the Codex harness in T3 Code, with xhigh
reasoning, repository inspection, code execution, and tests. No
subagents were used. The context-window size is not exposed by this
runtime.

## 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 14:12:09 -08: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
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>
2026-11-11 21:29:25 +00: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 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 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 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 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 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 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 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
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
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>
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
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 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
DottaandClaude Opus 5.5 57f1afdf19 fix(daytona): retry a sandbox create that timed out before any agent work (#20828)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - On a Daytona sandbox environment, every new task run first creates a
fresh sandbox
> - When one Daytona create did not start, the plugin waited the full
300 s acquisition budget
> - The run then failed `setup_failed` with `lease_acquire_failed`. No
agent work had started, but nothing retried it
> - On staging this hit two agents in 15 minutes. Each attempt used
about 5 minutes, and the task stopped until a person pressed Retry. The
manual Retry then got a sandbox in 28 s
> - This pull request ends a stuck create early, adopts a create that
started late, and routes the timeout to the bounded bootstrap retry that
already exists for transient provider refusals
> - The benefit is that a stuck create costs about 90 s and recovers by
itself. The task shows Retry only after the bounded attempts are used up

## Linked Issues or Issue Description

Refs #20626 (bounded resume of a reusable sandbox), Refs #20212 (wait
out a Daytona state change), Refs #20609 (force a stuck stop).

**What happened?**
A run on a Daytona sandbox environment failed after 307 s with `Failed
to acquire lease for environment "Paperclip Sandbox" (sandbox): Daytona
lease acquisition timed out; allocation cleanup is pending`. The run
recorded `errorCode: setup_failed`, `failureCause:
lease_acquire_failed`, and `executionRecovery: { kind: "bootstrap",
providerWorkStarted: false }`. The task became blocked with Retry. No
automatic retry was scheduled.

**Steps to reproduce**
1. Use a Daytona sandbox environment with the default `timeoutMs`
(300,000).
2. Start a run while Daytona keeps the new sandbox in `creating`.
3. See that the run waits 300 s, fails `setup_failed`, and is not
retried.

**Expected behavior**
A create that has not started before provider work must not cost 5
minutes. The run must retry by itself within bounds. A late sandbox that
has started must be used. A sandbox that was abandoned must be removed.

**Root cause**
- `packages/plugins/sandbox-providers/daytona/src/plugin.ts`
(`onEnvironmentAcquireLease`): the only bound on the create step was the
whole acquisition budget (`timeoutMs`, default 300 s).
- `server/src/services/environment-provider-unavailable-error.ts`
(`readTransientEnvironmentProviderFailure`): only an HTTP status or
errno on the create call counted as transient. The timeout envelope
carries neither, so the run fell through to generic `setup_failed`,
which `isTransientWorkspacePreparationCode` does not retry.

## What Changed

- Daytona: one fresh acquisition is now capped at 180 s. A create that
has not returned after 90 s is looked up once by its creation name. A
sandbox in `started` is adopted, and setup continues. A sandbox in
`pending_build`, `building_snapshot`, or `pulling_snapshot` keeps
waiting inside the cap. Any other state ends the attempt with the
existing cleanup envelope and an acquisition diagnostic whose elapsed
time reached the expired budget.
- Daytona: a late SDK failure no longer runs the compensating delete for
a sandbox that was adopted.
- Host: `readTransientEnvironmentProviderFailure` reads the validated
acquisition diagnostic. An acquisition whose first failure was its own
deadline is transient. The run fails with
`environment_provider_unavailable`, category `timeout`, and uses the
bounded bootstrap retry (30 s, then 60 s). An earlier setup failure
stays `setup_failed`. Message text is never parsed.
- Host: the task reason for this case is plain: "The sandbox for ... did
not finish starting in time. No agent work had started. Paperclip
retries automatically ...".
- Docs: `doc/execution-semantics.md` and
`doc/plugins/SANDBOX_PROVIDER_CAPABILITIES.md`.

## Verification

Red before the fix, green after:
- `server/src/services/heartbeat/run-completion.test.ts` "routes a fresh
sandbox create that timed out through bounded bootstrap recovery" uses
the staging error shape through the orchestrator. Red: `errorCode` was
`setup_failed`. Green: `environment_provider_unavailable`, category
`timeout`, bounded retry scheduled. The `started` and `early_failure`
cases stay `setup_failed` with no retry.
- `server/src/services/environment-provider-unavailable-error.test.ts`:
deadline diagnostic is transient (red, then green). An early failure is
not. The task message is plain.
- `packages/plugins/sandbox-providers/daytona/src/plugin.test.ts`: "ends
a create still pending at 90 seconds" (red: still pending at 90 s;
green) and "adopts a create that started by 90 seconds" (red; green, and
a late SDK rejection does not delete it). "keeps waiting for a create
that is still preparing its snapshot" is a safety case.
- Existing tests that encoded the 300 s acquisition budget now use the
180 s cap.

Commands run locally:
- `npx vitest run src/plugin.test.ts` (Daytona): 414 passed.
- `npx vitest run src/services/heartbeat/run-completion.test.ts
src/services/environment-provider-unavailable-error.test.ts
src/__tests__/environment-acquisition-diagnostics.test.ts
src/__tests__/task-interruption-query.test.ts`: 229 tests passed. The
run-completion file reports an `afterAll` embedded-Postgres teardown
timeout. The same timeout occurs on unchanged `origin/master`, so it is
a local platform fault.
- Daytona plugin `pnpm typecheck` and server `pnpm typecheck`: pass.
- `pnpm verify`: asked for broader CI coverage, because
`doc/execution-semantics.md` is a global CI input. Required CI supplies
that coverage.

Upgrade safety: nothing new is persisted. A task already blocked by this
failure keeps `canRetry: true`. Its Retry starts a new run with a new
creation record, and that run uses the new path. On staging, the manual
Retry of the affected task got a fresh lease in 28 s. The creation
record of the earlier attempt stays with the creation reconciler.

Not verified: a live Daytona create that stays in `creating`. That
cannot be forced on demand.

## Risks

- A legitimately slow create that stays in `creating` or `starting` for
more than 90 s now ends the attempt and retries. Snapshot preparation
states still wait inside the 180 s cap. Staging creates normally take
about 20 to 30 s.
- An abandoned create is removed by the creation reconciler. It can stay
alive until that reconciler settles the record (the existing settle
window). This PR does not change that window.
- Other sandbox providers get the host classification only when they
send the same acquisition diagnostic.

## Model Used

Claude Opus 5.5 (claude-opus-5-5), Anthropic, used through Claude Code
with tool use (shell, file edits, browser for 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
- [ ] 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:04:17 -06:00
Nicky LeachandClaude Opus 5.5 369e238295 fix(projects): make workspace storage the project's only placement choice (#20835)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A repository project gets its working files from core staging or
from a workspace storage plugin
> - #20459 reported that an installed storage plugin attached every new
repository project, so sandbox agents could not use those projects
> - #20587 fixed creation with an **Available in** choice that selects
storage
> - #20651 then saved an environment on every new project and used it
before the agent default to place runs. That solved a problem that did
not exist: the project needs storage, not a run environment
> - Creation saves the first environment of the chosen group, so a
project silently sends every agent's runs to that environment. Project
Configuration also shows two controls (Environment and Workspace
storage) for one decision
> - This pull request removes the project run environment and makes
storage the only placement choice a project owns. Storage is fixed at
creation
> - The benefit is that runs follow the agent again, and project
settings show one clear, read-only fact about storage

## Linked Issues or Issue Description

Refs #20459. Follow-up to #20587 and #20651.

Refs #11644. That issue asks for a project-level run environment
override. This pull request removes the override that #20651 added for
it, because the override is not necessary to fix #20459. #11644 stays a
separate feature request.

Related: #18176 (open) repairs placement through the legacy
workspace-policy field. This pull request does not restore project
placement, so it conflicts with the direction of #18176.

## What Changed

- **Remove the project run environment.** Migration `0466` drops
`projects.environment_id`. Run placement no longer reads the project.
Runs use the agent default, then the instance default, then local
execution. The low-trust task sandbox and the enforced sandbox policies
keep their precedence.
- **Keep the #20651 recovery and binding guards.** A recovered run still
uses its admitted environment lease (source `run`). Company-binding
checks before admission and during lease creation stay.
- **Keep the creation choice.** **Available in** in New Project still
selects storage. It still shows only when the environments need more
than one storage owner. That can only occur when a storage plugin is
installed. `POST /projects` with `environmentId` selects the storage of
that environment and does not save the environment.
- **Fix storage at creation.** `PATCH /projects/:id` rejects any storage
change with `409 project_storage_locked`, from core to a plugin and from
a plugin to core. Before, a core project could move to a plugin.
- **Drop legacy writes.** Older clients can still send
`executionWorkspacePolicy.environmentId`. The service removes it on
create and update.
- **Simplify Project Configuration.** Remove the Environment and
Workspace storage selectors. Show an advisory **Available in** list of
the environments that can use the project's storage. Show it when a
storage plugin is installed or when the project uses one. When the
project's plugin is not installed, show **Storage plugin unavailable**.
Without a storage plugin, show nothing.
- **Issue workspace card.** Remove the "project default" environment
label.
- **Shared contract.** Remove `Project.environmentId`, the
`environmentId` project field, and `expectedStorageProviderPluginId`
from the update schema. The create schema keeps `environmentId` and
`expectedStorageProviderPluginId` from #20587.
- **Docs and maps.** Update `doc/project-environment-compatibility.md`,
`docs/api/goals-and-projects.md`, and the project feature and
agent-access maps.
- **SDK source pins.** Refresh
`tests/runner-e2e/source-sdk-public-inputs.json` for the two shared
files. The shared file blobs return to their values before #20651. The
refresh also corrects the `pnpm-lock.yaml` pin and `lockSha256`:
`master` pins an older lockfile blob than the one it contains.

## Verification

- Targeted server suites pass (227 tests): `project-environments`,
`project-workspace-provider.integration`,
`heartbeat/environment-selection`, `execution-workspace-policy`,
`environment-selection-route-guards`, `environment-run-orchestrator`,
`openapi-routes`.
- Related server suites pass (349 tests): `environment-service`,
`environment-routes`, `plugin-workspaces.integration`, `issues-service`.
- Targeted UI suites pass: `ProjectProperties.concurrency`,
`IssueWorkspaceCard`, `NewProjectDialog`,
`NewProjectDialog.managed-sandbox`, `ProjectDetail`.
- New tests:
- Creation saves storage, not an environment. Core and plugin projects
both reject a storage change with `project_storage_locked`.
  - A legacy policy `environmentId` is dropped on create and update.
- A new run follows the agent default for any project. A recovered run
keeps its admitted lease after the agent default changes.
- Project Configuration has no storage or environment selector. The note
is hidden without a plugin. It lists the core or plugin environments for
the project's storage. It shows **Storage plugin unavailable** for a
removed plugin.
- `pnpm --filter @paperclipai/server typecheck`, `pnpm --filter
@paperclipai/ui typecheck`, and `pnpm --filter @paperclipai/shared
typecheck` pass.
- `pnpm --filter @paperclipai/db check:migrations`, `pnpm
check:token-gates`, `node scripts/check-maps.mjs`, and `node
tests/runner-e2e/source-sdk-public-inputs.mjs <HEAD> --check` pass.
- Not run locally: the full `pnpm test:run`, `pnpm build`, and browser
suites. CI supplies that coverage.
- Manual check: open Project Configuration on an instance with a storage
plugin. Confirm that there is no Environment or Workspace storage
selector, and that **Available in** lists the environments.

## Risks

- **Migration.** `0466` drops `projects.environment_id` and its foreign
key. The migration deletes the saved project environments. Apply it
before you serve the new revision. An old server revision that reads
`projects` fails until it stops. This is the same pattern as earlier
drop-column migrations (`0229`, `0289`).
- **Behavior change for existing projects.** Projects created after
#20651 shipped have a saved environment that overrides the agent. After
this change, their runs follow the agent default again. This is the
intended fix. Before #20651, a project environment selection was not
applied to runs (#11644), so no older behavior is lost.
- **No path from core to a plugin.** A core project, including one
created before a plugin was installed, cannot move to the plugin. To use
plugin storage, create a new project.
- **API.** A `PATCH` that sends `environmentId` or
`expectedStorageProviderPluginId` no longer has an effect. The schema
strips these fields.

> 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`) by Anthropic, in Claude Code, with
tool use (shell, file edits, test runs). Extended thinking was
available.

## 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:00:21 -08:00
DottaandClaude Opus 5.5 0b8ba1ac92 fix(queue): show messages a booting run's first turn takes as sent (#20822)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Task chat lets a person send follow-up messages to a running agent,
and steer them into the live turn
> - A sandboxed run (for example on Daytona) can take minutes to boot
before its provider session exists
> - A message sent during that boot cannot be steered, so it waits in
the steering pane
> - The runner then puts the message into its first turn, and the server
removes it from the queue, but the view has no signal for this, so the
message stays in the steering pane as "queued"
> - That stale local entry also hides the queue id, so Steer and Discard
fail for every later message
> - This pull request tells the view which messages the first turn
takes, and drops local queue entries whose run has settled
> - The benefit is that a message sent while the sandbox boots shows as
sent at once, and later messages keep working Steer and Discard

## Linked Issues or Issue Description

**What happened?**
A person sent a follow-up while the agent's sandbox was booting. The
message stayed in the steering pane with Steer unavailable. After the
agent started, the message stayed there. The agent had received it in
its first turn. Later, Discard on that message failed with "Couldn't
discard. Message is still queued.", and a new message also stayed in the
steering pane.

**Steps to reproduce**
1. Assign a task to a Paperclip Runner agent that runs in a Daytona
sandbox.
2. While the sandbox boots, send a follow-up message.
3. Wait for the agent's first response. The message is still in the
steering pane.
4. After the run hands off to a successor run, send another message and
try to discard the first one.

**Expected behavior**
A message sent while the run is starting shows as a sent message. Later
messages keep working Steer and Discard.

**Actual behavior**
The first message stays in the steering pane. Discard fails, and the new
message cannot be steered until the page reloads.

## What Changed

- `firstTurnQueuedCommentIds` (server, `first-turn-queued-input.ts`)
returns the messages in a deferred queue that the target run's first
turn already sealed, or will take while it is still starting. It uses
the same eligibility rule as `claimFirstTurnQueuedInput`. It reads only
lean columns, so the queue poll does not transfer run snapshots or large
wake payloads.
- The queue read path and the queue mutation path both put these ids in
a new optional `firstTurnCommentIds` field on `IssueQueuedCommentQueue`.
- The task view puts each of those messages in the conversation as input
of that run, at its submission time, and not in the steering pane.
- The task view no longer treats a saved message as a pending local
queue entry after its target run is no longer live. A request that is
still in flight keeps its original target.

## Verification

- Red-green server test: `issue-queued-comments-routes.test.ts` "reports
the messages a preparing/native run's first turn takes, before and after
it seals them" and "reports no first-turn messages once the run's turn
already started". Before the fix, `firstTurnCommentIds` was `undefined`.
- Red-green UI tests: `IssueDetail.test.tsx` "shows a message the
booting run's first turn takes as sent, not queued" (still starting, and
sealed) and "keeps queue controls for a new message after the run that
took an earlier one hands off". Before the fix, the queue held
`['new-message', 'boot-message']` and the queue id was withheld.
- Reproduced on staging first: the run's context held a first-turn
receipt for the stuck message, and the server queue had no entries,
while the view still showed it as queued.
- `npx vitest run` for `issue-queued-comments-routes`,
`queued-comment-consumption`, `queued-comment-postgres`,
`queued-comment-use-cases`: 172 passed. The existing queue-poll byte
budget tests still pass.
- `npx vitest run src/pages/IssueDetail.test.tsx
src/components/task-chat src/lib/issue-queued-comment-queue
src/lib/optimistic-issue-comments`: 1288 passed.
- `tsc --noEmit` for shared, server, and ui: no new errors.
- `pnpm verify` exits 2 because the change touches `packages/shared`.
Required CI supplies the broad coverage.

## Risks

- Low risk. The new field is optional and only for display. Delivery
still belongs to the first-turn claim and the deferred wake.
- If a starting run fails before its first turn, the field goes away and
the message shows in the queue again for the next run. This is correct,
because the deferred wake still holds it.
- The queue read adds one lean run read and one comment read only while
a deferred queue targets a running run.

## Model Used

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

## 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:52:14 +00:00
DottaandClaude Opus 5.5 4529dfb77f fix(runner): keep Codex planning read-only on Daytona without network namespaces (#20820)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A planning task runs a native Codex agent in read-only planning mode
before the user approves a Plan
> - On a Daytona sandbox, every Codex planning run failed before its
first turn: `error creating thread: failed to load AGENTS.md
instructions for environment 'local': fs sandbox helper failed ...
bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted`
> - Codex 0.160.0 reads files through a filesystem helper when the
permission profile restricts reads. That helper always runs bubblewrap
with `--unshare-net`. Daytona has no `CAP_NET_ADMIN`, so bubblewrap
cannot bring up loopback in the new network namespace
> - The planning profile must stay read-only. Codex's plan collaboration
mode is only a prompt, so the external-sandbox profile (#20808 tried it,
then reverted it) would let planning runs write files
> - This pull request keeps the read-only profile, but inside an
external sandbox it gives the profile full-disk read, no writes and
network. Codex then reads files directly and runs commands without a new
network namespace, and it still refuses writes
> - The benefit is that Codex planning works on Daytona and stays
read-only

## Linked Issues or Issue Description

Refs #20808

**What happened?**
A Codex planning task on a Daytona execution environment failed at
startup. The run ended with `provider_session_open_rejected` and the
Codex message `bwrap: loopback: Failed RTM_NEWADDR: Operation not
permitted`. Standard and ask runs on the same sandbox worked.

**Expected behavior**
A planning run on Daytona starts, writes its Plan and asks for approval.
Codex must still refuse file writes and mutating commands during
planning.

**Steps to reproduce**
1. Configure a native Codex agent with a Daytona execution environment.
2. Create a task in planning mode.
3. The first run fails before its first turn with the bubblewrap
loopback error.

## What Changed

- `createIsolatedCodexAppServerArgs`: when
`PAPERCLIP_RUNNER_EXTERNAL_SANDBOX=1`, the
`paperclip-runner-workspace-read-only` profile is now `{":root"="read"}`
(plus the brokered `GH_CONFIG_DIR` write that planning already had). Its
network still follows `PAPERCLIP_RUNNER_NETWORK_ACCESS`. It has no deny
entries, so Codex 0.160.0 treats it as full-disk read and skips its
filesystem helper, which always unshares the network. Local and SSH
planning keep the narrower profile unchanged.
- The Codex session context now records `rootAccess` `read` (planning)
or `write` (default) in an external sandbox, instead of always `none`.
- The Runner still sends `permissions:
"paperclip-runner-workspace-read-only"` and the plan collaboration mode
on `thread/start` and `turn/start` for planning. A new Rust test pins
that for an externally sandboxed read-only launch (no `sandbox:
danger-full-access`, no `externalSandbox` `sandboxPolicy`).
- A new Rust test proves that a launch saved with the previous planning
profile rules has the same stable launch identity as a launch with the
new rules, so saved sessions carry forward.
- New manual Product E2E suite `codex-planning-read-only`
(`runner-codex` on local and Daytona). The planning turn runs one shell
write, writes a Plan whose first line records the outcome, asks for
approval, and finishes after approval. A grader reads the run events and
fails if no write attempt was seen or if any write succeeded.
- `docs/codex-driver.md` describes the external-sandbox planning
profile, why it avoids the helper, and its read scope.

## Verification

- Red on Daytona, master `123ee31552`: [run
43029207500](https://github.com/paperclipai/paperclip/actions/runs/43029207500),
cell `core-compatibility.runner-codex.daytona.plan-revise-accept` failed
twice with `error creating thread: failed to load AGENTS.md instructions
for environment 'local': fs sandbox helper failed with status exit
status: 1: bwrap: loopback: Failed RTM_NEWADDR: Operation not
permitted`.
- Planning turn completes on Daytona: [run
43031462803](https://github.com/paperclipai/paperclip/actions/runs/43031462803),
cell `core-compatibility.runner-codex.daytona.plan-revise-accept`. The
planning run succeeded, saved a two-step Plan with the draft marker and
opened a confirmation for that revision (all API checks passed). The
cell then failed a later browser check: the Plan preview rendered as the
`task-chat-plan-preview-fallback` card, which the harness does not
accept. That is a separate UI placement gap.
- Write attempt on Daytona: [run
43038547892](https://github.com/paperclipai/paperclip/actions/runs/43038547892),
cell `codex-planning-read-only.runner-codex.daytona.plan-write-refused`
on `f30aeb3e72`. The planning run succeeded and ran the shell write
`printf probe > plan-write-probe-....txt || echo WRITE_REFUSED`. It
ended `failed` with exit 1 and no file, because bubblewrap could not
start the command (`bwrap: setting up uid map: Permission denied`). The
write was refused fail-closed by the sandbox launcher, not by a
read-only filesystem error. The stricter grader in a3d43b7bfd reports
this case as 'sandbox failed to start' rather than a pass. Shell
commands in Daytona planning therefore do not run on the current image;
the driver doc records this as a remaining gap.
- Red/green unit test: `npx vitest run
src/drivers/codex/codex-security-config.test.ts` (in
`packages/paperclip-runner`). The new test failed on the unfixed code
(the profile was `":root"="none"` with `CODEX_HOME` denied and network
off) and passes with the fix.
- `cargo test --locked -p paperclip-runner-core --test codex_provider --
external_sandbox` and `cargo test --locked -p paperclip-runner-core
--lib -- external_sandbox_planning_profile_rules
runtime_launch_rebinding`: pass.
- `npx vitest run --config tests/runner-e2e/vitest.config.ts
tests/runner-e2e/codex-planning-read-only.test.ts
tests/runner-e2e/catalog.test.ts`: pass. `pnpm
test:e2e:runner:typecheck`: pass.
- Real Codex 0.160.0 app-server probe (macOS) with the generated launch
arguments and `PAPERCLIP_RUNNER_EXTERNAL_SANDBOX=1`: `thread/start`
returned sandbox `{"type":"readOnly","networkAccess":true}` and loaded
the workspace `AGENTS.md`; `command/exec` with the planning profile
refused `printf x > plan-write-probe.txt` (exit 1, `Operation not
permitted`, no file) and allowed `cat AGENTS.md`. Trace logs showed 0
filesystem-helper invocations with the new profile and 336 with the
previous restricted profile.
- Codex 0.160.0 source: `exec-server/src/fs_sandbox.rs` always sets
`NetworkSandboxPolicy::Restricted` for the helper,
`core/src/agents_md.rs` uses the helper only without full-disk read, and
`linux-sandbox` adds `--unshare-net` only for a restricted or proxy-only
network.

## Risks

- Inside an external sandbox, planning commands can now read the whole
sandbox filesystem, including the isolated Codex home. Default-mode
turns in the same sandbox already have full read and write access, so
this does not widen what the sandbox exposes. Local and SSH planning are
unchanged.
- Inside an external sandbox, planning shell commands fail closed on the
current Daytona image because bubblewrap cannot map user IDs there.
Planning still reads files and writes the Plan through Codex and
Paperclip tools.
- If a future Codex release adds deny-read entries or makes the helper
mandatory, planning on Daytona could fail again. The new Daytona E2E
suite detects that.
- Upgrade safety: the profile filesystem and network keys are excluded
from the stable launch identity, so saved sessions resume with the new
rules (new Rust regression). No persisted schema, pack contract or ACPX
profile changes.

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

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code with tool use (shell, file edits, GitHub CLI) 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
- [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:44:28 -06:00
Nicky LeachandClaude Opus 5.5 ff8ec41e6a feat(runner): upgrade ACPX to 0.22.0 and move patched behavior into Paperclip (#20770)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Paperclip Runner and the shared `adapter-utils` engine run ACP
agents through the embedded ACPX library
> - Paperclip pins ACPX 0.13.1 and keeps a large patch
(`patches/acpx@0.13.1.patch`) on it
> - ACPX 0.22.0 adds host hooks (`onAcpMessage`, `assertActive`,
`processLifecycle`, prompt-response `_meta`, `spawnAgent`-friendly
launch) that make much of that patch unnecessary
> - A large patch is expensive to port and hides behavior that belongs
to Paperclip
> - This pull request upgrades to ACPX 0.22.0 and moves most patched
behavior into Runner and `adapter-utils`
> - The benefit is a patch that is 484 lines instead of 1,334, with the
moved behavior covered by Paperclip tests

## Linked Issues or Issue Description

Refs #20675, #14430, #15075, #17942, #9980

**Proposed enhancement**
Upgrade the embedded ACPX library from 0.13.1 to 0.22.0. Keep only the
patch behavior that stock 0.22 cannot express.

**Motivation**
The 0.13.1 patch held behavior that Paperclip owns, for example usage
receipts, Copilot policy, model checks and the agent environment. Each
ACPX upgrade had to port all of it. ACPX 0.22 gives hooks that let
Paperclip do this work itself.

**Current behavior**
Runner and `adapter-utils` depend on 1,334 lines of patched ACPX 0.13.1
code.

**Expected behavior**
Runner and `adapter-utils` use ACPX 0.22.0. The patch keeps only:
extension methods, Cursor subagent routing, event `_meta` and the
message boundary chunk, the protocol guard hook, `spawnAgent`, and
`requestExtension`.

## What Changed

- ACPX 0.13.1 → 0.22.0 in Runner, `adapter-utils`, the server, the
Daytona runner images and the qualified profiles (`acpxVersion`).
`@agentclientprotocol/sdk` moves to 1.9.0.
- New `patches/acpx@0.22.0.patch` (484 lines). The 0.12.0 and 0.13.1
patches are removed.
- Delivery receipts: Runner confirms a permission reply when
`onAcpMessage` sees the exact outbound reply. Question and extension
replies count as delivered when Runner hands them back to ACPX. New
`interaction-delivery.ts`.
- Copilot policy: the existing policy function runs from `onAcpMessage`.
A violation stops the agent, cancels later permission requests and fails
the next prompt through `assertActive`.
- Model selection: stock 0.22 validation, plus a sidecar gate
(`createAcpxEffectiveModelGate`) that refuses a prompt if the agent
reports another model. Grok binds its model when the session opens.
Antigravity adds its concrete model to the model list ACPX reads.
- Typed session failures: Runner and `adapter-utils` read them from the
turn result `_meta` and fail the turn. They advertise the capability
themselves.
- Pi and Cursor usage receipts: a collector (`prompt-receipts.ts`) binds
them to the turn's own `session/prompt` reply.
- Goal capability: Runner reads it from the `initialize` reply. It keeps
it under `paperclip.goalCapability` in the saved agent capabilities
`_meta` and restores it when a saved session loads.
- Agent environment: Runner and `adapter-utils` set the agent
environment in `spawnAgent`. ACP terminals keep their current
environment. `adapter-utils` blocks inheritance of `XAI_API_KEY` and
refuses to start if the server has an `ACPX_AUTH_*` variable.
- `adapter-utils` spawn hooks (`spawnCwd`, stderr, `onAgentSpawn`): a
new `stockAcpRuntimeOptions` maps them to stock `spawnAgent` and
`processLifecycle`.
- Windows: ACPX 0.22 rejects raw command strings on Windows.
`adapter-utils` now passes an argv array there.
- Grok rules: a Runner guard (`grok-session-rules.ts`) writes
`session/new` `_meta.rules`.
- Turn-dispatch journal (#20675): ACPX 0.22 ignores errors from
`onAcpMessage`. The adapter keeps a failed `dispatched` write and throws
it from the turn's `assertActive`. ACPX calls `assertActive` before the
prompt write, so the write still stops. The real-ACPX test now checks
this route. `docs/durable-recovery.md` is updated.
- Plan events: the sidecar reads 0.22 `status` events tagged `plan`.
- CI: the backup-checkpoint lane now installs through the shared
`setup-ci-dependencies` action, like PR CI, so it resolves a stale
`pnpm-lock.yaml` in place. It runs on any root `package.json` change,
and pull requests never commit the lockfile, so its own frozen install
failed on dependency changes.
- Tests that checked removed patch internals are removed or rewritten:
the response-delivery, Cursor-usage and Pi-receipt package contracts,
and the goal-persistence packaging test. New unit tests cover the moved
behavior.

## Verification

Local runs on macOS (darwin-arm64), Node 26, on the rebased head:

- `pnpm --filter @paperclipai/paperclip-runner exec vitest run
src/drivers/acpx src/cli`: 2,000 passed.
- `tsc -p packages/paperclip-runner/tsconfig.json` and
`tsconfig.test.json`: no errors, except the existing
`@paperclipai/paperclip-eval-kernel` build gap in `workflow-harness.ts`.
- Runner package contracts (`node --test
test/acpx-*-package-contract.test.mjs
test/acpx-sidecar-contract.test.mjs`), `scripts/build-inputs.test.mjs`,
`scripts/contracts/contract-fingerprints.test.mjs`,
`scripts/acpx-patch-packaging.test.mjs`: all pass. `node
scripts/generate-acpx-sidecar-contract.mjs --check` passes.
- `cargo test -p paperclip-runner-core --test acpx_park_preparation
--test acpx_park_readiness --test acpx_warm_attachment --test
native_provider_backend`: 86 passed. `cargo fmt --check` passes.
- `adapter-utils`: `tsc` clean. `vitest run src/acpx-engine
src/mcp-isolation.integration.test.ts`: 457 passed (one Gemini flag test
failed once under load and passes when run alone).
- Adapters: `claude-local` and `codex-local` `acp.quota.test.ts` (23 and
39 passed), `grok-local` (149 passed).
- Server: `scripts/prepare-runner-vendor.test.mjs`,
`native-session-executor.test.ts`, `quarantined-task-retry.test.ts`
(1,416 passed).
- `cargo test -p paperclip-runner-core --lib -- previous_acpx_release
acpx_upgrade unrestorable_saved_sessions`: 3 passed. `node --test
test/acpx-session-carry-forward-package-contract.test.mjs`: passes.
- `patch -p1 --forward` on a clean `acpx-0.22.0.tgz` gives output
identical to the port.
- Under full-suite load, some Runner live and recovery tests time out.
Each one passes when run alone. Full regression is left to CI.

Not run: live provider runs (they need provider credentials), the opt-in
Kimi live peer test, the Gemini filesystem smoke test, and Windows.

## Risks

- Previous-release story: sessions saved by the previous release (ACPX
0.13.1) carry forward. ACPX 0.22 loads the session records 0.13.1 wrote.
Runner moves a saved descriptor that names 0.13.1
(`CARRIED_ACPX_RELEASES`) to 0.22.0, and the sidecar keeps 0.13.1 as the
ACPX value in the session identity, so the saved session key and
identity record still match. The existing launch-profile rebind still
decides which sessions resume: a settled session, or the same run's
interrupted turn through lost-turn recovery. A session saved by any
other ACPX release is still refused as `provider_state_incompatible`.
`rules/acpx-profile-identity.mdc` and `docs/durable-recovery.md` now
describe this.
- Proof: `acpx-session-carry-forward-package-contract.test.mjs` resumes
a session record written by the real ACPX 0.13.1 with its Paperclip
patch. 0.22 loads the same provider session (no `session/new`), runs a
turn and keeps the earlier turn. Rust tests cover a settled 0.13.1
session that the next run attaches to, and a turn interrupted by the
upgrade. A recovery-identity test checks that the session key and
identity record match across the versions.
- `pnpm-lock.yaml` is not committed. CI resolves it for each run, and
the lockfile refresh workflow updates it after merge.
- Behavior change on the server: `adapter-utils` refuses to create an
ACPX runtime if the server environment has an `ACPX_AUTH_*` variable.
Before, ACPX ignored such variables. Stock 0.22 would use them to
authenticate agents with server credentials.
- Delivery confirmation is now Runner-owned. A permission reply is
confirmed by the exact outbound message. Question and extension replies
are confirmed when Runner hands them to ACPX, not when the pipe accepts
them.
- Users can select only models that the agent advertises. Stock 0.22
rejects other model IDs (Claude is exempt).
- ACP terminals keep the sidecar or server environment, as before. This
existing exposure is not changed by this pull request.
- The `adapter-utils` Gemini version probe now runs with the server
environment plus the run variables, not the run variables alone.
- The pack contract digest and profile `acpxVersion` change, so Daytona
needs a new runner image.
- Grok launch flags (`--model`, `--reasoning-effort`) were checked on
1.0.13 but session open needs a login, so Grok keeps the existing
`_meta` 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, in Claude Code with tool
use (shell, file editing, subagents). 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 07:28:00 -08:00
DottaandClaude Opus 5.5 34f2bb78ee feat(connections): connect Sentry with a long-lived user auth token (#20823)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents use app connections to call provider tools, such as the
Sentry MCP server
> - The Sentry connection has only one method: browser sign-in through
the hosted MCP OAuth server
> - That grant expires. An unattended triage agent loses Sentry access
until a person signs in again
> - During that time, a scheduled triage routine completes each run but
reads no Sentry data
> - This pull request adds a second Sentry method that uses a user auth
token
> - The benefit is a Sentry connection that stays valid until a person
revokes the token

## Linked Issues or Issue Description

**Subsystem affected**
App connections: the Sentry entry in the connection catalog.

**Problem or motivation**
A Sentry connection that was created with browser sign-in on 2026-10-11
changed to "OAuth authorization expired. Reconnect this app to
continue." at 04:01 UTC on 2026-11-10. The agent that used it had no
Sentry tools after that time. Each hourly triage run reported the
blocker but could not continue. Unattended agents need a Sentry
connection that does not need a person to sign in again each month.

**Proposed solution**
The hosted Sentry MCP server accepts a Sentry user auth token in the
`Authorization: Sentry-Bearer <token>` header. It reserves `Bearer` for
its own OAuth tokens. Paperclip API-key methods already support a custom
header prefix. Add a Sentry API-key method that uses this header.

**Alternatives considered**
Reconnect with browser sign-in again. That restores access only until
the next grant expiry.

## What Changed

- Add the `mcp-user-token` method ("Use a user auth token") to Sentry in
`scripts/ingest-app-definitions.mjs`. It uses the same server URL and
sends the token as `Authorization: Sentry-Bearer <token>`.
- Regenerate `packages/shared/src/app-definitions/sentry.json`. I did
not include unrelated output that the content corpus drift caused for
other providers.
- Add a permission review entry for the new method in
`doc/connections/tool-method-permission-reviews.json`.
- Add a test for the new method and its header prefix.
- Document the token method, its required scopes, and its ownership risk
in `doc/connections/SENTRY.md`.

## Verification

- `pnpm exec vitest run packages/shared/src/app-definitions.test.ts`: 40
passed. The new assertion fails on `master` because Sentry has only one
method there.
- `pnpm exec vitest run src/__tests__/tool-access-service.test.ts -t
Sentry` (server): 2 passed.
- `pnpm exec vitest run ui/src/pages/apps/app-connect-policy.test.ts
server/src/__tests__/satisfied-connection-intents.test.ts`: 64 passed.
- `pnpm verify` returned exit 2 (a doc is a global CI input). Required
CI supplies the broad coverage.
- Live probe of `https://mcp.sentry.dev/mcp` with an `initialize`
request: `Authorization: Sentry-Bearer <invalid token>` starts an MCP
session (the token is checked on tool calls). `Authorization: Bearer
<same token>` returns `401 invalid_token`. This confirms the server
accepts the header scheme.
- Not done yet: a real agent tool call with a real token after
deployment.

## Risks

- Low risk. This adds a method. The existing OAuth method and saved
connections do not change.
- A user auth token acts as its creator and lasts until revoked. The
method guidance and docs say this. Rotate the token when that person
leaves.
- If Sentry changes the `Sentry-Bearer` scheme, connections that use
this method fail on tool calls. The OAuth method still works.

> 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`) in Claude Code, with tool use
(shell, browser, web search).

## 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 09:26:24 -06:00
DottaandClaude fd1460bb24 fix(native): continue a governed-wait follow-up in a fresh session when its saved provider state is gone (#20809)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness (`tests/restart-chaos/`) runs real agent
tasks and breaks the control plane on purpose, so failures show up
before staging users see them
> - A census of 48 hours of staging failures found runs whose result
save-back failed because the sandbox disk was full (class F). The
harness never filled a sandbox disk, so it missed that class
> - #20758 made a full-disk save end recovery at once with cause
`sandbox_disk_full` and a Retry. Nothing in chaos exercised that path
end to end
> - A real full disk cannot be made on demand. The owner chose a
test-only hook that makes the save report a full disk, not a small disk
> - This pull request adds the hook at the save's sandbox boundary and a
DF1 fault that arms it, clears it, and grades the result
> - The benefit is that every wave can now check the disk-full rest, the
Retry offer, and that Retry works after space is freed

## Linked Issues or Issue Description

Refs #20758 (product behaviour under test), #20678 (finalization hands a
full-disk copy-back to recovery)

**What is the problem?**
The chaos harness had no fault for a full sandbox disk during result
save-back. Staging saw this failure (census class F in
`doc/plans/2026-11-11-staging-failures-vs-chaos-coverage.md`), and chaos
could not reproduce it.

**What is the expected behaviour?**
A wave can schedule DF1. While DF1 is armed, the task's final workspace
saves fail as a full disk. When DF1 is cleared, saves succeed again. The
task must rest with `sandbox_disk_full` and a Retry, and that Retry must
finish the task.

**Why does it matter?**
Without a fault, a regression in the disk-full path (no Retry, a wrong
cause, or a Retry that does not work) reaches staging users before chaos
sees it.

## What Changed

- `server/src/services/sandbox-disk-full-fault.ts` (new, test-only):
`sandboxDiskFullFaultTarget` wraps the target of a final save only when
the harness armed that save. The capture commands then exit 1 with the
exact message the in-sandbox script writes on ENOSPC, and the provider
download fails with `No space left on device`. The gate follows
`@paperclipai/shared/crash-points`: `PAPERCLIP_DISK_FULL_FAULT_CONTROL`
is read once at import and ignored under `NODE_ENV=production`.
Disarmed, the hook is one comparison and returns the save's own target.
- `server/src/services/remote-workspaces.ts`: the capture call in
`saveRemoteWorkspace` passes its target through the hook. No other
product change. `workspace-recovery.ts` is not touched.
- `tests/restart-chaos/disk-full-faults.ts` (new): the DF1 kind, the
control-file seam (atomic writes), the rest evidence
(`activeRecoveryAction.evidence.systemRecovery.failure.cause.label`),
the `disk_full_fault` T check, and the DF1 R3 rule (one expected Retry
that must finish the task).
- Harness wiring: `--faults DF1` (refused without `--env daytona`, with
a storm, or with PV faults), no scheduled restart, arm at launch, clear
at the first rest just before the Retry click, receipts in
`<wave>/disk-full-faults.jsonl`, and ledger fields `diskFull: {
faultedSaves, restedDiskFull, retryOffered, retryWorked }`.
- Docs: a "Sandbox disk full (DF1)" section in
`tests/restart-chaos/README.md`, and the census plan marks DF1 as built.

## Verification

- `cd server && pnpm exec vitest run
src/services/sandbox-disk-full-fault.test.ts`: the gate is off by
default and under `NODE_ENV=production`, the save's own target comes
back when disarmed, only a listed final save is faulted, the product
capture turns it into the named disk-full failure, and clearing the file
disarms the next save.
- `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts
tests/restart-chaos/disk-full-faults.test.ts
tests/restart-chaos/grader.test.ts tests/restart-chaos/scheduler.test.ts
tests/restart-chaos/provider-faults.test.ts`: planning, the control
file, and grading.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos` and the
server `tsc --noEmit`: clean.
- Live wave: a Daytona DF1 wave is running. Its result is added here
before merge.

## Risks

- Low risk for production. The hook reads its variable once at startup
and is ignored under `NODE_ENV=production`. Without the variable it does
one comparison and returns the same target object.
- The hook fails every sandbox command of an armed capture, including
the read-only fallback's inventory. A real full disk can let that
inventory succeed and fail later at the download. The outcome is the
same (`sandbox_disk_full`), but the fallback's inventory step is not
exercised under DF1.
- DF1 runs only with `--env daytona`. A local run has no sandbox
workspace save, so the harness refuses DF1 there.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-11-11 08:47:06 -06:00