Commit Graph
2233 Commits
Author SHA1 Message Date
DottaandClaude Opus 5.5 9a52a2dfeb fix(native): retry ACPX session.open startup timeouts and give cold Claude a 60 s bound (#19661)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native runs start an ACPX provider through Runner. `session.open`
opens the provider session before any prompt.
> - On a loaded host, a cold ACPX Claude `session.open` took longer than
its 30-second bound. The controller reported
`runner_local_connect_failed: PRP command session.open timed out`.
> - The server classified this as the generic
`native_session_interrupted` with `ambiguous_state`, so the task was
blocked at once. A person had to click Retry, sometimes several times.
In one restart-chaos storm, one task needed three Retry clicks, and on
another task both Retry runs timed out too.
> - The server already has a bounded same-run bootstrap retry for
failures that come before provider work (#18854). It did not recognize
this timeout. The 30-second bound also cut off normal slow startups.
> - This pull request gives cold ACPX Claude a 60-second `session.open`
bound, like Pi and OpenCode (#19487). It also retries a startup
`session.open` timeout automatically on the same run, from clean state.
> - The benefit is that a slow cold start on a busy host completes, or
retries itself, instead of waiting for a person.

## Linked Issues or Issue Description

Refs #18854 (bounded bootstrap retry for definite `session.open`
failures), #19487 (60-second cold OpenCode admission), #19665 (the
bootstrap proof that this change reuses).

**What happened?**

ACPX Claude runs failed with `runner_local_connect_failed: PRP command
session.open timed out` about 35 to 40 seconds after they started. The
coordinator recorded `native_session_interrupted` with `recoveryMode:
"ambiguous_state"` and `providerSessionEstablished: false`. The task was
blocked with a board-owned action. No provider event and no checkpoint
existed. The host 1-minute load was 17 to 44, sometimes with no restart
nearby.

**Expected behavior**

A normal slow cold start completes. A `session.open` that still times
out during runner startup, with no provider event, no checkpoint and no
turn, retries on the same run with backoff. At most two automatic
attempts follow the first. A run where provider work may have started is
never retried automatically.

**Steps to reproduce**

1. Run ACPX Claude tasks on a host with a 1-minute load of about 25 or
more.
2. Observe a run fail after about 35 to 40 seconds with `PRP command
session.open timed out`.
3. Observe the task blocked with `ambiguous_state` and a manual Retry.

**Paperclip version or commit**

`master` at `6c7cc2c5d6`.

## What Changed

The bound (evidence below):

- `coldAdmissionTimeoutMs` gives cold ACPX Claude 60 seconds (live
adoption keeps 30 seconds). The runner's Rust sidecar
`session_open_timeout` and the sidecar's provider handshake
(`acpxSessionHandshakeTimeoutMs`) give Claude and Pi 60 seconds too. All
three bounds must agree, or the shortest one fails first. Claude's warm
run attachment uses the same cold budget, like Pi.

The automatic retry:

- `nativeSessionFailureSourceCode` returns
`provider_session_open_timeout` only for exactly `<startup failure
code>: PRP command session.open timed out`. The startup failure codes
are `runner_local_connect_failed`, `runner_direct_wss_failed` and
`runner_ingress_unavailable`. The transport uses them only before
startup completes. After startup the code is
`provider_transport_failed`, which is unchanged.
- `nativeProviderRecoveryEvidence` treats the cause as pre-session.
Durable evidence still decides: a bootstrap retry needs no provider
event (`session.started`, `turn.started` and the others) and no
checkpoint. The existing budget applies: three attempts with backoff of
about 30 seconds, then about 60 seconds. When the budget is used, the
task is blocked with the existing "Provider startup failed on 3 attempts
before any provider work began" action.
- A timeout is indeterminate: the runner can open a provider session
after the controller stops waiting. Live runs showed both leftovers, so
the retry now starts from clean state:
- `prepareNativeSessionOpenTimeoutRetry` (server) runs when the failure
settles. It quarantines a local runner root only when the
restart-recovery proof (`runnerdStateProvesIncompleteBootstrap`, #19665)
shows that no turn began and every recorded process is gone. Any other
root, and any remote runner, stays `ambiguous_state` as before.
- `resetAbandonedBootstrapRecord` (runner ACPX adapter) runs on a fresh
open (no runner-recorded identity). If ACPX has a saved record for the
session key with no messages, no prompt and no events, it is marked
`reset_on_next_ensure`. Claude saves nothing before the first prompt, so
reusing such a record fails the first turn with
`session_resume_required`. A record that holds any conversation is
unchanged.
- A chat retry of an exhausted `provider_session_open_timeout` run is
accepted like the other pre-provider causes.
- `doc/execution-semantics.md` and the Runner README describe the cause
and the bound.

## Verification

**Measured startup distribution** (ACPX Claude
`runner.session.bootstrap` / `runner.session.startup` spans from
restart-chaos run events, 1-minute load 8 to 44):

| Source | Bound | Successful cold bootstraps (s) | Failures |
| --- | --- | --- | --- |
| Earlier storm evidence | 30 s | 2.9, 4.7, 7.4, 8.6, 8.7, 26.2, 27.4,
27.6, 29.2 | startups ended at 34.9 to 40.3 s (the 30 s `session.open`
bound plus spawn and `run.prepare`) |
| This branch, wave s3601 | 60 s | 6.3, 7.8, 8.1, 13.4, 14.4, 22.3,
22.8, 22.9, **37.3** | 0 `session.open` timeouts |
| This branch, wave s3602 | 60 s | 13 samples, p50 10.5, p90 17.9, max
25.7 | 0 `session.open` timeouts |
| This branch, wave s3610 (final head, load 28 to 42) | 60 s | 10.4,
11.3, 11.7, 12.0, 13.3, 14.2, 29.6, **30.4**, **30.9** | 0
`session.open` timeouts |

Under load, successes clustered just under 30 s and failures sat just
above it, so the 30 s bound was cutting the distribution off. With 60 s,
cold starts of 30.4, 30.9 and 37.3 s succeeded that the old bound would
have failed. 60 s is about 1.6 times the largest observed success, and
it matches Pi and OpenCode.

**Live restart-chaos waves** (local, ACPX Claude only, 4 in flight,
`--rc06-masked`, real provider, K1/K2 restarts):

- s3601 (8 tasks, load 19 to 32): 7/8 PASS. No `session.open` timeout.
The one failure is an R3 Retry signature after K1
(`native_restart_recovery_blocked`), not this cause.
- s3602 (8 tasks, load 17 to 39): 8/8 PASS. No `session.open` timeout.
Three Retry clicks were for `native_provider_terminal_failed`
(`provider_turn_lost_on_restore`), a different restart signature.
- Forced timeout, s3609 (4 tasks, B5, K2): a local, uncommitted fault
injection gave every other cold startup a 1.5 s `session.open` deadline.
Result 4/4 PASS. Run 4fe6537a timed out, retried itself on the same run,
and then succeeded through four K2 restarts with no person involved. Run
02799a85 hit the injection three times in a row; the budget stopped it
after attempt 3, and one Retry completed it.
- Before the two clean-state fixes, the same forced wave (s3607 and
s3608) showed the retry reusing leftover state: `turn.start` failed with
`session_resume_required`, and then "ACPX session is not open". Those
runs are the red evidence for the two clean-state fixes.
- Final head, s3610 (8 tasks, K1/K2, no injection, load 28 to 42): **8/8
PASS**, 0 `session.open` timeouts. The one Retry click (t06) was for
`provider_turn_lost_on_restore`, a different restart signature.

**Tests:**

- Red before the fix: `native-startup-retry.integration.test.ts -t
timeout` (embedded PostgreSQL, real executor, real resume claim). The 3
retry cases failed with `native_session_interrupted` /
`ambiguous_state`.
- Green: `native-startup-retry.integration.test.ts` passes 36/36. New
cases:
- for each startup failure code, a timeout retries on the same run with
backoff, keeps the task `in_progress` with an agent-owned
`resume_native_run` wake, and blocks after attempt 3 with a board-owned
action;
- no automatic retry when `session.started` or `turn.started` is
durable, when the code is `provider_transport_failed`, or when another
command timed out;
- a real runner root with an opened session and no turn is quarantined;
a root with a turn command stays `ambiguous_state` and intact; a remote
runner is never prepared.
- `codex-runtime-adapter.test.ts`: 113 pass. New cases: a 45 s delayed
handshake is admitted for Claude and rejected for Codex; a
never-prompted record is reset only on a fresh open; records with
messages, a prompt or events are kept.
- `abandoned-bootstrap-record.test.ts` (real `createAcpRuntime` with
`createRuntimeStore` and a controlled ACP peer that, like Claude, cannot
load a never-prompted session): without the reset, a fresh open reuses
the record and the first turn fails with SESSION_RESUME_REQUIRED after
`session/load`. With the reset, the provider receives `session/new` and
the turn completes.
- `vitest run src/drivers/acpx src/cli` in the runner: 107 files, 1,935
tests pass.
- `native-session-executor.test.ts -t "session.open|startup|bootstrap"`:
144 pass. `runnerd-codex-transport.test.ts -t "cold process admission"`:
4 pass. `cargo test -p paperclip-runner-core --lib admission_budget`
passes.
- `npx tsc --noEmit -p server` and the runner `tsc -p
tsconfig.test.json` pass.

## Risks

- **This changes Runner Rust code. The Daytona runner image must be
rebuilt after merge** for remote sandboxes to get the 60-second sidecar
bound. Until then, a remote Claude `session.open` still fails at 30
seconds in the sidecar (as a definite failure, which already retries). A
remote timeout stays operator-owned, because a remote root cannot be
proven clean here.
- A cold ACPX Claude start that hangs now fails after about 60 seconds
instead of 30 seconds.
- A timed-out `session.open` may have created an empty provider session.
No prompt was sent. The retry quarantines that runner root and asks ACPX
for a new session.
- Same-run retries of a fresh-attempt Retry run can still fail with
`isolated_task_directory_binding_mismatch`. This is RC-35 (workspace
re-preparation), which has its own fix in progress.
- No schema or API change. The new error code appears on runs and in run
error codes.

## Model Used

Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, through Claude Code with
tool use (shell, file edits, 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
- [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-06 19:37:11 -06:00
DottaandClaude Opus 5.5 20fc37778f fix(runner): keep settled OpenCode sessions across a controller proxy rebuild (#19641)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The native Runner keeps an OpenCode conversation in durable state,
so a task can continue after a control-plane restart.
> - Saved OpenCode state binds one digest of the launch profile: the
Node command, the proxy bundle and the OpenCode executable, each by path
and SHA-256.
> - The Node command and the proxy bundle are controller runtime. A
rebuild or deploy between a hard kill and recovery replaces their bytes.
> - The recovery probe then refused a settled conversation with
`provider_state_incompatible`, although the OpenCode release and the
conversation were unchanged.
> - ACPX already re-binds a settled session after a Runner upgrade
(#18879). OpenCode did not.
> - This pull request records the OpenCode release digest in saved state
and re-binds a settled session when that release is unchanged.
> - The benefit is that a task continues automatically after a restart
that also changed the controller build. A real release change still
fails closed.

## Linked Issues or Issue Description

Refs #19558, #19501, #18879.

**What happened?**

A restart-chaos wave killed the control plane with SIGKILL while an
OpenCode run was settled between turns. The first recovery attempt
restored the session. The controller's runner `dist` was then rebuilt in
the same checkout. The next recovery attempt failed in the
`runner.drain` resume probe with `OpenCode runner launch profile changed
across durable recovery` (`native_provider_state_incompatible`). The
saved state matched the original proxy bytes exactly. Only the proxy
bundle bytes had changed. The OpenCode executable, the paths and the
model were the same.

**Expected behavior**

A settled OpenCode session continues after a restart when only
controller runtime bytes changed. A changed OpenCode release, or a
session with work in flight, fails closed. The existing disposition then
offers one Retry that continues in a fresh provider session.

**Steps to reproduce**

1. Start an OpenCode run and let it settle between turns.
2. Kill the runner and the controller.
3. Replace the qualified proxy bundle with valid new bytes (rebuild or
deploy).
4. Recover the run. The resume probe fails with
`provider_state_incompatible`.

**Paperclip version or commit**

`master` at a08f9494ea.

## What Changed

- `CodexProviderState` records `opencodeReleaseDigest`: the SHA-256 of
the qualified OpenCode executable, which owns the saved conversation.
- Restore re-binds a saved OpenCode session to the current launch
profile only when all of these are true:
  - The saved release digest equals the current executable digest.
- The saved configuration names the current command and proxy paths (the
existing binding check).
- The session is settled: no active or ambiguous turn, startup attempt,
receipt-limit interrupt, pending tool call, undelivered or queued
provider event, or working goal. The lifecycle is `prepared`,
`session_open`, or `provider_exited` (an observed idle exit that restore
already restarts).
- Every other mismatch keeps the exact binding and fails closed with
`provider_state_incompatible`. State written before the new field has no
release digest, so it stays exact.
- Session start still verifies every launch artifact and the restored
provider identity.
- `docs/durable-recovery.md` documents the OpenCode re-bind rule next to
the ACPX rule.
- The #19558 regression that required refusal of a new proxy for a
prepared run now asserts the re-bind. The remote pack retention from
#19558 is unchanged.

## Verification

- Red first:
`settled_opencode_session_continues_after_a_controller_proxy_rebuild`
failed on unchanged production code with the production message, from
the same `runner.drain` probe. It passes after the change. It also
checks that the conversation is resumed (`thread/resume`), that a new
turn completes, and that the re-bound digest persists.
- Counter-tests that still refuse: changed OpenCode release on a settled
session (state bytes unchanged), proxy rebuild with an active turn
(state bytes unchanged), and legacy state without a release digest. The
existing executable-drift test still refuses.
`exited_opencode_session_continues_after_a_controller_proxy_rebuild`
covers a kill after an observed idle provider exit (red before the
follow-up commit, green after).
- `cargo test -p paperclip-runner-core --test native_provider_backend`:
45 passed (one unrelated Pi lifetime-fence test flaked once under load
35 and passed 3/3 alone). `cargo test -p paperclip-runner-core --lib
provider_backend`: 145 passed. `cargo fmt` and `cargo clippy` are clean
for the changed code.
- Live restart-chaos waves (local env, OpenCode only, concurrency 2,
`--rc06-masked`). A drift injector appended a valid comment to the local
OpenCode proxy bundle after every fault took the controller down, so
each recovery saw a changed proxy digest.
- Wave 1, seed 3201, 4 tasks, 4 × K1: 3 tasks finished automatically.
The fourth was killed with a turn in flight. Recovery failed closed with
`native_provider_state_incompatible`, as designed. One Retry worked on
the first click and completed the task (R3 pass). That task's grade is
`R5:silent_dead_run`: the Retry run was quiet for 180 s during the B5
benchmark. This is not a launch-profile failure.
- Wave 2, seed 3207, 3 tasks, K1 × 2 and K6: 3/3 pass. The K6 task also
had a turn in flight. It failed closed and one Retry completed it.
- In both waves every kill landed with a turn in flight or before a
durable session. The settled re-bind path was therefore not reached
live. The Rust executor regression above covers that path.
- Environment: macOS arm64, Node 26.4.0, local env, load average 17–39.

## Risks

- A settled OpenCode session now runs on the current proxy bundle after
a controller upgrade. The proxy keeps no state of its own; OpenCode owns
the conversation store. ACPX already accepts the same trade-off for its
sidecar.
- Sessions with work in flight are still refused. This is conservative
and matches ACPX.
- Runs saved by an older Runner have no release digest, so they keep the
old exact behavior once.
- Cursor Cloud keeps its exact binding. It is out of scope.

## Model Used

Anthropic Claude, model ID `claude-opus-5-5`, used through Claude Code
with tool use (shell, file edits) 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-06 19:18:29 -06:00
DottaandClaude Opus 5.5 e05bced39f fix(runner): adopt an accepted Codex turn whose start outlives the bound (#19608)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native Codex runs submit each turn through runnerd. The transport
answers `turn/start` only after it sees the provider's `turn/started`
for the turn the durable command result named
> - That wait has a fixed 30 s bound
> - At host load around 50, after two hard restarts, Codex accepted a
turn and started it about 27 s after the command, beyond the bound once
scheduling delay was added. The transport failed the start, but the
provider kept running the turn and made a tool call
> - No controller owned that turn. Its close could not settle, and the
task's Retry was refused at once
> - This pull request adopts the accepted turn when the bound expires,
but only when the runner still reports that turn active
> - The benefit is that a slow but live provider turn stays owned, so
its events, Stop and close keep working

## Linked Issues or Issue Description

**What happened?**
In a restart chaos replay (two SIGKILLs about 20 s apart, local runner,
real Codex, load 50), a resumed run failed with `runnerd did not report
the provider turn identity`. The runner journal shows the durable
`turn.start` result with the provider turn id, then `turn.started` for
that same turn 27 s later, followed by items and a semantic tool call.
The run's close then stayed incomplete (`semanticResultsSettled:
false`), and a Retry of the task failed with
`native_session_cleanup_quarantined: prior session cleanup remains
incomplete`.

**Steps to reproduce**
1. Use a Codex provider that answers `turn/start` and emits
`turn/started` later than the transport's turn-start bound.
2. Start a turn through the runnerd transport.

**Expected behavior**
The accepted turn is owned and observed by the controller.

**Actual behavior**
The start fails after the bound, while the provider still runs the turn.

## What Changed

- `runnerd-codex-transport.ts` (`#startTurnWork`):
- Near the end of the bound, if `turn/started` has not arrived, the
transport reads an authenticated `session.snapshot`. It reserves at most
5 s (or a quarter of the bound) for this read.
  - It binds the accepted turn only when the turn is provably live:
    - the runner reports that exact provider turn as active;
- the runner attributed no provider launch
(`provider_startup_ownership`) to a command after this `turn.start`;
- for a local provider, the same process (pid and start time) that
accepted the turn is still alive.
- Otherwise it keeps waiting for `turn/started` until the configured
bound and fails exactly as before. A turn lost across a provider
relaunch keeps the same-run resume path.
- `fake-codex-app-server.rs`: new test options `--turn-started-delay-ms`
(a delay between the `turn/start` response and `turn/started`) and
`--exit-before-turn-started`.

## Verification

- Red before the fix: `pnpm --filter @paperclipai/paperclip-runner exec
vitest run src/live/runnerd-codex-transport-deadlines.test.ts -t "adopts
a durably accepted"` (real runnerd and fake Codex, 12 s start delay, 8 s
bound) failed with `runnerd did not report the provider turn identity`.
This is the replay's error.
- Green after the fix: the start resolves with `provider-turn-1`, and
`turn/started` and the turn's terminal follow in order. There is one
provider `turn/start`.
- Negative case (`does not adopt an accepted turn the runner no longer
reports active`): the provider accepts the turn and exits before
starting it. The runner relaunches the provider and still lists the
turn, but the turn is not adopted. The start fails within the bound plus
scheduling slack. A snapshot-only version of this change adopted that
dead turn, and it hung for 20 s with no terminal.
- `src/native-session-runtime.test.ts`,
`src/live/runnerd-restart-continuation.test.ts` and
`src/live/runnerd-codex-transport-deadlines.test.ts` pass. The package
typecheck passes.
- Live chaos replays: results are added in a comment.

## Risks

- A provably live provider that accepts a turn and never starts it now
waits on the turn's own timeout and Stop paths instead of failing after
30 s. A dead or relaunched provider still fails at the bound.
- Remote runners can adopt only on the runner's own evidence (active
turn and no later provider launch), because their provider pid is not a
local process.
- A stale `turn/started` for a previous turn that arrives after the new
command result is still rejected. The transport cannot attribute that
turn's following events, so rejecting it fails only the transport, and
the same-run resume continues the task (seen in the replay). Delivering
it would attach the old turn's items and usage to the new turn.

## 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

Refs #19532, #19539.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 19:06:45 -06:00
DottaandClaude Opus 5.5 785ead5c7a fix(tasks): hold a Retry while the task's own native session close is retained (#19609)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - When a native provider session close fails, the runner keeps a
cleanup owner or quarantine in process memory. Its admission gate
refuses new sessions for that task (scoped to the task since #19532)
> - The server could not see that state. A task's Retry was accepted
(202), its run was dispatched, and the runner refused it at once with
`native_session_cleanup_quarantined: prior session cleanup remains
incomplete`
> - The user saw a Retry that failed instantly, and nothing said what to
do
> - This pull request lets the server read the runner's per-task cleanup
gate and hold the Retry with a clear reason. The Retry starts
automatically when the close is released
> - The benefit is that a Retry is never dispatched only to fail on a
block that is already known

## Linked Issues or Issue Description

**What happened?**
After two hard restarts, a Codex run's close stayed incomplete. The
board Retry for that task was accepted, and its run failed in about 1 s
with `native_session_cleanup_quarantined: prior session cleanup remains
incomplete`. The next Retry did the same.

**Steps to reproduce**
1. Let a native session close fail or stay pending for a task.
2. Press Retry on that task.

**Expected behavior**
The Retry waits with a clear reason, and starts when the cleanup
finishes. If the cleanup needs an operator, Retry is not offered and the
one working action is shown.

**Actual behavior**
The Retry is accepted and its run fails at once.

## What Changed

- `native-session-runtime.ts`:
- New `nativeSessionCleanupBlock({ companyId, issueId })`. It is a
read-only view of the in-process admission gate for one task: `pending`
(a retained close or a bounded automatic close recovery still owns the
task's provider resources), `operator_required`, or `null`. It grants no
authority.
- New `onNativeSessionCleanupSettled(listener)`. It is called whenever a
cleanup owner or quarantine entry is released.
- `explicit-native-continuation.ts` (the continuation gate for a Retry
or a new message):
- `pending`: the input is held with `local_cleanup` ("Retry is waiting
for the previous execution to finish cleanup. Your message is saved.").
It starts automatically when the close is released.
- `operator_required`: handled exactly like a recorded
`native_session_cleanup_quarantined` run. Retry is not offered. A new
message runs the existing stopped-process proof
(`verifyStoppedNativeSessionForContinuation`), which retires the runner
quarantine when the old processes are proven stopped.
- `native-cleanup-release-wake.ts` (registered in
`server/src/index.ts`): each cleanup release wakes deferred execution
for the company in a transaction, so a held Retry re-evaluates.
- `server/src/vendor/paperclip-runner/index.ts`: exports the two runtime
functions.

## Verification

- Red before the gate (server change stashed): `pnpm --filter
@paperclipai/server exec vitest run
src/services/quarantined-task-retry.test.ts -t "holds a Retry"`
dispatched a successor run while the task's close was retained
(`expected [ { … } ] to have a length of +0 but got 1`).
- Green after the gate:
- The Retry is held (`deferred_issue_execution`, `executionWait.reason =
local_cleanup`), and no successor is dispatched.
  - A different task is not blocked.
- When the close settles, the release is announced, and the next pass
dispatches exactly one successor.
- The whole `quarantined-task-retry.test.ts` file passes (28 tests).
`src/native-session-runtime.test.ts` passes. Server `tsc --noEmit`
passes after the runner package build.
- Live chaos replays: results are added in a comment.

## Risks

- The gate reads process memory, so it applies only where the server and
the runner session share a process (local runner). Other deployments
keep today's behavior.
- A held Retry re-evaluates on the release wake (sent in its own
transaction) or on the existing 30 s deferred-execution pass. When the
bounded automatic close budget is spent with nothing else scheduled, the
view reports `operator_required`. Retry is then no longer offered, and a
new message reaches the stopped-process recovery instead of waiting
forever.
- No schema or API shape changes.

## 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

Refs #19532.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 18:52:46 -06:00
Nicky LeachandClaude Opus 5.5 fb3cf1a072 feat(ci): retry a failed Vitest case once in PR CI and report each retried pass (#19637)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Contributors wait for PR CI before merge, so CI wall-clock time sets
the pace of work
> - PR CI runs Vitest shards with 4 workers on 4-vCPU runners, so
timing-sensitive tests sometimes time out under load
> - One such timeout fails the whole run, and a re-run costs another 15
minutes or more
> - In one 7-hour window on 2026-11-06, about 30 failed jobs were test
flakes on commits that passed on a later attempt
> - This pull request retries a failed Vitest case once in PR CI only,
and reports every pass that needed the retry
> - The benefit is fewer whole-run re-runs, while each flaky test stays
visible so that we can fix it

## Linked Issues or Issue Description

**What existing behavior does this improve?**

PR CI fails the whole run when one Vitest case fails once because of
runner load.

**Subsystem affected**

CI: the PR Vitest lanes (general, serialized, workspace and Runner
package lanes).

**Current behavior**

Vitest uses `retry: 0` everywhere. One timeout under load fails the run.
A person must notice it and re-run the failed jobs. About 20% of
recently merged PRs needed at least one re-run.

**Proposed behavior**

PR CI retries a failed Vitest case one time. A test that passes only on
the retry gets a GitHub warning annotation and a row in the job summary.
Local runs, release, cloud readiness and master keep `retry: 0`.

**Reason and benefit**

A re-run of one test case costs seconds. A re-run of the CI run costs 15
minutes or more. The report keeps flaky tests visible. Separate pull
requests fix the known flaky tests.

**Breaking changes**

None.

## What Changed

- Add `scripts/ci/vitest-retry.mjs`. It sets 1 retry when
`GITHUB_ACTIONS=true` and `GITHUB_WORKFLOW=PR`, and 0 in other contexts.
`PAPERCLIP_VITEST_RETRY` overrides the value.
- Add `scripts/ci/retry-reporter.mjs`. It writes a `::warning`
annotation and a step-summary table row for each test that passed only
on a retry.
- `scripts/run-vitest-stable.mjs` passes `--retry` to `vitest run`. The
`vitest list` collection step does not retry.
- The root `vitest.config.ts` and
`packages/paperclip-runner/vitest.config.ts` add the retry reporter
beside the existing reporters. The Runner config sets `retry` from the
same gate.
- `.github/workflows/pr-trusted.yml` reads the `CI_VITEST_RETRY`
repository variable. Set it to `0` to turn retries off without a code
change.
- Add `scripts/ci/retry-reporter.test.mjs` to `test:release-registry`,
and document the behavior in `doc/DEVELOPING.md`.

## Verification

- `node --test scripts/ci/retry-reporter.test.mjs`: 11/11 pass. The
tests cover the gate matrix, the reporter output, a real Vitest run with
a test that fails once, the wrapper arguments, and the Runner config.
- `node --test scripts/__tests__/run-vitest-stable-shard.test.mjs`:
39/39 pass.
- `node --test .github/scripts/tests/*.test.mjs`: 744/744 pass.
- Manual check: a temporary test that fails once ran through
`run-vitest-stable.mjs` and through the Runner package config. With the
PR environment it passed and wrote the annotation and the summary row.
With `GITHUB_WORKFLOW=Release` it failed. The temporary test is removed.

## Risks

- A retry can hide a flaky test. The annotation and the summary row
reduce this risk.
- A test that fails for a real reason now runs twice in PR CI, so a
failed run takes slightly longer.
- `pr.yml` loads `pr-trusted.yml` from master, so the `CI_VITEST_RETRY`
switch works only after merge. The retry itself is in the scripts and
starts with this pull request.
- Low risk otherwise: release and master verification are not changed.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with tool use and
code execution. A Claude sub-agent wrote the change, and the parent
session reviewed it.

## 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-06 16:43:02 -08:00
DottaandPaperclip a0ac81e114 fix(ui): show task image results in compact thumbnail grids (#19626)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Task feeds show agent messages, documents, and files in time order.
> - Each image currently uses a full feed card.
> - A turn with seven screenshots makes the conversation hard to scan.
> - This pull request groups adjacent images from the same run in a
compact grid.
> - Users can scan the results and open each image in the existing
gallery.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

Image results in task history and the live task feed.

**Current behavior**

Each image uses a separate full-width card. Native image upload receipt
messages can also separate related screenshots.

**Proposed behavior**

Adjacent images from one run share a wrapping thumbnail grid. New image
results join the grid. Documents, video, and authored messages keep
their positions. The grid opens the existing gallery.

**Reason and benefit**

Users can see a batch of screenshots without scrolling past one large
card for each file.

**Breaking changes**

The feed layout changes. The stored comments, files, APIs, and
permissions do not change.

Related public work: Refs #17228. Refs #8785. Refs #7667. These changes
supplied the artifact cards and media gallery that this pull request
reuses.

## What Changed

- Add one image grouping function for settled history, turn history, and
live results. Group only images with the same known run. Keep the first
row key and each image key when results arrive.
- Add a token-based grid that reuses the media artifact cards and image
gallery.
- Add an optional typed attachment-preparation marker to file
registration and chat-file reuse. Preserve the marker through comment
normalization. Use it with matching image, author, and run links to hide
generated receipts. Keep the exact historical sentence as a fallback for
old receipts.
- Add nine production component stories with seven sample-account
screenshots. Cover mobile, Classic, mixed results, single images,
missing images, streaming, gallery navigation, and the complete feed
projection.
- Add grouping and production-feed regressions. Add 18 browser cases
across light and dark themes.
- Improve contrast for the shared fallback label and gallery counter
after the accessibility checks found low contrast.
- Match alternate original-image paths when opening the gallery. Keep a
separate Open original link beside Download in the gallery.
- Record the feed layout in DESIGN.md.

## Verification

- Final qualification at `09b12381ce85bee6593050bd8a2fa18cbe2c45a6`:
[required CI
passed](https://github.com/paperclipai/paperclip/actions/runs/42317080212/job/128237094513),
including typecheck, build, all test lanes, E2E, and verify. The
snapshot has 58 successful check runs, three conditional skips, one
neutral advisory job, and a successful legacy Snyk status. No pending or
failed checks remain. Greptile and Paperclip Review both score this
exact head 5/5; all review threads are resolved.

- Current-head local checks: 544 focused UI tests passed across 11
files, with one existing skip. UI/shared typechecks and token gates
pass. All 18 focused Storybook browser cases passed after the CSS merge,
with retries disabled.
- Two database integration cases verify file registration and chat-file
reuse receipt markers. All 52 issue-validator tests and six upstream
default-model tests passed.
- Regressions failed before the fixes for unknown-run grouping, receipt
wording, and authored progress text with an image. The repair uses a
typed receipt marker and requires matching author, attachment, image,
and run links before hiding a generated receipt. All five review
findings are resolved.
- Browser checks cover loaded images, wrapping columns, overflow,
missing-image fallback, streaming stability, keyboard gallery
navigation, original-file links, and downloads. Fixtures use production
renderers. These checks do not prove deployed persistence or provider
behavior and do not compare visual baselines.
- Local limits: the preflight helper could not finish because Cargo was
absent. The earlier low-memory server typecheck and dependency-free
selector did not replace final-head CI. The now-passing final CI
supplies broad typecheck, build, server, Runner, and E2E coverage.
- Prior independent CI failures remain recorded: one Slack timer case
passed a single diagnosed rerun; its exact timer cause was not proved. A
later chat shard lost its self-hosted runner without a test assertion
failure. The current revision passes that shard and all applicable
gates. Master conflict resolution retained both image-grid and patch CSS
rules.

## Risks

- Images without a run ID remain separate. Messages and other resource
types end each batch.
- Receipt suppression requires a matching preparation marker or the
exact legacy sentence, plus matching attachment, image work product,
run, and author. Tests preserve human messages and native progress text
with attachments. The optional marker uses existing JSON metadata and
needs no database migration.
- Shared gallery and fallback text now have stronger contrast.

## Model Used

OpenAI GPT-6, through Codex. The agent used reasoning, repository tools,
code execution, and browser tests. The runtime did not expose a more
specific model ID or context window size.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 18:42:22 -06:00
DottaandClaude Opus 5.5 7c673e695f fix(chat): name tasks by identifier, not UUID, in tool rows and agent text (#19635)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - New users meet Paperclip through the onboarding chat with their
first agent, and through task briefs that agents write
> - A browser walk of onboarding found internal handles and
control-plane jargon in those user-facing surfaces
> - A tool row showed `Read taskId: e3ecf9bc-…`, an agent-written brief
cited `task … (id 2b330993-…)`, and a Codex agent told a new user about
"receipts", "execution policy", and a "rejected policy patch"
> - Raw UUIDs and mechanism narration make the product look broken and
give the user nothing to act on
> - This pull request makes tool rows name a task by its identifier, and
tells agents to reference identifiers and to describe outcomes, not
mechanics
> - The benefit is that a new user sees "Read <task identifier>" and
plain progress updates

## Linked Issues or Issue Description

**What happened?**
In the task chat, a collapsed tool row rendered the raw call argument
`taskId: <uuid>`. An agent-written task description included a raw task
UUID next to the task identifier. A Codex chief of staff narrated
internal mechanics (receipts, execution policies, a rejected policy
patch) to a new user in chat.

**Expected behavior**
Tool rows name the task by its identifier. Agent-written text links
tasks and documents by identifier. Agents tell the user what happened,
what it means, and the next step.

**Steps to reproduce**
1. Run onboarding with the Codex API runner and accept a single-task
proposal.
2. Watch the handoff in the chief-of-staff chat.
3. See the task lookup tool row and the narration. With the Claude API
runner and the plan path, open a child task created from the plan and
read its description.

## What Changed

- `ui/src/components/task-chat/transcript-adapter.ts`: a tool row's
target never contains a UUID argument. A `taskId`/`issueId` UUID is kept
as `taskRefId` on the tool item.
- New `ui/src/components/task-chat/task-reference.ts`:
`useTaskReferenceLabel` reads the task from the shared issue detail
query cache and returns its identifier. It returns "task" until the task
resolves or when it cannot.
- `TaskChatRunnerActivityGroup` and `TaskChatToolCard` show that
identifier as the row subject. They load the task only for rows that
have a task reference.
- `skills/paperclip/SKILL.md` (Comment Style): two new required rules.
"No raw IDs in prose" and "Describe outcomes, not mechanics".
- First-task onboarding skill: a "Talk in outcomes" rule. Non-inline
handoff inputs use an identifier link, not a raw UUID.
- Chief-of-staff persona (`AGENTS.md` for the first agent): the same
plain-language rule, so it applies in every chat.
- `skills/paperclip-converting-plans-to-tasks/SKILL.md`: task
descriptions reference other tasks and documents by identifier link,
never by raw UUID.
- No change to `skills-releases/` snapshots. They are frozen releases.

## Verification

Red on master (fix files reverted, new tests kept):

```
ui  tool-row-task-reference.test.tsx
 FAIL  ... > falls back to a plain word while the task cannot be resolved
AssertionError: expected 'ReadtaskId: e3ecf9bc-099a-4b10-8169-9…' not to contain 'e3ecf9bc-099a-4b10-8169-9ce490a91e3d'
      Tests  3 failed (3)
server  user-facing-wording-guidance.test.ts
      Tests  6 failed (6)
```

Green with the fix:

```
ui  tool-row-task-reference.test.tsx        Tests  3 passed (3)
server  user-facing-wording-guidance.test.ts Tests  6 passed (6)
```

Neighbouring suites:

- `cd ui && npx vitest run src/components/task-chat
src/components/TaskChatThread src/components/transcript`: 70 files, 1448
passed, 1 skipped.
- From the repo root: `onboarding-first-task-assets`,
`paperclip-skill-utils`, `onboarding-first-task-admission`,
`hiring-operational-examples`, `cli-invocation-safety`, and the new
guidance test all pass.
- `pnpm check:token-gates`: all gates clean. The change adds no styles.
- `ui` typecheck: no errors in the changed files.
- Not run locally: `agent-skills-routes.test.ts` hit a 30 s `beforeAll`
import timeout on a cold cache. CI covers it.

## Risks

- Low risk. A row that names a task loads that task once through the
shared issue detail cache. The thread usually has it cached already. A
failed load shows "task" and does not retry.
- Rows now show other non-UUID arguments after the identifier (for
example `<identifier> · status: done`). Rows without a task reference do
not change.
- The skill text changes the guidance that agents read. They do not
change permissions, tools, or API behavior.

> 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, run in Claude Code
with tool use (shell, file edits, 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
- [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-06 18:12:48 -06:00
DottaandPaperclip 19c2bebc9c fix(runner): settle durable provider exits on open streams (#19657)
## Thinking Path

> - Paperclip manages AI agents and their work.
> - Runner keeps the native session and its event stream until a turn
ends.
> - A provider can exit with a terminal error while its event stream
stays open.
> - Waiting for that stream to close leaves the run and its resource
owner active.
> - The existing durable failure receipt already identifies the failed
current turn, including an authoritative replay.
> - This pull request uses that event to enter the existing failure and
cleanup path.
> - Recovery and later idle retirement can then proceed from a real
terminal result.

## Linked Issues or Issue Description

Refs #18988 for native resource retirement. This is a separate
terminal-event defect found during failed-turn recovery qualification. A
search of open and closed PRs found no duplicate `provider_exited` fix.
The related merged #12596 changes Rust test polling; it does not change
this TypeScript session owner.

**What happened?**

`executeNativeSession` records a nonrecoverable `provider_exited` event,
but can continue to wait on an open event iterator. A proposed result
does not authorize success, and stream EOF may never arrive. The run
cannot reach its existing failure and resource cleanup path.

**Expected behavior**

Settle the committed or authoritative duplicate terminal failure for
this exact run, session and provider. Close through the existing owner.
Preserve recoverable goals, completed-turn and identity guards. A
duplicate must not add another committed event count.

**Steps to reproduce**

1. Supply a native session whose iterator emits a current,
nonrecoverable, unexpected `provider_exited` failure.
2. Keep that iterator open until the session owner closes it.
3. Observe that the old implementation does not settle within the
existing test bound.
4. Apply the guard below. The same execution fails with the typed
terminal error and calls the existing close owner once.

**Paperclip version or commit**

The original regression was reproduced against master source
`05a19768ae4d337a4ae88ecdbc259f251406487c`. Current candidate
`f4bb8f1b68e2793062dd640ee56b87a9208b4fd9` normally merges fetched
master `07a548073ab48f13cb93b2fdc31553a5da88a070`. All unrelated master
paths are preserved exactly. The two-file PR diff contains only the
guarded settlement and its tests.

## What Changed

- Add a guard after the existing durable event receipt. It requires a
committed or duplicate receipt, a nonrecoverable unexpected exit, no
completed current or previous turn, and exact provider/run/session
identity.
- Throw the existing typed provider failure so the existing failure and
close owners run. Add no scheduler, polling loop, provider parser or
cleanup owner. Event counting remains committed-only.
- Cover open streams with and without a result proposal, authoritative
duplicate replay, all six safety predicates, and a recoverable goal that
later completes. Preserve accepted-result and close-once assertions.

## Verification

- The original open-stream regression fails before the production
change. Review then found that the duplicate test released EOF before
asserting settlement. The corrected duplicate control fails on the prior
production code at its original timely-settlement assertion; it passes
after accepting the authoritative duplicate. All 21 selected exit,
safety, goal and compaction controls pass.
- The normal durable transport validates authenticated current
run/session/turn/item identity and canonical replay equality before
delivery. The App port validates current authority and binding; its
durable event transaction returns duplicate only for matching source
identity and canonical payload. No absent or conflicting event
authorizes settlement. The six negative consumer tests use a
deliberately permissive mock port; they prove this consumer guard, not
that production accepts foreign events.
- The first remote Typecheck job found five errors in new mocks that
accessed PRP fields without narrowing a native-or-PRP event. Both mocks
now explicitly reject the wrong event shape. Complete original CI
evidence is retained. No cast, suppression or narrowed test compiler
program is used.
- On current integrated candidate `f4bb8f1`, normal Runner emission and
the full normal `tsconfig.test.json` source/test program pass. All 26
selected cases pass: the same 21 controls plus all five structured
provider-failure/model-retry rows affected by the incoming master
change. The other 197 cases were filtered, not counted as passing.
- Before master integration, all 411 declaration paths and bytes were
identical across the settlement change. Incoming master independently
adds one OpenCode declaration and changes three OpenCode declarations;
those deltas are recorded separately. This fix changes no runtime
declaration. No Rust source or binary is changed by this PR.
- Actual guarded workers recorded zero PG sockets, active listeners or
blocked attempts. All known worker processes/groups closed. The working
tree is clean after qualification. No install, Rust build, App run,
provider connection or PostgreSQL suite was part of the final freshness
loop.
- Fresh exact-head remote CI and complete bot reviews remain required. A
local whole-repository test/build, successful task retry or
live-provider acceptance is not claimed.

Run the selected controls from a normal dependency installation:

```sh
pnpm --filter @paperclipai/paperclip-runner exec vitest run src/native-session-runtime.test.ts --testNamePattern='settles committed or duplicate provider exits while their stream stays open|retains EOF settlement when provider exit fails the|preserves a recoverable provider-exit goal until its later authoritative completion|preserves a durable session failure when its stream closes|preserves session-goal failure priority|context compaction|never continues task work when a restart loses a compaction turn|preserves structured provider failure and model retry classification' --maxWorkers=1 --no-file-parallelism
```

## Risks

- A false terminal classification could stop useful work. The guard
requires the existing durable receipt and explicit current-turn failure
fields; recoverable and completed-turn controls remain covered. An exact
durable duplicate may settle the same failure without repeating
accounting.
- Close can still fail. This change reuses the existing close owner and
does not manufacture resource retirement or clear retained failure debt.
- This proves Runner failure settlement under a controlled session
boundary. It does not establish successful task retry, whole-tenant
quiet, live provider closure, or deployed suspension.

## Model Used

OpenAI `gpt-6.1-sol` produced the delegated fix and qualification.
OpenAI Codex, based on GPT-6, performed managing technical review and
integration. Both used repository tools and code execution. The
coordinator's exact backend snapshot and context-window size were not
exposed.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (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 issue template
- [x] I have not referenced internal or instance-local Paperclip issues
or links
- [x] My branch name describes the change and contains no internal
Paperclip ticket id or instance-derived details
- [x] I have run the selected tests locally and they pass
- [x] I have added tests for the changed behavior
- [x] I have reviewed documentation; no command or public contract
changes require a documentation edit
- [x] I have considered and documented the risks above
- [ ] All Paperclip CI gates are green — fresh exact-head checks pending
- [ ] Greptile is 5/5 with no open findings — fresh exact-head review
pending
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 18:10:33 -06:00
DottaandClaude Opus 5.5 e33aa8a46a fix(runner): let saved Codex threads follow the current feature policy (#19659)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The Paperclip runner keeps a saved Codex thread so the next run of a
task continues the same conversation
> - On each new run, the controller sends `run.attach` with the current
Codex launch arguments, and the runner refuses any change to "protected"
arguments
> - #19516 removed `--disable image_generation` from the Codex launch
arguments, but every thread saved before that deploy still records the
flag
> - The runner treated the feature toggle as protected, so every
pre-deploy Codex thread failed with `run.attach cannot change protected
launch arguments`
> - Same-run recovery then tried to replace the thread, the replacement
failed, and the tasks became blocked for an operator
> - This pull request treats Codex feature toggles as controller launch
policy, so a saved thread adopts the current toggles and restarts on the
same thread
> - The benefit is that a release that turns a Codex feature on or off
no longer strands every saved Codex thread

## Linked Issues or Issue Description

Refs #19516

**What happened?**
After a staging deploy that included #19516, Codex agent runs on tasks
with a saved thread failed at the start. The run log showed
`native_session_recovery_failed: PRP command run.attach failed:
run.attach cannot change protected launch arguments` (error code
`native_session_interrupted`). Same-run retries then failed with
`runner_continuity_break_archive_failed` and
`runner_remote_state_unavailable`, and the tasks were blocked with
"Workspace repair required". Four tasks on one Codex agent showed this
first failure. Each one last ran successfully before the deploy.

**Steps to reproduce**
1. Run a Codex task so that its thread is saved with the launch
arguments from before #19516 (`--disable image_generation`).
2. Deploy a revision that includes #19516.
3. Wake the same task again. `run.attach` sends the current arguments
without the toggle, and the runner rejects them.

**Expected behavior**
The saved thread continues under the current launch policy.

**Root cause**
`CodexCommandExecutor::stable_launch_args` drops only the per-run
filesystem and shell-environment entries before it compares saved and
current arguments. All other arguments, including Codex
`--enable`/`--disable` feature toggles, must be identical. The
controller computes the toggles from Paperclip policy for every launch,
so any change to that policy made every saved thread impossible to
attach.

## What Changed

- `provider_backend.rs`: `stable_launch_args` also skips `--enable
<feature>` and `--disable <feature>` pairs and the
`--enable=<feature>`/`--disable=<feature>` form. The function contract
comment now lists the rebindable arguments. `run.attach` then adopts the
controller's current toggles. Because the arguments changed, the
existing path stops the provider and restarts it on the same thread.
- New test
`saved_thread_adopts_the_controllers_current_codex_feature_policy`:
- A thread saved with `--disable image_generation` attaches with the
current arguments, keeps its thread ID, and records the new arguments.
  - A new `--disable` toggle (a tighter policy) is also adopted.
- A new `--enable` toggle is adopted in both flag forms, and the thread
ID stays the same.
- A sandbox bypass flag, a different `default_permissions` profile, and
a different subcommand are still rejected with "protected launch
arguments", and the saved state does not change.

## Verification

- Red on `master` (8621bcf53e) with only the new test added:
  ```
test
provider_backend::tests::saved_thread_adopts_the_controllers_current_codex_feature_policy
... FAILED
a saved thread must follow the current feature policy:
DurableRunnerError("run.attach cannot change protected launch
arguments", None, None)
  ```
- Integration regression
`tests/codex_provider.rs::saved_codex_thread_attaches_after_a_release_changes_its_feature_toggles`
drives the real durable executor with the fake Codex app-server. It
saves a thread with `--disable image_generation`, attaches with the
current arguments, runs a second turn, and checks that the provider
restarted once. A sandbox-bypass change is still rejected. Red on
unfixed source:
  ```
the saved thread adopts the current feature toggles:
DurableRunnerError("run.attach cannot change protected launch
arguments", None, None)
  ```
Green with the fix. The full `codex_provider` binary passes (116
passed).
- Green after the fix (unit test):
  ```
test
provider_backend::tests::saved_thread_adopts_the_controllers_current_codex_feature_policy
... ok
  ```
- `cargo test -p paperclip-runner-core --lib`: 560 passed, 0 failed.
- `cargo test -p paperclip-runner-core --no-fail-fast`: all Codex,
native-provider-backend, and durable-recovery integration binaries pass.
Some tests in `acpx_park_readiness` and `stdio_mcp` failed. Different
tests in these same binaries also fail on unmodified `master` on this
machine. They are timing tests under load and do not use this code.
- Not run: a live staging attach with a pre-#19516 thread. The runner
binary in a sandbox must match the controller's binary digest, so this
fix reaches sandboxes with the server deploy.

## Risks

- A saved thread now accepts any `--enable`/`--disable` toggle that the
controller sends. The controller is the only source of `run.attach`, and
a new thread gets the same toggles. A tightened policy now also applies
to saved threads, where before the saved thread was stranded.
- The sandbox, permission profile, approval, model, and subcommand
checks do not change.
- When the toggles change, the warm provider process restarts on the
same thread once. This is the existing path for changed runtime
arguments.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code with tool use (shell, file edit, browser reads of staging).

## 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-06 18:07:26 -06:00
DottaandClaude Opus 5.5 f4b19e4231 fix(runner): finalize a durable proposed result when restore loses its Codex turn (#19658)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native Codex runs survive control-plane restarts: runnerd restores
the provider conversation, and the control plane continues the task from
its checkpoint
> - A hard kill can land after the agent proposed its result
(`run.result.proposed`) but before the provider turn ended
> - The restored runner then proves that the turn ended with the
accepted result. It records `session.reconciled` and `run.terminal`, but
no provider turn terminal
> - The Codex recovery read did not see that turn as ended, so recovery
failed with "persisted active turn … is missing from thread/read".
Retries ran out, and the Retry re-ran finished work
> - This pull request lets recovery settle that turn with its durable
result
> - The benefit is that a task that already finished its work continues
to finalization after a restart, instead of failing and redoing the work

## Linked Issues or Issue Description

Refs #19525, #19591, #19539, #19501

**What happened?**
In a restart storm, a native Codex task (B1) proposed a complete
`paperclip.run_result.v1` with passing tests. The control plane accepted
the completion tool call. About 9 s later, a K1 (SIGKILL and restart of
the control plane) killed the controller, runnerd, and the Codex
app-server. After startup, `resume_dead_runner` resumed the same run two
times. Each resume spent about 25 s in `runner.session.startup` and
failed with `native_session_recovery_failed: HarnessReconciliationError:
persisted active turn <id> is missing from thread/read`. Then
`native_session_retry_exhausted`, replay was blocked
(`preserve_without_replay_v1`), and the run ended
`native_session_interrupted`. The task needed a Retry, which re-ran the
finished work.

**Steps to reproduce**
1. Start a native Codex task through runnerd.
2. Let the agent call `paperclip_finish`, and let Paperclip accept the
result.
3. Kill the controller, runnerd, and Codex before the provider turn
ends.
4. Let restart recovery resume the run.

Durable evidence from the kept instance:
- The run events have `run.result.proposed` for the turn, then a
`completion.tool_accepted` span, then the kill.
- The saved checkpoint has `activeTurnId` set to that turn and
`semanticResult: null`.
- The Codex rollout on disk does contain the turn. The provider did
persist it.
- The runnerd journal for the first resume has `session.reconciled
{previousProviderTurnId: <turn>, activeProviderTurnId: null}` and then
`run.terminal {turnTerminalState: completed, runTerminalState:
succeeded, reportedWorkDisposition: done}`. It has no `turn.*` event for
that turn. The runnerd Codex provider state shows
`completedTurnAuthoritative: true` and `completedProviderTurnId:
<turn>`.

**Expected behavior**
Recovery finalizes the accepted result. It does not re-run the task.

**Actual behavior**
The runnerd Codex transport builds the recovery `thread/read` reply from
runnerd's journal. It looked only for
`turn.completed|failed|interrupted|cancelled` events that name the turn.
Runnerd's "turn ended with an accepted result" branch
(`provider_backend.rs`, `recovered_turn_ended_with_result`) emits only
the reconciliation and the run terminal. So the reply omitted the turn,
and `CodexHarnessSession.reconcile()` threw at
`codex-harness-session.ts:796`. The second resume read the same journal
and failed the same way.

## What Changed

- `runnerd-codex-transport.ts`: a new `runnerdRecoveredTurnTerminal`
helper selects the terminal for a restored turn. A provider `turn.*`
terminal that names the turn wins, as before, with its cause. A
`run.terminal` that follows a `session.reconciled` that names the turn
as the ended previous turn (no active turn, no later `turn.started`) now
also settles that turn, with the run terminal's `turnTerminalState`. The
`thread/read` wait loop uses this helper.
- `codex-harness-session.ts`: `reconcile()` now settles a checkpointed
active turn that provider history no longer contains when the session
holds the semantic result that this exact turn proposed (`resultTurnId`
equals the turn). Recovery restores that result from the durable
`run.result.proposed` event. Without such a result, a missing turn still
fails closed.
- `codex-harness-session.ts`: the same rule applies when the restored
runner reports that turn `failed` with `provider_turn_lost_on_restore`.
This happens when the runner cannot prove the accepted result itself.
Later recoveries keep the settled outcome. Without a held result, a
restore-lost turn keeps its existing failure and same-conversation
continuation.
- `fake-codex-app-server`: a test-only `--hold-after-terminal-result`
flag keeps the turn open after an accepted completion report.
- `doc/execution-semantics.md`: one additive paragraph on this recovery
rule.

## Verification

- Red before the fix, real `paperclip-runnerd` and fake Codex: `pnpm
--filter @paperclipai/paperclip-runner exec vitest run
src/live/runnerd-restart-continuation.test.ts -t "durable proposed
result"` failed with `native_session_recovery_failed:
HarnessReconciliationError: persisted active turn provider-turn-1 is
missing from thread/read` at `native-session-runtime.ts:2267`. This is
the same stack as the incident.
- Green after the fix: the run finalizes on `provider-turn-1` with the
proposed result and `runTerminalState: succeeded`. It has no
`session.failed` or `turn.failed`, and Codex received only one
`turn/start` (no re-run). With the transport fix alone, the test passes
in about 5 s. With only the session fix, it also passes (about 30 s,
through the 5 s read wait).
- Driver red/green:
`src/drivers/codex/codex-app-server-driver.recovery.test.ts -t "missing
persisted turn"`. The settle case failed before the fix (`recovered:
false`) and passes after it. The guard case (the held result belongs to
another turn) still fails closed. The existing "fails recovery
explicitly when the persisted active turn is missing or replaced" test
still passes.
- Driver red/green for the restore-lost case: `-t "restore-lost turn"`.
The held=true case failed before the fix and passes after it. The
held=false case still maps `turn.failed` with the lost cause.
- Transport unit test: `runnerd-codex-transport.test.ts -t "reconciled
result terminal"` covers the run-terminal rule and its negative cases.
- Suites: `src/drivers/codex/`, `src/native-session-runtime.test.ts`,
`src/backends/`, `src/live/runnerd-restart-continuation.test.ts`, and
`src/live/runnerd-codex-transport.test.ts` pass. Some load-sensitive
tests time out at host load 25–30 and pass when rerun alone. The two
`proves local suspension after an event backlog … (1024 suffix deltas)`
cases also time out on unmodified master.
- `pnpm --filter @paperclipai/paperclip-runner typecheck:typescript`
passes.
- Live chaos replay with real Codex: K1 0–3 s after
`run.result.proposed`, 3/3 PASS (R1). Each case resumed the same run,
which ended `succeeded`. No Retry was needed and no work was re-run.
Details are in the comment below.

## Risks

- The transport rule accepts a run terminal only directly after
runnerd's own reconciliation that names the ended turn with no active
turn. Runnerd emits that pair only after it restored the conversation
and proved the turn ended. A provider turn terminal still wins. Other
run terminals do not settle a turn.
- The session rules use only a result that Paperclip already holds for
the exact turn. The control plane accepted that result before the kill.
A later provider terminal for that turn cannot change the outcome. The
finalizer can already interrupt a provider after an accepted result.
- A missing turn without a durable result still fails closed. Its
outcome is unknown, and the existing Retry stays available.
- No profile, schema, pack contract, or database changes. The Rust
change touches only the fake Codex test binary.

## 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-06 18:02:40 -06:00
Nicky LeachandClaude Opus 5.5 97953e9b59 fix(ci): repair the weekly test-weight refresh and extend it to e2e and Runner lanes (#19650)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Contributors wait for PR CI before merge, so CI wall-clock time sets
the pace of work
> - CI test shards are balanced with per-file durations that a weekly
workflow refreshes
> - The weekly refresh has failed since 2026-11-02, and the e2e weights
have not been measured since 2026-09-15. Today e2e shards 3 and 6 take
7.0–7.7 minutes of tests, and shard 1 takes 4.3 minutes
> - A single file can also grow until it sets the time of its shard, and
nothing tells us about it
> - This pull request repairs the refresh, extends it to the e2e and
Runner package lanes, and warns about test files over 180 seconds
> - The benefit is balanced shards that stay balanced, and early notice
of oversized test files

## Linked Issues or Issue Description

**What existing behavior does this improve?**

The weekly refresh of the test-duration weights that balance CI shards.

**Subsystem affected**

CI: `scripts/ci/refresh-test-durations.mjs`,
`.github/workflows/ci-test-durations.yml`, and the Vitest and Playwright
CI reporters.

**Current behavior**

- The 2026-11-02 refresh (run 41408033686) failed. `gh api …/logs` now
refuses output that contains terminal escape sequences.
- Most successful `cloud-readiness.yml` master runs are admission-only
and have no server jobs. The script took the newest 2 runs, so it would
fail with "No server timing jobs found" on most weeks.
- The e2e weights (`scripts/e2e-shard-durations.json`) have no refresh
and are from 2026-09-15.
- No signal tells us when one test file becomes too slow.

**Proposed behavior**

The refresh runs again, selects master runs that ran the server jobs,
refreshes the e2e and Runner package-lane weights from merged PRs, and
lists oversized files. CI writes a warning for each test file over 180
seconds.

**Reason and benefit**

Unbalanced shards add minutes to every PR. The current e2e weights put
472 seconds on the slowest shard. The new weights put about 300 seconds
on each shard.

**Breaking changes**

None. Weights change only shard placement, never which tests run.

## What Changed

- Add `scripts/ci/gh-api.mjs`. It always passes
`--allow-escape-sequences`, retries without the flag when an older `gh`
does not know it, and strips ANSI codes from logs.
- `scripts/ci/refresh-test-durations.mjs`:
- scans up to 50 master runs and uses the 2 newest that ran the server
jobs;
- adds the `--dry-run`, `--pr-body=<path>` and
`--lanes=server,e2e,runner` options;
- validates every lane before it writes a file. If a merged-PR lane has
fewer than 2 valid runs, it keeps the current weights and writes a
warning.
- Add `scripts/ci/pr-run-durations.mjs`. It uses the final passing
`pr.yml` run of each merged PR, and it validates:
  - the PR is merged into `master`;
  - the merge commit is in the checkout;
  - the run's head SHA is the PR's final head SHA;
  - every shard of the run succeeded.

  It parses Playwright per-spec times and Runner per-file times.
- Add `scripts/ci/oversized-test-files.mjs` (Vitest) and
`scripts/ci/oversized-test-files-playwright.mjs` (Playwright). In GitHub
Actions they write a `::warning` for each test file over
`PAPERCLIP_CI_OVERSIZED_TEST_FILE_SECONDS` (default 180). They never
fail a job. The root and Runner Vitest configs and the e2e Playwright
config load them.
- `scripts/ci/test-timing-reporter.mjs`: move the per-file duration
calculation into an exported `moduleDurationMs()` helper. The behavior
does not change.
- `.github/workflows/ci-test-durations.yml` and
`.github/ci-duration-pr.md`:
  - the bot PR can also update the e2e and Runner manifests;
  - the PR body lists oversized files;
  - the verify step also runs the new tests and the e2e shard tests.
- Refresh `scripts/e2e-shard-durations.json`: 56 of 56 specs measured
(before: 30), from merged PRs #19613, #19191 and #19176. The file now
uses the shared schema with `sources` and `coverage`.

## Verification

- `node --test scripts/ci/test-selection-pr-timings.test.mjs
scripts/ci/test-selection-timings.test.mjs
scripts/__tests__/e2e-shard.test.mjs`: 32/32 pass. These include the e2e
shard balance check with the new weights.
- `node scripts/ci/refresh-test-durations.mjs --dry-run` against real
GitHub data, all lanes:

| Manifest | Measured | Sources |
| --- | --- | --- |
| `general-server-shard-durations.json` | 1488/1534 files | master runs
42296943853, 42294279748 |
| `serialized-shard-durations.json` | 167/167 files | the same two runs
|
| `e2e-shard-durations.json` | 56/56 specs | PR runs 42296374365,
42297700476, 42296994179 |
| Runner `vitest-lane-durations.json` | 310/311 files | the same three
PR runs |

- e2e placement over 8 shards, with the new medians as the real costs:
  - with the old weights, the slowest shard has 472 seconds;
  - with the new weights, every shard has about 300 seconds.
- The dry run lists 12 files over 180 seconds. The largest is
`server/src/services/workspace-recovery.test.ts`, at 636 seconds.
- In this PR, only the e2e weight file changes. The weekly run refreshes
the server weights. The Runner manifest is added by #19646.

## Risks

- The `--allow-escape-sequences` path ran only in unit tests. The local
`gh` did not support the flag, so the real dry run used the fallback.
The next weekly run is the first real test of the flag.
- The changes to the bot-PR shell steps did not run in Actions yet.
- The e2e weights come from 3 PR runs. Real shard times vary more than
the estimate.
- This PR and #19637 both edit the `reporters` lists in
`vitest.config.ts` and `packages/paperclip-runner/vitest.config.ts`. The
second PR to merge needs a small merge resolution.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with tool use and
code execution. A Claude sub-agent wrote the change, and the parent
session reviewed it.

## 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-06 15:50:02 -08:00
DottaandPaperclip 374a918d67 fix(runner): retain qualified OpenCode artifacts for cold recovery (#19622)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native recovery must keep admitted provider state and useful
workspace files.
> - OpenCode binds its saved session to exact executable paths and
hashes.
> - A replacement image can remove those files even when session data is
saved.
> - Retaining a manifest alone cannot restore the missing launch files.
> - This pull request saves the qualified files before provider work and
restores the same owned paths.
> - The benefit is cold continuity without relaxing the Runner guard or
replaying a prior turn.

## Linked Issues or Issue Description

Refs #19558. That fix retains the selected pack for ordinary
continuation. This change adds durable file retention for a cold
allocation. Related #19487 adjusts the admission budget; it does not
retain launch files. A search of open and closed OpenCode cold/durable
artifact PRs found no duplicate.

**What happened?**

A saved OpenCode session could outlive the image that supplied its Node
executable, proxy, and native executable. A new image supplied different
bytes. The Runner then correctly refused the changed launch profile. The
saved harness did not contain the old files.

**Expected behavior**

Core must save qualified launch files before the first turn. It must
restore those exact files under the same owned account and absolute
paths. Unknown ownership, missing files, and changed hashes must still
stop recovery.

**Steps to reproduce**

Start an admitted remote OpenCode session. Save its workspace, encrypted
checkpoint, and harness data. Destroy the original allocation and
controller files. Restore into a new allocation with an image whose
qualified proxy has different bytes. Resume the same session. The new
opt-in local qualification command automates this path with actual
OpenCode and a local synthetic model.

## What Changed

- Store company-bound, encrypted launch objects and canonical run/backup
references. Generate migration 0438 through the normal database
workflow.
- Stage qualified Node, proxy, and native OpenCode files under a private
content-addressed OS-account home. Record the account UID and exact
launch paths. Preserve the Rust profile guard.
- Gate first provider work on durable capture under the current native
claim. Bound Stop/deadline waits while the actual late transfer keeps
its worker and scratch ownership until settlement. Restore cold caches
with verified staging and atomic rename. Keep live reattachment and
historical image-rooted sessions on their existing paths.
- Reuse bounded canonical expiration owners. Atomically fence old
checkpoint authority with a strict company/run-scoped retirement marker
before external deletion. Keep its indexed cleanup anchor across failed
deletes and restart. Preserve earlier resume handles and reject later
header or source-consumer writes. Add no idle scan or network call
inside a transaction.
- Add storage, race, executor, filesystem, and actual Rust resume
regressions. Add the opt-in local Linux qualification command and
operating limits in `doc/opencode-cold-recovery.md`.

## Verification

- RED before repair: real PostgreSQL/AES cold restore lacked the launch
files; normal executor preparation chose the new image pack. Separate
regressions exposed lost upload receipts, missing cache repair,
last-owner retirement, an account-UID mismatch, and a resume-handle race
during external deletion. Each expected failure was captured before its
repair.
- Local qualification before the final master integration: 1,169
executor tests, 233 storage/archive/admission/expiration tests, all
seven server typecheck layers, migration safety, and the actual Rust
cold-resume test passed.
- The maintained Linux command passed with two cached immutable
Paperclip images whose proxy hashes differ. It removed the original
account/controller trees, restored the production published cwd under a
new lease, resumed the same actual OpenCode session, kept a useful file
and prior model context, and made one new synthetic model request. No
external model call or network was available in either container.
- The object-store fixture uses real encrypted StorageService streams
with an independent owned local directory. It is not hosted S3
acceptance or a complete task HTTP/actor workflow. Historical tasks
still require normal effects reconciliation and recovery after
deployment.
- Review regressions reproduced partial-cache publication,
resume-versus-deletion, fresh-writer and source-consumer resurrection,
and unbounded Stop/deadline waits before each repair. They now pass
through real filesystem, encrypted storage, PostgreSQL, and SDK
boundaries. File validation is split from database ownership (384/831
source lines).
- Clean runtime head `aadab7acc69ff959678030db46ffe2dcfbefe28f`: the
maintained actual OpenCode cold qualification passed with `dirty:false`.
It preserved the provider session and recorded launch paths, restored
useful workspace content and prior model context, and made exactly one
resumed synthetic model request. The 314,560,000-byte launch capture
took 23.118 seconds; the later harness was 833,536 bytes. Subsequent
repairs change backup-reference retirement and governed continuity-cache
classification; capture, restore, launch, and published-workspace inputs
are unchanged, so that runtime receipt remains scoped to those paths.
The continuity-break consumer is verified separately.
- Retention repair `c14d386cecf7123257918ba6a8d47307df2da397`: 68/68
affected real PostgreSQL/AES storage tests and 15/15 focused executor
authority/admission cases pass. All seven server typecheck layers pass
(source layers and unchanged test shards, with the final 590-file test
layer repeated after fixture edits). All review threads have
evidence-backed resolutions. Its subsequent CI caught the separate
lifecycle-classification gap described below. The refreshed Paperclip
review reported paused work, not a completed new review.
- Final head `4d1cd3e3309e328b11abc85adc6d78c6d0c7f55d`: all 19 focused
root-entry/executor cases and all three source declaration layers pass.
[Greptile reviewed this exact head at
5/5](https://github.com/paperclipai/paperclip/pull/19622#issuecomment-6695984874).
Final-head [CI
verification](https://github.com/paperclipai/paperclip/actions/runs/42308908167/job/128214100339)
passes: 60 successful checks, five conditional skips, and the explicitly
neutral paused Paperclip review. No check is pending or failing.
- CI also caught the launch cache missing from the session-root
lifecycle contract. Both the writer scan and a real governed-replacement
consumer failed locally before repair: old launch bytes remained in the
active root. The cache now archives with the prior session state; the
consumer verifies exact bytes are preserved in the archive and absent
from the active root.
- The latest review found that backup retirement could delete an
independent run pin while a selected session retained the checkpoint. A
real PostgreSQL/AES regression failed before the repair. Backup-only
ownership now uses a NULL-safe exact backup predicate; the run pin
belongs to fenced checkpoint expiration. The regression verifies
checkpoint decryption and launch-file restoration after pending-backup
cleanup, followed by deletion only after removing the final resume
handle.
- An executor first-session case checks new-session durable capture
through the real capture helper and receipt propagation into the actual
harness manifest at the backup boundary. Real storage cases verify the
run-profile receipt and canonical backup/run pins.
- Sequential closure reuse and simultaneous first admission are
different scopes. A deterministic two-uploader test measures both
independent uploads, one verified ready winner, loser-only deletion, and
usable restoration for both runs. Each capture is bounded to 512 MiB and
five minutes; up to three pending receipts per run are retained. There
is no claim of global single-flight capture or a company-wide egress
bound.
- Earlier optional warm-ownership verification encountered missing local
binaries. After exact-source setup, all nine ownership assertions
passed; two other suites encountered local PostgreSQL setup/teardown
contention, so those interrupted attempts are not passes. Required
current-head CI supplies the broad final coverage. No deployment or
hosted historical recovery is claimed.

## Risks

- This adds two tables and a forward migration. The file object is
bounded to 512 MiB. Capture adds one initial transfer; later harness
archives exclude those large files. Company-scoped ready objects are
shared and retained until the last canonical owner retires. Concurrent
initial admissions can duplicate bounded transfers; each keeps
independent ownership and cancellation rather than waiting on another
controller. A continuity break retains the controller cache with its
prior state until normal session-root cleanup, adding up to one bounded
launch closure per retired state.
- Cold continuity requires the same qualified Linux platform, supported
runtime dependencies, owned account UID/home, and published absolute
cwd. A mismatch fails closed. No alias or Rust guard change is included.
- Existing image-rooted profiles have no retained file object. This
change cannot backfill missing historical binaries. A fresh governed
session must reconcile prior effects first.
- Live reattachment does not read artifact objects. Storage or
provenance failures before a new turn hold admission. They do not grant
new execution, model, tool, or replay authority.

## Model Used

OpenAI Codex, GPT-6 family, with reasoning, repository tools, code
execution, and local browser tools. The exact served model ID and
context window are not exposed in this session. No claim is made for
unavailable model metadata.

## 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-06 17:30:00 -06:00
DottaandClaude Opus 5.5 fd58378ee6 fix(runner): never dispatch an OpenCode tool call its MCP client abandoned (#19546)
## 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 an MCP bridge inside the OpenCode proxy. The
proxy sends each call to runnerd as an `item/tool/call` frame on stdout.
> - runnerd reads the proxy stdout only after the controller
acknowledges earlier events. A backlog of event frames can build in the
proxy. A tool call waits behind that backlog.
> - The OpenCode MCP client can time out while the call waits. The agent
then gets `MCP error -32001: Request timed out`. But the queued frame
still reaches runnerd later, and runnerd still runs the call.
> - This is a safety bug. A mutating call (for example, create a gateway
token) can succeed after the agent was told that it failed. The agent
can then repeat it.
> - This pull request makes sure that a call whose MCP client abandoned
it is never sent to runnerd while it is still queued in the proxy.
> - The benefit: "timed out" now means "did not run" for every call that
had not left the proxy.

## Linked Issues or Issue Description

**What happened?**
In chat-feature evals with native OpenCode, the agent got `MCP error
-32001: Request timed out` for a POST tool call. The server still ran
the POST and returned 201 about one second later. Other Paperclip tools
that took 0.03–0.5 s on the server reached OpenCode 10–22 s later. The
delay grew over the run, and then the calls failed with -32001.
Sometimes they failed at once.

**Expected behavior**
If the MCP client abandons a tool call before Paperclip dispatches it,
Paperclip must not run it.

**Steps to reproduce**
1. Run a native OpenCode agent (`paperclip_runner` adapter, runnerd
path) for a long chat turn with many tool calls.
2. Make the controller acknowledge runnerd events slowly, so a backlog
builds in the proxy.
3. Let the OpenCode MCP client time out on a mutating call that waits in
the backlog.
4. See that the agent gets -32001, but the call still runs on the
server.

**Where the per-call lag comes from (finding)**
- runnerd (`provider_backend.rs`, `poll_current_provider_with_control`)
does not read proxy stdout while any event is not acknowledged. It reads
at most 128 events per poll.
- Each poll that admits events, and each acknowledgement, calls
`persist_state`. That function writes the full provider state JSON again
(up to 16 MiB, and it grows during the run) and does two fsyncs. So each
round trip gets slower during the run.
- Pipe writes from Node are asynchronous on POSIX. OpenCode event frames
and `item/tool/call` requests therefore wait in one FIFO buffer in the
proxy. A tool call waits behind all earlier event frames, and the
results come back through the same gated path.
- OpenCode starts its MCP timeout when it calls the tool, not when
runnerd reads the frame. The -32001 event frame can itself wait in the
backlog, so it can look "instant" when it arrives.
- Evidence: the delay from runner emit to server receipt stays below 0.5
s. But runnerd's `item.completed` for a call comes 10–22 s before
OpenCode's `tool.execution.completed`, and the gap grows during the run.
- The fix for the lag is not small. It needs a change to runnerd
persistence: do not gate stdout reads on a full acknowledgement, or use
an append-only event log instead of a full state rewrite with two fsyncs
for each batch. This pull request does not change runnerd.

## What Changed

- `mcp-bridge.ts`: abort the call signal when the MCP client drops its
HTTP connection before a response. The `notifications/cancelled` path
already aborted it.
- `opencode-server-driver.ts`: send the bridge abort signal to
`dynamicToolHandler` (new optional `signal` field).
- `opencode-proxy-output.ts`: add `createBackpressuredFrameWriter`. It
gives the stream only what its buffer accepts and keeps the backlog in
its own queue. Frames keep their order. If a queued frame's signal
aborts, the writer removes the frame and calls `onAbandoned`.
- `opencode-app-server-proxy.ts`: write stdout through the new writer.
Tool-call requests carry the call signal. An abandoned request fails
with "abandoned by the MCP client before it was dispatched; it did not
run". Shutdown drains all live frames.
- Tests: a new regression test uses the real bridge and the writer with
a stalled stdout. It covers a cancel after timeout, a dropped
connection, and a live call. There are also unit tests for the writer.

## Verification

- `npx vitest run src/drivers/opencode src/cli/opencode` in
`packages/paperclip-runner`: 23 files, 226 tests pass.
- Red check: I removed the disconnect abort and the queue gating. Then 2
of the 3 tests in `opencode-proxy-abandoned-tool-call.test.ts` failed:
the abandoned `item/tool/call` reached stdout. After I restored the
code, they pass.
- `pnpm verify` exits 2 because the changed proxy files are outside its
resolved test graph. The required CI jobs supply broad coverage.
- `tsc --noEmit` for the runner package shows no errors in the changed
files. One error remains in `src/eval/workflow-harness.ts`. It comes
from a workspace package that is not built and is not related to this
change.

## Risks

- A call that already left the proxy (it is in the kernel pipe buffer or
in runnerd's reader) still runs after a client timeout. The window is
about 64 KiB of pipe buffer plus Node's 16 KiB high-water mark. To close
it fully, runnerd must get a deadline or a cancellation for each call.
That is a follow-up.
- Frames now wait in a proxy queue instead of the Node stream buffer.
Delivery order does not change. The new code drains this queue on
shutdown before the last stdout flush.
- When a client disconnects, a durable call is now cancelled. Before
this change it stayed pending. The client cannot get the result in
either case.
- No schema, contract, or ACPX profile changes.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), used in Claude Code
with tool use (shell, file edit, 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-06 23:29:26 +00:00
Nicky LeachandClaude Opus 5.5 a2aa69bc62 perf(ci): split the Runner package Vitest lane into three duration-balanced lanes (#19646)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Contributors wait for PR CI before merge, so CI wall-clock time sets
the pace of work
> - The Runner package Vitest lane is the PR CI critical path. It
finishes a median 15.3 minutes after the push
> - That lane runs all 311 package test files in one job with 2 workers:
1,381 seconds of serial work and 727 seconds of wall time
> - Ten files account for 61% of the time. Vitest's `--shard` splits by
file count, not by duration, so it cannot balance this work
> - This pull request splits the package files into three lanes balanced
by duration, and runs the native server group in its own lane
> - The benefit is about 232 seconds of tests per package lane instead
of 727 seconds

## Linked Issues or Issue Description

**What existing behavior does this improve?**

The time that PR CI needs to run the Runner package Vitest tests.

**Subsystem affected**

CI: the `Verify Paperclip Runner` Vitest lanes in `pr-trusted.yml`, and
`packages/paperclip-runner/scripts/run-pr-vitest-lane.mjs`.

**Current behavior**

The `vitest package` lane takes a median of 13.6 minutes. It is the last
job to finish in most PR runs. The lane keeps a 2-worker limit so that
Runner and sidecar processes can start within their deadlines. Average
CPU use is 29%.

**Proposed behavior**

Three package lanes each run about one third of the files, balanced by
measured duration. Each lane keeps the 2-worker limit. The native server
group runs in its own lane, as before.

**Reason and benefit**

The Runner lane stops being the critical path. More workers per lane
would add CPU contention and timeouts, so this change adds lanes
instead. The cost is about 3 runner-minutes per run for the extra setup.

**Breaking changes**

None. The `master` ruleset requires only `ci / verify` and `ci / e2e`,
so the new job names do not change required checks.

## What Changed

- Add `packages/paperclip-runner/scripts/vitest-package-lanes.mjs`.
- It gets the file inventory from `vitest list --filesOnly --json`, so
it follows `vitest.config.ts`.
- It partitions the files with the `partitionGeneralServerSuites`
function from `scripts/general-server-shard.mjs`.
- Add `packages/paperclip-runner/scripts/vitest-lane-durations.json`.
- It uses the same schema as
`scripts/general-server-shard-durations.json`.
- It is seeded from CI run 42290920882. It measures 310 of the 311
files.
- `run-pr-vitest-lane.mjs` accepts two new arguments:
  - `--package-lane=K/N` runs one partition.
  - `--native-server` runs only the native server group.
- The `--shard=1/2` and `--shard=2/2` arguments keep their exact
behavior for the matrix that master still sends.
- `.github/ci-task-matrix.json` and the fallback matrix in
`pr-trusted.yml` have three `vitest package (K/3)` lanes and the `vitest
native server` lane. The labels keep the `vitest` prefix because
`scripts/ci-profile/trace.mjs` matches `Verify Paperclip Runner
(vitest`.
- Tests:
- the lanes cover the inventory exactly once, checked with real `vitest
list`;
  - the old `--shard` commands plan the same commands as before;
  - the manifest schema is correct;
  - bad lane arguments are refused.
- Update the Runner Vitest lane paragraph in `doc/DEVELOPING.md`.

## Verification

- `node --test scripts/__tests__/run-vitest-stable-shard.test.mjs`:
44/44 pass.
- `node --test scripts/__tests__/runner-release-crate-cache.test.mjs
.github/scripts/tests/ci-plan.test.mjs`, together with the file above:
71/71 pass.
- Related workflow, cache, routing and profile tests: 446/446, 153/153
and 14/14 pass.
- Local end-to-end run of `GITHUB_WORKFLOW=PR node
scripts/run-pr-vitest-lane.mjs --package-lane=3/3` on macOS:
- the prep steps ran, and the lane ran 104 of 311 files in 123 seconds;
- 2 tests failed: a macOS `kill EPERM` and a 5-second timeout under full
local load without the CI worker limit;
  - both files pass when run alone (35/35 tests).
- This PR's own CI runs the matrix from master, so it uses the old
`--shard` commands. The new lanes run in CI only after merge. Reviewers
can compare the job durations of the first PR runs after the merge.

| Lane | Files | Serial estimate | Wall estimate with 2 workers |
| --- | --- | --- | --- |
| 1/3 | 103 | 464.9 s | ~232 s |
| 2/3 | 104 | 465.0 s | ~232 s |
| 3/3 | 104 | 465.0 s | ~232 s |

## Risks

- The weights come from one CI log and do not include collection or
setup time. The balance can be uneven until a refresh measures real lane
runs.
- Each lane passes its file paths to Vitest as filters, and Vitest
matches a filter as a substring of the path. A path that is a substring
of another path would run a file in two lanes. No current path does
this. If one does, the script prints a warning, and no file is dropped.
- Each package lane runs `ensure:eval-build-deps` and `build:rust`
again, which adds about 3 runner-minutes per run.
- A comment in `scripts/run-vitest-stable.mjs` (about line 110) still
says that the last Runner shard runs the native group. A follow-up will
correct it.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with tool use and
code execution. A Claude sub-agent wrote the change, and the parent
session reviewed it and ran the local lane check.

## 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-06 15:12:05 -08:00
DottaandPaperclip 2e340d6235 fix(ui): show unread message counts in agent chat pickers (#19627)
## Thinking Path

> - Paperclip helps people manage AI agents and their work.
> - People switch between agent chats to read replies.
> - The agent picker does not show how many messages are unread.
> - People need that count before they select a chat.
> - The chat list already has the viewer's read state.
> - This pull request adds numeric badges from that shared list.
> - People can find unread replies without extra requests for each
agent.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

The chat header agent picker and the secondary sidebar's Add chat
picker.

**Subsystem affected**

Agent Chat UI, shared task types, and the chat read-state service.

**Current behavior**

The pickers do not show the unread message count for each agent.

**Proposed behavior**

Show the unread count beside each agent. For example, show 3 for three
unread agent messages. Hide the badge when the count is zero. Keep
switching available while chat data loads.

**Reason and benefit**

People can see which chats need attention before they switch. The count
uses the existing shared request and cache.

**Breaking changes**

The chat response gains an optional `unreadMessageCount` field. Existing
fields and read receipts keep their behavior. There is no database
migration.

**Additional context**

Related: #19425 changes chat structure. This change adds counts to the
current agent pickers. The duplicate search found no PR for these count
badges.

## What Changed

- Count unread agent comments in the existing grouped read-state query.
Keep company and viewer scope.
- Exclude human messages, deleted comments, system notices, and
fast-response receipts.
- Pass memoized count maps to both agent pickers. Use a direct lookup
for each row.
- Show numeric badges and accessible singular or plural labels.
- Test count changes, account isolation, cached opens, search, and large
agent lists.
- Update the chat stories, fixture data, and development guide.
- Make the Slack owner retry-deadline test deterministic after
reproducing its CI failure.

## Verification

- On current head `05f5fa6201`, the [main CI
workflow](https://github.com/paperclipai/paperclip/actions/runs/42305845930)
passed, including build, typecheck, all applicable test shards, browser
shards, and final verification. The prior server shard 2, 6, and 7
failures do not recur.
- [Greptile's current-head
review](https://github.com/paperclipai/paperclip/pull/19627#issuecomment-6696161631)
is 5/5, its check succeeded, and both previous review threads are
resolved. Current head check inventory is complete: 55 successful
checks, three intentional skips, and no pending or failed checks. The
Paperclip application review summary reports paused; no new completed
Paperclip review is claimed. Its existing findings are resolved, and the
independent current-head Greptile review is 5/5.

- Current head `05f5fa6201` merges master `8621bcf53e` to resolve an
import conflict. Master now includes the same model-default test repair.
This branch keeps its upstream import, so that file no longer differs
from master.
- After this merge: all 21 server tests in the Slack owner and
model-default suites pass, all 22 tests in the three picker/sidebar
suites pass, and token gates pass. The standard preflight still cannot
build Rust because `cargo` is absent. Fresh CI must supply the broad
checks.

- Head `7d125fb8c7` replaces the Slack owner test's real timer wait with
the worker's existing clock input. It checks that the intent stays
pending one millisecond before the retry deadline and clears at the
deadline. All 15 Slack owner integration tests pass locally. Runtime
retry behavior is unchanged.
- On `4cfb1e9d55`, both review checks gave 5/5. CI [server shard
2](https://github.com/paperclipai/paperclip/actions/runs/42302458830/job/128183671349)
failed the Slack credential-lease retry test. The same assertion failed
in an unchanged local run. A diagnostic run with added logging passed;
the exact timer deviation was not captured. The new test removes the
real-time wait assumption and checks the clock boundary directly. The
earlier model-default and cleanup failures did not recur in that CI run.

- On `a3378dfc40`, both Greptile and Paperclip Review gave 5/5 with no
open findings. Typecheck, build, all eight browser shards, and most test
shards passed.
- That CI attempt failed two server shards. The model-default test still
expected `gpt-5.6-sol`, while the current default is `gpt-6.1-sol`. I
reproduced both failures locally. Commit `4cfb1e9d55` uses the exported
default-model constant; all six tests in that suite now pass. Production
model selection does not change.
- The retained-cleanup backlog test exceeded its 15-second limit in
[server shard
6](https://github.com/paperclipai/paperclip/actions/runs/42299858777/job/128175193320).
The unchanged test passes locally with the same limit. Its CI timeout
cause is not established. No assertion or timeout was changed. The fresh
CI run must recheck it.

- Review repair: commit `0db631b215` keeps the existing-chat and
unread-agent arrays stable when chat data and the active agent do not
change. A new 500-agent test fails before the repair and passes after
it. The test also checks count refreshes.
- The first CI typecheck failed on duplicate `IMAGE_ONLY` and
`TESTOPS_REQUESTED` keys in `tests/runner-e2e/workflow-target.test.ts`.
This was present in upstream code. Master commit `e482f0f632` removes
the duplicate keys. This branch now includes that commit. All nine
workflow-target tests pass locally.
- After the latest master merge: all 22 tests in the three affected
picker/sidebar suites pass. Before that merge, all 38 focused UI tests
passed.
- The current head is `05f5fa620134ad199a3d85b66fb2eb93d3bb6077`. All
current status checks are complete, the aggregate verify gate succeeds,
mergeability is CLEAN, and no unresolved review threads remain. The
maintainer has instructed normal merge after checks pass.

- After merging current master: 37 focused UI tests pass across
AgentChatPicker, CoordinatorAgentSwitcher, AgentConversationsSidebar,
useAgentChatRead, and SidebarAgentChats.
- After merging current master: all 38 proactive-chat integration tests
pass with a real disposable PostgreSQL database. They check counts of 2,
1, and 0 at exact read cutoffs.
- The prior revision passed UI typecheck and build, shared build,
isolated low-memory server typecheck, token gates, and 30 map/catalog
tests.
- The 500-agent test bounds index work and count lookups. Search does
not rebuild the existing-chat index.
- Browser checks used production components with simulated API data.
Dark, light, mobile, and Add chat cases passed. Counts of 3 and 12
stayed visible through search. Read agents had no badge. Cached opens
made no chat API requests.
- Twenty local cached opens took 34.5–57.6 ms, with a 38.4 ms median.
This is an unthrottled fixture measurement.
- Earlier full local checks could not complete because Rust cargo is
absent and the workspace has limited memory. The standard worktree
preflight also stops at missing cargo. Required CI must supply broad
checks for this PR head.
- The first server typecheck ran out of memory while Storybook was
active. Its isolated low-memory retry passed. No visual baseline
comparison or live provider delivery is claimed.

## Risks

- Low risk. The count replaces a boolean aggregate in the same grouped
query. Very long chat histories still need that existing scan.
- A count can be stale until the existing chat-list cache refreshes.
Read receipts still control the unread cutoff.
- The change adds no polling, per-agent history request, database
migration, or permission change.

## Model Used

OpenAI GPT-6 through Codex, with code editing, terminal tools, and
browser verification. The runtime did not expose a more specific served
model ID or context-window size.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 17:10:56 -06:00
DottaandClaude Opus 5.5 7941f6b051 fix(runner): show OpenRouter's invalid model ID as a model rejection (#19548)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs use the Paperclip runner with drivers such as OpenCode
> - When a provider rejects the selected model, the product shows "Model
unavailable" and offers a model choice
> - OpenCode reports provider HTTP errors as `APIError` with the message
under `data`, not at the top level
> - The runner then reported "Provider turn failed", so an invalid model
ended as a generic adapter failure
> - This pull request classifies OpenRouter's invalid model ID with a
closed provider-failure category, and makes the server keep that
classification
> - The benefit is that a person sees "Model unavailable" and the real
provider message, and can choose another model

## Linked Issues or Issue Description

**What happened?**
A chat evaluation selected a model ID that OpenRouter does not know.
OpenCode reported `turn.failed` with `error: { name: "APIError", data: {
message: "<model> is not a valid model ID", statusCode: 400,
isRetryable: false } }`. The run was saved with `errorCode:
adapter_failed`. The task chat showed "The OpenCode run failed before it
completed." and no "Model unavailable" marker. The provider notice said
only "OpenCode session failed.".

**Expected behavior**
A non-retryable rejection of the selected model is saved as
`native_provider_model_rejected`, so the product shows "Model
unavailable" and the model choice. The notice shows the provider's
message.

**Steps to reproduce**
1. Configure an OpenCode agent with an OpenRouter model ID that does not
exist.
2. Send the agent a message.
3. Before this change, the run fails as `adapter_failed` and the notice
says "OpenCode session failed.".

## What Changed

- `drivers/opencode/provider-failure.ts`: a non-retryable OpenCode
provider error whose message says the model is not valid (`not a valid
model`, `model_not_found`, model not found or not supported) gets the
closed category `model_unavailable`. A provider that allows a retry
keeps the existing category.
- `native-session-runtime.ts`: a non-recoverable failure with
`model_unavailable` is a model rejection. The runtime reads the closed
category, not provider text.
- `native-session-executor.ts`: a non-recoverable
`NativeProviderTerminalFailure` that the runner marked as a model
rejection is saved as `native_provider_model_rejected`. Before, every
such terminal was saved as `native_provider_terminal_failed` before the
marker was read.
- `drivers/opencode/opencode-server-driver.ts`: the provider notice
summary shows the provider message from `data` when there is no
top-level message. Provider text is not used as the turn failure
message, so it does not reach `run.error` or Sentry.
- Tests: provider-failure mapping (retryable and non-retryable), runtime
classification by category, driver notice summary with no failure
message, and an executor test for the saved code and terminal
disposition.

## Verification

- `npx vitest run src/native-session-runtime.test.ts
src/drivers/provider-failure.test.ts src/drivers/opencode` in
`packages/paperclip-runner`: 16 files, 412 tests passed.
- `npx vitest run
src/services/native-runtime/native-session-executor.test.ts` in
`server`: 1,152 tests passed.
- Negative controls: reverting the provider-failure mapping, the runtime
check, or the executor branch each makes its new test fail.

## Risks

- Low risk. Classification uses a closed category and a runner-set
marker, not provider text.
- The model check on the provider message is text-based inside the
OpenCode mapper. A provider that uses "not a valid model" for another
reason with a non-retryable error will show "Model unavailable".
- Provider text appears in the notice summary, as provider notices
already did for top-level messages.

> 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-06 17:08:04 -06:00
DottaandClaude Opus 5.5 02a2299173 feat(codex): default Codex to gpt-6.1-sol, half the gpt-5.6-sol price (#19623)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Codex agents run through Paperclip Runner or the legacy
`codex_local` harness, and the runner E2E suites run paid Codex cells
> - The shared Codex default and many selections preferred `gpt-5.6-sol`
> - The reviewed OpenAI rate card in
`server/src/services/codex-pricing.ts` (`openai-standard-2026-09-30`)
prices `gpt-6.1-sol` at $2 input / $0.10 cached / $10 output per 1M
tokens, and `gpt-5.6-sol` at $4 / $0.40 / $20
> - So every default run and every paid Codex eval cell cost twice as
much as necessary
> - This pull request moves every place that prefers or selects
`gpt-5.6-sol` to `gpt-6.1-sol`, and keeps historical facts, rate cards
and compatibility fallbacks on `gpt-5.6-sol`
> - The benefit is half the token price for new default Codex agents and
for the Codex E2E and TestOps cells

## Linked Issues or Issue Description

**What existing behavior does this improve?**
The default Codex model for new agents and the Codex model that the
runner E2E suites and TestOps plans select.

**Subsystem affected**
Adapters (`adapter-utils`, `codex-local`), server native runtime
fallback, UI AI connection pools, Paperclip Runner devtools and live
evals, runner E2E catalog and TestOps mappings.

**Current behavior**
New Codex agents without an explicit model store `gpt-5.6-sol`. The
runner E2E `runner-codex` profile, the Reliability `queue-steer` plan
cell, the Product regression TestOps mapping and the ACPX Codex
qualification selection all use `gpt-5.6-sol`.

**Proposed behavior**
They use `gpt-6.1-sol`. Existing agents that store a model keep it.
`gpt-5.6-sol` stays selectable.

**Reason and benefit**
`gpt-6.1-sol` costs half as much per token on the in-repo reviewed rate
card. The current OpenAI pricing page (retrieved 2026-11-06) shows the
same short-context rates and also lists long-context rates of $4 / $0.20
/ $15 for `gpt-6.1-sol` against $8 / $0.80 / $30 for `gpt-5.6-sol`.

**Breaking changes**
None for stored configurations. See Risks.

## What Changed

Changed defaults and selections (all now `gpt-6.1-sol`):

- `PAPERCLIP_RUNNER_DEFAULT_MODELS.codex` in
`packages/adapter-utils/src/paperclip-runner-permissions.ts`. This also
changes `DEFAULT_CODEX_LOCAL_MODEL`,
`resolvePaperclipRunnerModel("codex", …)`, runner config normalization,
`codex_local` → Paperclip Runner conversion, the UI new-agent and
agent-config defaults, the composer default model and the public MCP
effort lookup.
- `codex-local` `models`: the 5.6 entry is now the literal
`gpt-5.6-sol`, because it used to be `DEFAULT_CODEX_LOCAL_MODEL`. The
list order and contents do not change.
- AI connection pool: a new OpenAI member profile uses
`PAPERCLIP_RUNNER_DEFAULT_MODELS.codex` instead of a hard-coded
`gpt-5.6-sol`.
- Paperclip Runner devtools ACPX Codex default and preset, the
`runner-codex-sol` local provider smoke, and the `acpx-codex-sol` live
workflow candidate.
- `tests/acpx-qualification-models.ts` Codex selection (runner E2E and
runner acceptance ACPX Codex profiles).
- Runner E2E: the `runner-codex` profile follows the default, so all
native Codex cells move. This includes the `runner-codex` cells among
the 52 standard `everyday-workflows` cells and the seven Codex
reliability cells. `runner-codex-mini` stays on `gpt-5.4-mini`. The
Reliability plan `queue-steer` `steeringProfile` is now
`['runner-codex', 'gpt-6.1-sol']`. Its executionId
(`everyday-workflows.runner-codex.daytona.queue-steer`), its fullName
and its TestOps case identity do not change. No separate profile is
necessary.
- `product-regression-testops.mjs`: the declared model for every
`runner-codex` case is now `gpt-6.1-sol` (it was already that for
`configured-repository`).
- `tests/runner-e2e/installed-cli.ts`: the credential-free installed
startup proof opens a session on the default model.
- `tests/runner-e2e/scheduled-prices.json`: add a `gpt-6.1-sol` price
(`openai-standard-2026-11-06`, including 30m/12h cache writes and
long-context rates from the OpenAI pricing page). The weekly scheduled
Codex cells need an exact price for their model, or admission fails
closed.
- Storybook: the composer model picker fixtures, the connection pool
stories and host, and the agent settings preview show `gpt-6.1-sol` as
the default.
- Docs: `doc/DEVELOPING.md` (the Codex conversion default) and
`tests/runner-e2e/EVERYDAY-WORKFLOWS.md` (local matrix, reliability
extras, `queue-steer` row).
- Tests that assert the default now expect `gpt-6.1-sol`. New
assertions: the default has the `0.159.0` CLI floor, the compatibility
fallback is floor-free, the `queue-steer` plan cell runs `runner-codex`
on `gpt-6.1-sol`, and all native Codex everyday cells (standard and
reliability) use `gpt-6.1-sol`.

Deliberately kept on `gpt-5.6-sol`:

- Rate cards: `server/src/services/codex-pricing.ts` and
`packages/paperclip-runner/src/evals/model-pricing.ts`. Old receipts
need them. (The runner eval rate card has no `gpt-6.1-sol` entry. An
unpriced candidate is reported as unpriced, not as zero. I did not add a
rate there because that card has its own version and source.)
- The remote Codex CLI compatibility fallback. A new
`CODEX_LOCAL_COMPATIBILITY_FALLBACK_MODEL = "gpt-5.6-sol"` replaces the
default in `codexModelFallbackCandidates`. `gpt-6.1-sol` needs Codex
0.159.0 or newer, so it cannot be the floor-free last resort. Fallback
behavior is unchanged.
- `tests/runner-e2e/provider-usage.ts`: the implicit request
cache-policy attribution stays qualified only for `gpt-5.6-sol`. I did
not verify that policy for `gpt-6.1-sol`. A `gpt-6.1-sol` response that
omits the write-TTL split keeps its cache-write cost unknown, which is
the existing behavior for unqualified models.
- `tests/runner-e2e/scheduled-measurements.json`: historical
`gpt-5.6-sol` measurements. They no longer match the current suite
definitions, so the forecast reports them as historical only. The
forecast tests now build their synthetic cohorts on the current
definition hash.
- Model catalogs that list `gpt-5.6-sol` as available (`codex-local`,
`cursor-local`, `opencode-local`, Speko voice diagnostics, Pi picker
fixture).
- The `gpt-5.6` → `gpt-5.6-sol` alias for old stored configs.
- Dated or evidence docs: `doc/plans/*`, `doc/adapter-model-audit-*`,
`QUALIFICATION-*`, `doc/connections/MEMORY.md`,
`doc/connections/TASK-REVIEWS-VERIFICATION.md`,
`tests/runner-e2e/CHAT-FEATURES.md`, the Slack eval evidence, and the
S49 soak signature text (the soak failure happened on `gpt-5.6-sol`).
- Test fixtures that parse, price, migrate or round-trip an existing
`gpt-5.6-sol` value (Rust runner tests, driver tests, pricing tests,
stored-config tests, stories that show an existing agent).

## Verification

- `npx vitest run` on every server, UI, adapter-utils and codex-local
test file that mentions `gpt-5.6-sol` or the default, plus all of
`packages/adapter-utils` and `packages/adapters/codex-local`: 5,483
passed. Three timeouts under load (`sandbox-managed-runtime`,
`acp.quota`) passed when rerun.
- `packages/paperclip-runner`: `vitest run` for `src/eval`, `src/evals`,
`src/drivers/acpx`, `src/drivers/codex`, `src/cli`, `src/contracts`,
`src/backends`: 2,641 passed.
- `node --test tests/runner-e2e/reliability-testops.test.mjs
tests/runner-e2e/product-regression-testops.test.mjs`: 28 passed.
- `node tests/runner-e2e/run-unit.mjs`: all model, catalog, reliability,
scheduled and TestOps tests pass. Failures remain only in
`copilot-protection-flow`, `denied-target-isolation-flow`,
`pi-controls-flow`, `workflow-target` and `unit-launcher`. These five
also fail on unmodified master in this local environment (filesystem
watch and git environment), and they do not touch models.
- `vitest run --config tests/runner-acceptance/vitest.config.ts`: 19
passed.
- `pnpm test:e2e:runner -- --list --suite everyday-workflows --group
reliability`: the seven `everyday-workflows.runner-codex.daytona.<case>`
cells show `gpt-6.1-sol` with `OPENAI_API_KEY,DAYTONA_API_KEY`.
`--matrix-json` gives 16 reliability cells with the same credentials as
before. `--list --all` shows no `gpt-5.6-sol` cell.
- Typecheck: `adapter-utils`, `adapter-codex-local`, `server`, `ui`,
`paperclip-runner` (TypeScript), and `pnpm test:e2e:runner:typecheck`.
- `pnpm check:token-gates`: clean.
- Not run: paid cells, Rust tests (no Rust change), the browser suites,
and the full `pnpm -r typecheck` / `pnpm build` (CI runs them).

## Risks

- Agents without an explicit model change model on their next save or
normalization, and Paperclip Runner rows with a blank model use
`gpt-6.1-sol` at execution. Stored models, including `gpt-5.6-sol`, are
never rewritten. No migration is necessary.
- `gpt-6.1-sol` needs Codex CLI 0.159.0 or newer with ChatGPT sign-in.
The runner pins `@openai/codex@0.160.0`. On a remote target with an
older image CLI, the existing compatibility check falls back to
`gpt-6-sol` and then `gpt-5.6-sol`, and that probe now runs for
default-model agents too. A legacy `codex_local` agent created with the
new default on a host with an older Codex CLI gets the existing
actionable "upgrade Codex" Test result.
- Qualification: the native Codex profile uses `adapter_constant` model
qualification, so it follows the default. No qualification gate is
dropped. The ACPX Codex qualification selection moves to `gpt-6.1-sol`.
Its next qualification run uses the new model. The provider-usage
cache-policy attribution stays limited to `gpt-5.6-sol`.
- Runner E2E history: the Codex cells change their suite definition
hashes, so earlier `gpt-5.6-sol` results are a separate cohort and do
not count as current.
- The `gpt-6.1-sol` scheduled price uses the file's existing `expiresAt`
(2026-11-19). It needs the same review as the other entries before then.
- No `.github/` change. The Reliability queue-steer cell keeps the
`runner-codex` profile id, so the existing workflow credential and setup
steps still match.

> 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 (Anthropic), model ID `claude-opus-5-5`, running in
Claude Code with tool use (shell, file edits, web fetch of the OpenAI
pricing page).

## 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

Related: #19604 (commits the Reliability TestOps identities; this PR
changes only `steeringProfile` in the same file).

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 15:35:34 -06:00
Nicky LeachandClaude Opus 5.5 05a19768ae fix(runner): require normalizedSessionId when validating PRP events (#19615)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The Paperclip Runner reports native session activity as PRP events
> - `validatePrpEvent` checks an event against the published event
schemas and then returns it as a `PrpEvent`
> - The `PrpEvent` type requires `normalizedSessionId`, but the schemas
leave it optional, so the validator accepts events that the type forbids
> - Every producer sets the field, and the consumers compare it to the
expected session
> - This pull request makes `validatePrpEvent` reject an event without
`normalizedSessionId`
> - The benefit is that a validated event now always matches its type

## Linked Issues or Issue Description

No issue exists. #18839 listed this under its product questions and did
not change it. #19495 fixes the other Runner contract types from that
list.

**What existing behavior does this improve?**

`validatePrpEvent` in
`packages/paperclip-runner/src/protocol/replay-contract.ts` returns `{
ok: true, event: PrpEvent }` for an event without `normalizedSessionId`.

**Current behavior**

- The TypeScript `PrpEvent` type requires the field. The Rust event
structs also require it (`normalized_session_id: String`).
- The published schemas (`protocol/schemas/event*.schema.json`) leave it
optional.
- Validated events go into event arrays, reducers and the control-plane
callback, and all of them expect the field. A plain type change to
optional shows 12 such places in the Runner.

**Proposed behavior**

After the schema check passes, `validatePrpEvent` rejects an event that
has no `normalizedSessionId`. The issue has the code `schema_validation`
and the path `/normalizedSessionId`. The schema still checks the field's
type and length when the field is present.

**Reason and benefit**

The validator, the type and both producers then agree. The published
schemas, their manifest hashes and the generated standalone validators
do not change.

**Breaking changes**

An event without `normalizedSessionId` now fails validation. No producer
in this repository creates such an event. The replay fixtures and golden
snapshots include the field.

## What Changed

- `packages/paperclip-runner/src/protocol/replay-contract.ts`:
`validatePrpEvent` requires `normalizedSessionId` after the schema
check.
- `packages/paperclip-runner/src/protocol/replay-contract.test.ts`: new
test. A fixture event passes, and the same event without
`normalizedSessionId` fails with the new issue.

## Verification

- Runner, in `packages/paperclip-runner` after `pnpm run
test:typescript:prep`:
- `pnpm exec vitest run` (the full Runner TypeScript suite): 299 of 309
files passed, and 16 tests in 9 files failed.
- I ran those 9 files again by themselves: 231 of 233 tests passed. The
2 remaining failures were 20-second timeouts in
`src/drivers/cursor-cloud/artifact-delivery.test.ts` and
`src/drivers/cursor-cloud/cursor-cloud-driver.test.ts`.
- I ran those 2 files again by themselves: all 117 tests passed. They
also pass with the master validator. So all 16 failures were load
timeouts, not results of this change.
- `pnpm run check:replay-goldens`: "Replay golden snapshots and parity
summaries are current."
- `tsc --noEmit` passes for the Runner `tsconfig.json`,
`tsconfig.surfaces.json` and `tsconfig.test.json`.
- Server:
  - `cd server && pnpm typecheck`: passes.
- I ran the 29 server test files that import `acknowledged-native-stop`
or `native-session-executor`. 27 files passed.
- `dot-runner.test.ts` failed at first because `cargo` was not on PATH.
With `cargo` on PATH, it passes.
- `heartbeat-process-recovery.test.ts` fails on my machine with
timeouts. These failures are not caused by this change: two of the
failing tests fail in the same way when I use the master
`replay-contract.ts`. No failure output mentions PRP events or
`schema_validation`. CI runs this file.
- `pnpm verify` selects a broad run for this change (exit 2). Required
CI supplies that coverage.

## Risks

- Medium-low risk. This is a runtime change to a protocol validator. The
server validates stored events again (`native-session-executor.ts`,
`acknowledged-native-stop.ts`). A stored event without the field would
now fail validation. The producers that wrote those events always set
the field, so I expect no such rows.
- The Rust event structs already reject an event without the field.

## Model Used

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

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 13:30:26 -08:00
Nicky LeachandClaude Opus 5.5 a7d3d4b78c fix(runner): rotate local and external authority through one resumable path (#19547)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The Paperclip Runner moves a native session from one run to the next
with an authority rotation, for Codex and for every ACPX provider
> - A rotation archives the prior run's controller state under
`authority-epochs/` and archives the Runner state, locally or on an
external Runner host
> - If the run fails after the rotation but before the new Runner
starts, retry preparation restores the prior run's controller checkpoint
into the session root
> - The retry then finds an active `control-plane` next to the archive
marker and fails with `native_runner_authority_archive_conflict`. Every
later run for the session fails the same way
> - The local and external rotations were two copies of the same steps,
with different retry rules, and the sandbox archive script had a third
set of rules
> - This pull request gives every provider and Runner placement one
resumable rotation path, with the same retry rules for controller state
and Runner state
> - The benefit is that one failed startup no longer makes a native
session unusable

## Linked Issues or Issue Description

No public issue exists. Bug description:

**What happened?**
A warm Codex session on an external (plugin) Runner failed its first
attempt after the authority rotation finished. The two automatic retries
both failed with `native_session_recovery_failed: Error:
native_runner_authority_archive_conflict`, and the run stopped. On disk,
the session root had
`authority-epochs/epoch-<key>/control-plane/control-plane-state.json`
and an active `control-plane/control-plane-state.json` with the same
identity (in this case, also the same sha256). The remote Launcher had
already archived the Runner state under the same key.

**Expected behavior**
A retry resumes the interrupted rotation. The rotation already supports
this when only the archive marker exists.

**Steps to reproduce**
1. Run a native Codex or ACPX session on an external Runner.
2. Start a new run for that session. Make the new Runner fail to start
after the rotation finishes.
3. Let the run retry. Retry preparation (`restoreWarmRunnerdController`
/ `restorePerTurnRunnerdController` →
`restoreNativeControllerProjections`) restores the prior run's
controller checkpoint into the session root.
4. The retry fails with `native_runner_authority_archive_conflict`. All
later runs for the session also fail.

The new `authority rotation after the prior controller state is
restored` tests reproduce steps 2–4 directly.

**Paperclip version or commit**
`master` at 35280516c0.

**Deployment mode**
Hosted, with a remote (plugin) Runner environment.

**Agent adapter(s) involved**
Codex (native Paperclip Runner). ACPX providers use the same rotation
code.

## What Changed

- **One rotation path.** `rotateAuthorityEpoch` replaces
`rotateLocalAuthorityEpoch` and `rotateExternalAuthorityEpoch`. The only
difference between Runner placements is a `RunnerStateArchive` (`read`,
`archive`):
- `localRunnerStateArchive(root)` for Runner state under `<root>/runner`
  - `#externalRunnerStateArchive()` for a plugin task or a sandbox host
- **One caller.** The transport chooses the rotation source (the live
control plane, or the latest archive), then makes one call. Locally
owned state is still quarantined on failure.
- **Committed rotation, normalized once.** Retry preparation restores
the prior run's controller checkpoint, but an earlier attempt may
already have archived that authority and its Runner state.
`takeCommittedRotation` runs right after the transport reads the control
plane. When the epoch archive for the active copy's exact identity
exists, it removes the restored copy and continues on the archive-only
path. Every later check (the warm-handoff evidence read, the
source-attachment settlement read, and the rotation) then sees the state
the rotation committed, instead of reading retired live Runner state. A
copy that still holds an unfinished warm transition, staged attachment,
or pending `run.attach` fails closed.
- **Restored controller state.** When the epoch archive exists, an
active control plane with the same schema and the exact archived
identity is a copy of authority that the rotation already retired. The
rotation removes it and resumes from the archived snapshot. This
compares identity, not bytes, because a checkpoint restore serializes
the state again and drops fields such as `ackedSourceSeq`. Any other
active state still fails closed.
- **Duplicate Runner state.** Local Runner state now follows the
computer Launcher's rules: move it, accept it already archived, drop a
byte-identical live copy beside the archive, or fail closed. The local
archive also checks that the live state is suspended before it moves it.
- **Sandbox archive script.** The script is now
`REMOTE_RUNNER_AUTHORITY_ARCHIVE_SCRIPT` and follows the same rules. It
uses `cmp` for the duplicate check and fails closed when `cmp` is not
available.
- **Sandbox host preparation after a resumed rotation.** A resumed
rotation archives the remote Runner state without preparing the host
first. When `archiveExternalRunnerState` succeeds, the server now
records the archived snapshot path. The launch's host preparation
(`inspectRemoteHarnessState`) validates that snapshot instead of the
missing live `runner-state.json`, with the same provider-state and
identity checks. Only these host preparation checks use the archived
snapshot; close checkpointing and backup-restore verification always
inspect the live `runner-state.json`. The SSH staging check treats it as
present, so retired state is not staged back.
- **Host preparation.** The external archive prepares its host only when
it reads live Runner state. Before, only one of the two caller branches
prepared the host. A resumed rotation finds the state already archived,
and preparing again could stage it a second time.

## Verification

- `npx vitest run src/live/runnerd-codex-transport.test.ts -t
"rotation"` in `packages/paperclip-runner`: 16 passed.
- External and local rotations with a byte-identical, a re-serialized,
and an other-authority restored control plane. The first two resume from
the archived snapshot. The third fails closed and keeps the copy.
- Local rotation with an identical duplicate Runner state (resumes) and
a different duplicate (fails closed and keeps it).
- The existing crash-before and crash-after remote archive retry cases.
- Before the source change, the byte-identical external case failed with
`native_runner_authority_archive_conflict`.
- `npx vitest run src/live/runnerd-codex-transport.test.ts -t "transport
retry after a committed rotation"` in `packages/paperclip-runner`: 4
passed. These go through the transport's real start path. A completed
warm handoff and a source attachment hook now resume from the archive;
before the normalization, both read the archived live Runner state and
failed. An unfinished warm transition and a pending attachment fail
closed; before, the pending attachment was silently dropped.
- `npx vitest run
src/services/native-runtime/native-session-executor.test.ts -t "remote
authority archiv"` in `server`: 6 passed. Five new cases run the real
archive script with `sh` against temporary files: a new archive, an
existing archive, an identical duplicate, a different duplicate, and no
state. The existing mocked archive test also passes.
- `npx vitest run
src/services/native-runtime/native-session-executor.test.ts -t "resumed
rotation archived"` in `server`: 2 passed. A resumed sandbox with a
retained backup goes through the real archive script and host
preparation. It failed with `runner_harness_state_mismatch:
resumed_unavailable` before the host preparation fix. A settled close
checkpoint with no live Runner state then fails closed with
`runner_remote_checkpoint_incomplete`, rather than checking the archived
snapshot. Without a completed rotation, a missing live state still fails
closed.
- `pnpm run typecheck:typescript` in `packages/paperclip-runner` and
`node scripts/typecheck-layers.mjs` in `server`: pass.
- The full `runnerd-codex-transport*.test.ts`,
`cursor-cloud-runner-launch.test.ts`, and adoption test files fail on
the same 110 tests with and without this change. These tests need the
built `paperclip-runnerd` binary, which needs `cargo`. `cargo` is not
installed locally, so CI covers them.

## Risks

- The change is only on the rotation path. A rotation that has no
archive marker behaves as before: it reads and checks live Runner state,
then archives the control plane.
- A rotation removes an active control plane only when an archive for
that exact identity exists and the active copy has the same schema and
identity. A new run's authority has a different identity, so it cannot
match.
- On a resume, the rotation now uses the archived controller snapshot,
not the caller's copy. The archive is the exact pre-rotation state. A
checkpoint restore can omit fields from it.
- An external Runner whose options define `readRunnerState` and
`archiveExternalRunnerState` but not `prepareExternalRunnerState` can
now rotate a live control plane. Before, that branch required the
prepare hook. The plugin and sandbox drivers define all three.
- The sandbox duplicate check needs `cmp`. A host without `cmp` fails
closed as before.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code with tool use (shell, file edits, test runs). It diagnosed the
failure from the run events, the server logs, and the on-disk controller
and Launcher state, then wrote the fix and the 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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 13:27:55 -08:00
github-actions[bot]andlockfile-bot e28158b353 chore(lockfile): refresh pnpm lockfiles (#19617)
## Thinking Path

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

## Linked Issues or Issue Description

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

Run a frozen install before and after this lock refresh.

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

Co-authored-by: lockfile-bot <lockfile-bot@users.noreply.github.com>
2026-11-06 21:08:43 +00:00
DottaandPaperclip b55d8a9201 fix(runner): retain admitted OpenCode pack on ordinary continuation (#19558)
## Thinking Path

> - Paperclip manages AI agents and preserves their work across runs.
> - Native OpenCode turns can restore a saved provider session after the
prior daemon stops.
> - The Rust provider binds that saved state to exact qualified launch
paths and artifact bytes.
> - Ordinary continuation selected the current provider pack, although
live reattachment retained the admitted pack.
> - A new proxy or a diagnostics setting could therefore replace the
launch profile behind saved state.
> - This pull request retains the recorded pack for ordinary OpenCode
continuation with a verified durable binding.
> - The benefit is compatible continuation without weakening artifact
integrity or granting replay authority.

## Linked Issues or Issue Description

Refs: #17512, #18237. These added saved authority and compatible pack
reuse. This change covers ordinary OpenCode continuation.

**What happened?**

An ordinary remote OpenCode continuation could select a newer pack while
restoring its existing provider session. The Rust recovery guard then
rejected the changed launch profile. The same error can occur after a
controller release changes the bundled proxy.

**Expected behavior**

A continuation with a valid saved binding and pack receipt uses the same
admitted artifacts. If those artifacts are unavailable, it holds the
saved state instead of selecting another pack or creating a provider
session.

**Steps to reproduce**

1. Admit a remote OpenCode session with a qualified protected provider
pack.
2. Persist its pack receipt and suspended durable Runner state.
3. Change the controller bundle to a valid pack with a different proxy
digest.
4. Start an ordinary continuation with diagnostics enabled.

The new executor regression fails before the repair because selection
rejects the available original pack in favor of the new bundle. It
passes after the repair.

## What Changed

- Retain the saved provider pack for ordinary OpenCode continuation with
a verified durable session binding.
- Keep provider-pack retention independent of surviving-daemon identity:
a new daemon records its current binary, and an authorized continuity
break restores normal compatible pack selection.
- Test retained pack selection after a proxy upgrade and fail closed
when only a newer pack is available.
- Test the real Rust executor with changed, valid proxy file bytes. It
rejects drift without changing saved state and accepts the original
artifacts again.
- Document the selection rule and its limits.

## Verification

- Initial prevention focused Vitest: 220 passed across native session
execution, pack authority, pack verification and pack preparation. The
executor regression was observed failing before the production change.
- Rust native provider tests: all five OpenCode tests passed. These
include the existing drift guard, actual changed proxy bytes, original
artifact restoration and restart-lost turn behavior.
- Advisory review follow-up: reproduced stale daemon identity in the
actual executor and incompatible pack selection after a governed
continuity archive. Both fail before their corresponding repairs and
pass after them. Added an executor regression for a missing authority
receipt; it remains at the current release integrity gate without
creating a new receipt.
- Updated head `639c080856` integrates master `17901d74ba` by normal
merge. Affected Vitest: 202 passed across four files, including all new
review regressions; all seven server typecheck layers passed. Exact-head
CI and fresh Greptile/Paperclip review are pending. The prior head had
57 successful checks plus successful Snyk and Greptile 5/5; those
results do not qualify this revision.
- Environment: macOS arm64, Node 26.4.0, locked pnpm 9.15.4
dependencies, controlled remote command boundary and real Rust provider
executor. No paid provider calls or hosted retries.
- This is a bounded prevention change. It does not deliver historical
provider artifacts to a replacement sandbox or replacement controller.
Missing saved receipts or unavailable old artifacts remain a separate
cold-recovery gap. No full hosted recovery is claimed.

## Risks

- A continuation now requires its exact recorded pack when it has a
durable binding. A newer installed pack cannot substitute for missing
historical bytes.
- Fresh launches, other providers and explicit continuity replacement
keep their existing paths. Current ownership, company, model, permission
and receipt binding checks remain in effect.
- The repair does not relax the Rust launch-profile guard, replay prior
tool effects, or authorize a new provider conversation. Existing
published review receipts remain history.

## Model Used

OpenAI Codex, GPT-6 family. The host does not expose the exact model ID
or context window. Capabilities used: reasoning, code editing, shell
execution, tests and authenticated read-only browser diagnostics.

## 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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 14:21:17 -06:00
DottaandClaude Opus 5.5 24508a8ba5 perf(dev): add agent preflight with a shared content-keyed build cache (#19555)
perf(dev): add agent:preflight with a shared content-keyed build cache

Agent worktrees each built the Runner from nothing: a cold cargo debug and
release build, the Runner TypeScript build and its checks, about 8 minutes
per fresh worktree on a loaded machine.

`pnpm agent:preflight` replaces the manual install + Runner build sequence.
It skips steps whose inputs are unchanged and reuses other worktrees'
results from ~/.cache/paperclip/agent-build:

- Runner and eval-kernel dist, keyed by the Runner build-input digest and
  the toolchain
- Runner debug binaries, keyed by every file the Rust build reads
- a cargo seed with third-party crates only; the Runner crate is scrubbed
  because cargo records its out-of-package includes by absolute path

Entries are written once with an atomic rename and restored as APFS clones.

Also exclude provider-assets from the Runner build inputs. The build writes
that directory itself, so the first build in every fresh checkout saw its
inputs change and never recorded a build stamp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 14:01:54 -06:00
DottaandClaude Opus 5.5 86e9c9354d test(opencode): pin Stop during a pending MCP tool call (#19550)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The native runner runs OpenCode agents and settles their cost after
each run
> - An operator can stop a run while the agent waits inside a Paperclip
MCP tool call
> - Before #18610, OpenCode then kept calling the model after the Stop.
The turn never closed, so the run cost never settled
> - #18610 fixed the cause, but no test covers a Stop during a pending
MCP tool call
> - This pull request adds that regression test at the driver boundary
> - The benefit is that a change to empty-answer recovery or
interruption cannot bring back runaway model work after a Stop

## Linked Issues or Issue Description

Refs #18610 (the fix that this test pins)
Refs #19497 (accounting that waits on a terminal run receipt)

**What happened?**

A board operator stopped a native OpenCode run. The agent was blocked
inside a gated `paperclip_call_assigned_tool` call. The runner
acknowledged the interrupt, but OpenCode made 8 more model requests in
the next 60 seconds. No `turn.cancelled` event arrived. The run stayed
at `costAccountingPending: true` and `accountingReceiptReady: false`, so
cost accounting never settled.

**Expected behavior**

A Stop aborts the provider. The turn closes with `turn.cancelled`. The
provider makes no new model requests. Cost accounting then settles from
the final receipt.

**Steps to reproduce**

1. Start an OpenCode turn with a dynamic Paperclip tool whose handler
does not return.
2. Let the provider call that tool through the MCP bridge.
3. Fail the pending call (runnerd does this first on Stop), then
interrupt the turn.
4. Let OpenCode report the abort as a bare `session.idle`.
5. Before #18610, the driver sent a corrective "give a final answer"
prompt. The provider then continued work.

**Root cause**

The driver sent `/session/:id/abort` correctly. OpenCode 1.18.34 aborts
correctly, and sometimes reports the abort only as `session.idle`.
Empty-answer recovery treated that idle as an incomplete answer and sent
a new prompt. #18610 added `#interruptedTurnId`, so recovery now stops
for an interrupted turn. The failing eval ran on `3b66c3c144`, which is
earlier than #18610.

## What Changed

- Add
`packages/paperclip-runner/src/drivers/opencode/interrupt-pending-tool-call.test.ts`.
It calls the real MCP bridge over HTTP, holds the dynamic tool call
open, and interrupts the turn. It checks three abort reports:
- `idle-after-settle`: runnerd fails the call, then the interrupt, then
a bare idle.
- `idle-before-settle`: an idle while the call is still pending, then
the late failure, then an idle.
  - `aborted-error`: `MessageAbortedError`.
- Each case asserts one prompt only, exactly one terminal event
(`turn.cancelled`), and no empty-response correction diagnostic.
- No product code changes. Master already has the fix.

## Verification

- `pnpm --filter @paperclipai/paperclip-runner exec vitest run
src/drivers/opencode/interrupt-pending-tool-call.test.ts`: 3 passed on
this branch.
- The same test at `3b66c3c144` (before #18610): `idle-after-settle` and
`idle-before-settle` fail with 2 prompts (the corrective prompt).
`aborted-error` passes. This matches the eval symptom.
- `vitest run src/drivers/opencode/`: 14 files, 185 tests passed.
- `tsc -p tsconfig.test.json --noEmit` for the runner: no new errors. A
pre-existing error in `src/eval/workflow-harness.ts` comes from an
unbuilt local eval-kernel package.
- `server` `native-stop-accounting.integration.test.ts`: 5 passed. This
confirms that settlement completes after a Stop when the terminal event
and final receipt arrive.
- Manual, not committed: the same flow with the real OpenCode 1.18.34
binary, a mock OpenAI-compatible model, and a gated tool. I tested it
through the driver alone and through runnerd, the proxy, and the driver.
The interrupt produced `turn.cancelled`, and the model received no more
requests. I also tried different gate-release and model-latency timings.
- The new test waits for the terminal event with a bounded
`expect.poll`, not a fixed delay (review feedback).
- `pnpm verify` exited 2 (broader verification required) because the new
test file is outside the resolved test graph. CI supplies the broad
coverage.

## Risks

Low risk. This change adds one test file only. The test uses the
existing fake OpenCode fixture and an injected `fetch`. It uses real
timers with short waits.

> 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 (Anthropic), model ID `claude-opus-5-5`, used in
Claude Code with tool use (shell, file edits, code execution) 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-06 13:23:24 -06:00
TonioandPaperclip c650f36007 fix(ui): refine agent profiles, identity editing, and list actions (#19535)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Agent pages expose identity files, reporting relationships, and
runtime settings.
> - Several controls had inconsistent spacing, weak navigation, or
misleading save feedback.
> - Empty task-row detail slots also reduced the title width before
truncation.
> - This pull request fixes those behaviors and aligns the reviewed UI
details.
> - The result is clearer agent editing and more readable task and agent
lists.

## Linked Issues or Issue Description

**What happened?**
Agent headers and file controls had inconsistent layout. The Identity
footer appeared without edits and did not provide clear save
confirmation. File names could wrap or omit the Markdown suffix. Task
titles truncated while empty detail slots occupied the row.

**Expected behavior**
Keep headers and actions aligned. Reveal the Identity footer only for
changes or save feedback. Use valid Markdown file names and actual file
dates. Let titles use available space.

**Steps to reproduce**
1. Open an agent overview, then its Identity tab.
2. Edit, undo, save, copy, and add an instruction file.
3. Resize the file panel and inspect file names and date labels.
4. Hover an agent row and inspect the Chat action and tooltip.
5. Open a task list with no recovery or external details and inspect
title width.

Related: #18716 is the prior UI bundle. #16934 and #14483 concern role
editing; this change does not add a role picker.

## What Changed

- Refine the character header, inline title and name editing, crown
position, runtime shortcut, and Chat/New task actions.
- Align profile metadata and manager selection; hide empty direct
reports.
- Refine Identity panels, Markdown filename validation, file-row hover,
truncation, Paper Mono typography, and copy feedback.
- Add a viewport-bound save footer with dirty, saving, and saved states.
Keep undo/discard separate from successful saves.
- Return recorded creation or modification dates for instruction files.
Reuse the active file transaction for metadata reads to avoid exhausting
the small connection pool.
- Reveal the agent-row Chat icon and label on hover, with a standard
delayed tooltip.
- Collapse empty task-row details so titles can use the available width.

## Verification

- Browser checks covered the local overview and Identity flows, narrow
file panels, successful file creation/update, undo/discard, header
navigation, row hover, tooltip delayed-open state, and full task titles.
- Focused instruction, save-footer, date, font-asset, and task-row tests
passed during development. After review, 55 file/instruction tests
passed, including cold restoration with recorded and unknown dates;
server typecheck and 18 immediate-save response tests also passed.
- Full `pnpm -r typecheck` and `pnpm build` passed after rebase onto
current master. The complete `pnpm test:run` attempt hit a 15-second
timeout in the existing chat Retry HTTP integration test; that case
passed in isolation (1 test passed). A second full attempt hit timeouts
in two other chat integration cases (GitHub webhook draining and
delivery retry budget); both passed in isolation without increasing
timeouts. All 55 remote checks passed on f31405a730, including the full
test matrix, build, typecheck, and final verification gate.
- `pnpm check:token-gates`, `node scripts/check-maps.mjs`, and `git diff
--check` passed on the submitted revision.

## Risks

- File dates are optional. Managed files require recorded dates because
restored projections have synthetic filesystem timestamps. External
files can use modification time; unknown dates are omitted.
- Save-footer timing depends on animation completion; reduced-motion and
failed-save paths have tests.
- Global Paper Mono typography can change text widths. Browser checks
covered the edited surfaces.
- No schema migration or new execution permissions are introduced.

## Model Used

OpenAI Codex, GPT-6, with repository tools, code execution, and browser
verification. The exact serving model ID and context-window size 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 references)
- [x] My branch name describes the change and contains no internal
Paperclip ticket id or instance-derived details
- [x] I have run the relevant tests locally and they pass; full-run
timeout limitations and successful isolated reruns are documented above
- [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-06 11:14:56 -08:00
Nicky LeachandClaude Opus 5.5 7dfdc97b9f fix(deps): let Dependabot update the sandbox providers (#19489)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Sandbox providers (`packages/plugins/sandbox-providers/*`) are
standalone published packages. `pnpm-workspace.yaml` keeps them out of
the root workspace on purpose.
> - `.github/dependabot.yml` watches only `/`, so Dependabot never saw
the providers.
> - Because of this, the Kubernetes provider stayed on zod 3
(GHSA-mrhx-c7xc-m3w5) after the rest of the repo moved to zod `^4.6.6`.
#18839 fixed it by hand.
> - We do not want to keep these versions current by hand in many
places.
> - This pull request lets Dependabot update the providers, and keeps
each provider standalone.
> - The benefit is that a provider dependency that falls behind gets a
pull request that can pass CI.

## Linked Issues or Issue Description

Refs #18839 (adds `scripts/typecheck-sandbox-providers` and fixes
Kubernetes zod by hand).
Refs #18998 (a Dependabot pull request for the Pi distribution that
fails CI by design).

**What existing behavior does this improve?**
Dependabot version updates. They skip every package that the root pnpm
workspace excludes.

**Subsystem affected**
CI and dependency automation: `.github/dependabot.yml`,
`scripts/typecheck-sandbox-providers`.

**Current behavior**
Dependabot does not update the sandbox providers. A provider can stay on
an old or unsafe version, and nobody gets a pull request.

**Proposed behavior**
Dependabot updates the providers each week. Each pull request changes
the provider manifests and the lockfiles that CI installs frozen, so the
pull request can pass CI.

**Reason and benefit**
We stop having to find and fix old provider dependencies by hand.

## What Changed

- `.github/dependabot.yml`: add an npm entry for
`/scripts/typecheck-sandbox-providers`. Dependabot treats this workspace
as one unit. One pull request per dependency changes every provider that
uses the dependency and the shared `pnpm-lock.yaml` together. So the
entry needs no `groups`. One entry per provider would open a separate
pull request for each provider, and each one would fail the frozen
install in CI.
- `.github/dependabot.yml`: add an npm entry for
`/packages/plugins/sandbox-providers/daytona`. Daytona is the only
provider with its own lockfile, and the release build and the Daytona
end-to-end job install it frozen. Each lockfile has exactly one
Dependabot entry, so every pull request changes a manifest and its
lockfile together.
- Take Daytona out of the shared typecheck workspace. This pull request
does not commit `scripts/typecheck-sandbox-providers/pnpm-lock.yaml`:
since #19522, only the lockfile refresh workflow writes it. After merge,
the bot removes Daytona's packages from it (no other version changes;
checked with pnpm 9.15.4). Until then CI resolves it in place.
`scripts/typecheck-test-programs.mjs` now installs Daytona from its own
lockfile (`LOCKED_PROVIDERS`, with `--ignore-workspace`). A stale
lockfile is resolved in place for the run, for both installs.
- `.github/dependabot.yml`: add a comment that tells why the Pi and Kimi
runtime distributions have no entry (see Risks).
- `scripts/typecheck-sandbox-providers/pnpm-workspace.yaml`: list each
provider by its full path. Dependabot does not expand a workspace glob
that starts with `../`, so with the glob it saw no providers.
- `scripts/typecheck-test-programs.test.mjs`: add tests. They check that
the workspace list has no glob and matches `SANDBOX_PROVIDERS` without
`LOCKED_PROVIDERS`, that each locked provider has its lockfile, that a
locked provider installs frozen with `--ignore-workspace`, and that a
stale locked-provider lockfile is resolved in place.
- Add `.npmrc` (`auto-install-peers=true`) in
`scripts/typecheck-sandbox-providers` and in the Daytona provider.
Without it, Dependabot reads the root `.npmrc` (`false`). That rewrites
the lockfile settings, and the frozen install then fails. npm and pnpm
never publish `.npmrc`.
- `scripts/typecheck-sandbox-providers/package.json`: add
`"packageManager": "pnpm@9.15.4"`, the version CI uses. Without it,
Dependabot guesses pnpm 10.
- `doc/DEVELOPING.md`: say that contributors do not refresh or commit
Daytona's lockfile either, and that a new provider goes in both lists.

No provider moves into the root workspace. There is no pnpm catalog. The
root `pnpm-lock.yaml` does not change.

## Verification

I ran the real Dependabot updater on this branch with the Dependabot CLI
(`dependabot update ... --local`, updater image
`dependabot-updater-npm@sha256:c5a9451c…`):

- Before the change, the workspace entry loaded only
`cloudflare/bridge-template` (the one path without a glob). It skipped
all the other providers.
- After the change, `--dep typescript` gave one pull request. It changed
the 9 non-Daytona provider `package.json` files and the shared
`pnpm-lock.yaml`, and did not touch Daytona.
- `--dep wrangler` changed only the wrangler closure in the shared
lockfile. Before the `.npmrc` fix, the same run changed
`autoInstallPeers` to `false` and 757 lockfile lines.
- The Daytona entry (`--dep @daytonaio/sdk`) changed Daytona's
`package.json` and its own lockfile in one pull request, and kept
`autoInstallPeers: true`. It keeps the exact pin exact: `0.203.0`
becomes `0.227.3`.
- Both frozen installs (shared workspace, and Daytona with
`--ignore-workspace`) pass with pnpm 9.15.4.
- Dependabot installed `pnpm@9.15.4` from the new `packageManager`
field.

Other checks:

- `.github/dependabot.yml` passes the SchemaStore `dependabot-2.0`
schema (Ajv). A wrong key fails the same check.
- With pnpm 9.15.4, `install --resolution-only --ignore-scripts
--no-frozen-lockfile` in `scripts/typecheck-sandbox-providers` (the
lockfile bot's command) gives exactly the lockfile that passed the
frozen installs earlier in this pull request.
- The lockfile rule from #19522 allows this pull request's diff: it
changes no lock content.
- `node --test scripts/typecheck-test-programs.test.mjs`: the new test
and the provider tests pass. Two other tests need a root `node_modules`
(`tsc`), which my worktree did not have. CI runs them.

These checks cannot run before merge. GitHub reads `dependabot.yml` only
from the default branch. After merge, open Insights → Dependency graph →
Dependabot and make sure that the two new entries show no errors and run
on Monday.

## Risks

- A dependency in both a non-Daytona provider and Daytona (for example
`typescript`) gets two pull requests: one from each entry. Each one
passes alone, and they can merge in any order.
- The typecheck job now typechecks Daytona's tests against Daytona's own
lockfile instead of the shared one. Its versions are a little older (for
example vitest 4.1.11), so a type difference can show up there.
- Majors of shared dev tools (for example TypeScript 7, Vitest 5) come
as one pull request across all providers. It can need code changes
before it passes. That is the same as the root entry.
- Pi and Kimi get no version updates. Every direct dependency there is a
pinned provider release, and the pins feed the provider's ACPX
`commandDigest`. The Runner checks compare the Pi lock with
`PI_DISTRIBUTION_PINS`, and the Kimi build compares the lock SHA-256
with a pin. So any Dependabot change there fails CI (see #18998), and a
real upgrade starts saved sessions fresh. GitHub security alerts still
cover both lockfiles.
- The Pi zod `3.25.76` pin is still affected by GHSA-mrhx-c7xc-m3w5
(open alert). It is deliberate: `pi-acp@0.0.33` has `zod ^3.25.0` as a
direct dependency, not a peer. A fix needs a `pi-acp` release that
allows zod 4. This pull request does not change it.
- `packages/plugins/examples/plugin-orchestration-smoke-example` is also
outside the root workspace. It uses `workspace:*` dependencies, so it
cannot install alone, and this pull request does not cover it.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code with tool use (shell, file edits, local Docker runs of the
Dependabot CLI).

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 11:08:39 -08:00
DottaandClaude Opus 5.5 71313207eb fix(runner): continue after a restore-lost turn the checkpoint never bound (#19539)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native Codex and ACPX runs survive control-plane restarts: the
runner restores the provider conversation, and the control plane
continues the task from its checkpoint
> - When the runner restores a conversation after process loss, it
reports the turn that was active as lost on restore
(`provider_turn_lost_on_restore`)
> - A controller can die after the provider accepted a turn but before
that turn reached the checkpoint. Then the next recovery does not know
the turn that the runner reports as lost
> - In a restart chaos wave with two hard kills about 20 s apart, this
made the second recovery fail with "Provider turn terminal did not name
the active turn". The session close then became unrecoverable
> - This pull request lets recovery continue: the transport no longer
stalls a new turn start behind that report, and the Codex session
records the report as restored history
> - The benefit is that a task continues automatically after
back-to-back restarts, instead of failing and needing an operator

## Linked Issues or Issue Description

**What happened?**
A Codex task was resumed after a hard restart. A second hard restart
came about 20 s later. The second recovery failed at once with
`NativeProviderTerminalFailure: Provider turn terminal did not name the
active turn.`, and automatic recovery stopped. The session close could
not settle, which quarantined later work (that blast radius is fixed
separately in #19532).

**Steps to reproduce**
1. Start a native Codex task. Kill the control plane (SIGKILL) shortly
after the provider accepts a turn and before the next checkpoint is
saved.
2. Let restart recovery resume the run. Kill the control plane again
while the resumed run is active.
3. Observe the second recovery.

Durable evidence from the wave (runner journal of the run):
- Restart 1 (17:32:34.568): the provider accepted continuation turn T2.
The controller was killed 170 ms later, before T2 reached the
checkpoint.
- Recovery 1 started a new turn T3. The runner command result named T3
(seq 15). The restored runner then reported T2 as lost on restore (seq
20) before `turn.started` for T3 (seq 23). The transport held the T2
terminal while the T3 start was pending, and stopped pumping at it. So
T3 ran in the provider (items, a tool call) but the control plane never
accepted or checkpointed it.
- Restart 2: recovery found no active turn. The restored runner reported
T3 as lost on restore. The Codex session had never bound T3, so it
failed the protocol (`turn_binding_mismatch`).

**Expected behavior**
Recovery continues the task after each restart (R1).

**Actual behavior**
The run failed with a protocol error. Its close could not prove
settlement, so cleanup needed an operator.

## What Changed

- `runnerd-codex-transport.ts`: while a turn start is pending, the event
pump holds a terminal only when the terminal can belong to that turn.
When the durable command result names the new provider turn and the
terminal names a different turn, the pump delivers it and continues to
the new `turn.started`. A terminal that names no turn, or any terminal
before the command result, is still held.
- `codex-session-notifications.ts`: a `turn/completed` with status
`failed` and a recoverable `provider_turn_lost_on_restore` error, for a
turn that this session never bound (not active and not terminal), is
recorded as restored history with a `harness.diagnostic`
(`unbound_restored_turn_lost`). The session keeps its own turn. The
runner reports this cause only after it proved that the old provider
process ended and restored the conversation, so that turn can produce no
further work or result.
- Other terminals for an unbound turn (a completion, an ordinary
failure) still fail with `turn_binding_mismatch`.

## Verification

- Red before the fix, real `paperclip-runnerd` and fake Codex app
server: `pnpm --filter @paperclipai/paperclip-runner exec vitest run
src/live/runnerd-restart-continuation.test.ts -t "never bound"` failed
with `NativeProviderTerminalFailure: Provider turn terminal did not name
the active turn.` at `codex-harness-session.ts:207` via
`native-session-runtime.ts` (the same stack as the wave).
- Green after the fix: that test completes the run on `provider-turn-2`
with no `session.failed` and no `turn.failed`.
- New driver tests in `codex-app-server-driver.terminal-replay.test.ts`:
the restore-lost unbound turn is recorded (also on replay) and the
session's own turn completes. A completion or ordinary failure for an
unbound turn still fails the protocol.
- New transport unit test for the hold rule (`does not hold a pending
turn start behind another turn's restore-lost terminal`).
- Existing suites pass: `src/drivers/codex/`,
`src/native-session-runtime.test.ts`,
`src/live/runnerd-restart-continuation.test.ts`,
`src/live/runnerd-controller-outage.test.ts`,
`src/live/runnerd-codex-transport-warm-attachment.test.ts`,
`src/live/runnerd-codex-transport.test.ts` (turn-start cases). Three
tests timed out once at host load 51 and passed when rerun alone.
- `pnpm --filter @paperclipai/paperclip-runner typecheck:typescript`
passes.
- Live replay of the wave task: results are added in a comment below.

## Risks

- The transport change applies only when the command result already
named the new turn and the terminal names a different turn. A terminal
for the new turn keeps the existing fence that puts `turn.accepted`
first.
- The session change accepts only the runner's restart-interruption
cause. A lost turn's work is already in the restored conversation. The
continuation turn tells the agent to reconcile unfinished work.
- No profile, schema or pack contract changes. ACPX sessions that use
the same Codex harness session get the same behavior.

## 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-06 13:02:25 -06:00
DottaandClaude Opus 5.5 d4d3383c97 fix(runner): scope cleanup quarantine to the session's task and conversation (#19532)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs (Codex, ACPX) execute through the runner's
`executeNativeSession`, which gates each new session behind cleanup of
earlier sessions
> - When a session close fails in a way that needs operator recovery
(`NativeSessionCloseUnrecoverableError`), the runner keeps a quarantine
entry for it
> - The admission gate keyed that quarantine by `[companyId, backend
kind, backend name]`, so one bad close blocked every later run of the
same backend in the whole company
> - In a restart chaos wave (two hard kills about 20 s apart), one Codex
session's close became unrecoverable. After that, every Codex run and
every Retry in the company failed in about 1 s with
`native_session_cleanup_quarantined`
> - This pull request scopes the gate to the resources that a retained
owner can still hold: its task (issue) and its provider conversation
(normalized session)
> - The benefit is that one failed close no longer stops unrelated
tasks, while the task and conversation that are really at risk stay
protected

## Linked Issues or Issue Description

**What happened?**
After two hard control-plane restarts about 20 s apart, one Codex
session close became unrecoverable. From then on, every Codex execution
in the company failed at admission with
`native_session_cleanup_quarantined: prior session cleanup requires
operator recovery…` (`retryQuarantinedSessionCleanups`). Board Retries
for four unrelated tasks were accepted and then failed at once.

**Steps to reproduce**
1. Run two native Codex tasks in one company (local runner).
2. Make one session's close throw `NativeSessionCloseUnrecoverableError`
(in the wave: a protocol failure after a second hard kill, then an
unsettled runner suspension).
3. Start or retry a different Codex task in the same company.

**Expected behavior**
Only work that shares a resource with the retained owner waits. Other
tasks run normally.

**Actual behavior**
Every later Codex run in the company fails within about 1 s, until the
server process restarts.

## What Changed

- `packages/paperclip-runner/src/native-session-runtime.ts`: the cleanup
domain is now `{ key, resources }`. `key` is the old backend key
(company, backend kind and name, optional remote cleanup scope).
`resources` are the session's issue and its normalized session (or its
run when it has no normalized session).
- The admission gate (`retryQuarantinedSessionCleanups`) and its
retained close owners block a new session only when the backend key
matches and at least one resource matches.
- The exact retirement paths (`completeRetainedNativeSessionCleanup`,
`completeTerminatedRemoteNativeSessionCleanup`,
`completeTerminatedLocalNativeSessionCleanup`) read the unchanged
backend key, so their behavior does not change.
- New regression test: an unrecoverable close on session A does not
block session B of another task. A session with the same issue, or with
the same normalized session, stays blocked.

Why this scope is safe: the resources that a retained owner can still
hold are its runner state root and its workspace. The server derives the
runner state root from the session scope (company, agent, workspace,
provider, normalized session). The workspace belongs to the task.
Different tasks already run concurrently on the same backend, so a
different task shares no resource with the retained owner that normal
concurrency does not already share.

## Verification

- Red before the fix: `pnpm --filter @paperclipai/paperclip-runner exec
vitest run src/native-session-runtime.test.ts -t "unrelated tasks of the
same backend"` failed with `native_session_cleanup_quarantined` for the
unrelated task (the RC-20 error).
- Green after the fix: the same test passes. The full file passes
(211/211).
- `pnpm --filter @paperclipai/paperclip-runner typecheck` passes.

## Risks

- A retained owner can no longer block a different task that shares a
project workspace. Concurrent tasks in a shared workspace are already
allowed, so this adds no new kind of overlap.
- The same task and the same conversation stay blocked. A Retry of the
task whose close failed can still be accepted and then fail at
admission. The trigger of that close is handled separately.

## 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-06 12:39:31 -06:00
github-actions[bot]andlockfile-bot 17b4f972bf chore(lockfile): refresh pnpm lockfiles (#19534)
## Thinking Path

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

## Linked Issues or Issue Description

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

Run a frozen install before and after this lock refresh.

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

Co-authored-by: lockfile-bot <lockfile-bot@users.noreply.github.com>
2026-11-06 18:09:43 +00:00
DottaandClaude Opus 5.5 30a0b5982e fix(runner): let Codex agents generate images with their existing provider key (#19516)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The Paperclip runner launches Codex agents in an isolated app-server
with a sandboxed shell
> - On the Codex API path, the agent's OpenAI API key reaches only the
trusted app-server process, never the shell
> - The runner launched Codex with `--disable image_generation`, so
Codex's built-in image tool was not available
> - Codex then falls back to its `image_gen.py` script, which needs
`OPENAI_API_KEY` in the shell, so the agent asked the user for a second
key
> - This pull request keeps Codex's built-in image tool on, so it uses
the agent's existing provider auth, and lets the sandbox read only the
folder where Codex saves the images
> - The benefit is that a Codex agent can make images without a second
key, and the provider key still does not reach the shell

## Linked Issues or Issue Description

**What happened?**
Manual onboarding test on Paperclip Cloud staging (Codex API path):
"Open AI requests another api key for image model even though it has an
active api key powering the agent."

**Steps to reproduce**
1. Create a company with a Codex agent that uses an OpenAI API key.
2. Ask the agent to generate an image.
3. The agent asks for another OpenAI API key.

**Expected behavior**
The agent uses the OpenAI API key that already powers it.

**Root cause**
`createIsolatedCodexAppServerArgs` passed `--disable image_generation`
and the thread config set `features.image_generation: false`. The pinned
Codex 0.160.0 binary has two image paths. Its own instructions say the
built-in `image_gen` tool "Does not require `OPENAI_API_KEY`". The
fallback CLI path "Requires `OPENAI_API_KEY`". The runner removes
`OPENAI_API_KEY` from the shell on purpose
(`codex-shell-environment.ts`), so the fallback can never work and the
agent asks the user.

## What Changed

- `codex-security-config.ts`: remove `--disable image_generation` and
`features.image_generation: false`. Codex's built-in tool runs in the
trusted app-server with the agent's existing auth.
- `codex-security-config.ts`: in the workspace and plan permission
profiles, grant **read** on `CODEX_HOME/generated_images` only. Codex
saves images there and tells the agent to copy them into the workspace.
The rest of `CODEX_HOME` (for example `auth.json`) stays denied.
Preconfigured-home mode is unchanged (it does not deny `CODEX_HOME`).
- `eval-gateway-runtime.ts`: set `image_generation = false` in the
controller-gateway Codex config. That gateway admits no hosted tools
(`unqualified_hosted_tool`), so qualified evals keep the earlier
behavior.
- Tests: two new N-12 tests, and updates to the tests that asserted the
old disable flag.

## Verification

Red on `master` (819420a60a), same test file before the source change:

```
     × N-12: lets an API-key Codex agent generate images with its existing provider key instead of asking for another 5ms
     × N-12: the pinned Codex binary keeps built-in image generation enabled under the isolated launch 4400ms
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯
 FAIL  src/drivers/codex/codex-security-config.test.ts > Codex security configuration > N-12: lets an API-key Codex agent generate images with its existing provider key instead of asking for another
AssertionError: expected [ '-c', …(22) ] to not include 'image_generation'
    378|     expect(args).not.toContain("image_generation");
 FAIL  src/drivers/codex/codex-security-config.test.ts > Codex security configuration > N-12: the pinned Codex binary keeps built-in image generation enabled under the isolated launch
AssertionError: expected 'image_generation                     …' to match /\strue\s*$/
+ Received:
"image_generation                         stable             false"
      Tests  2 failed | 17 skipped (19)
```

Green after the fix:

```
      Tests  2 passed | 17 skipped (19)
```

Neighbouring suites (`src/drivers/codex`,
`src/cli/eval-gateway-runtime.test.ts`, `src/provider-events.test.ts`):

```
 Test Files  25 passed (25)
      Tests  429 passed (429)
```

Real sandbox check with the pinned Codex 0.160.0 binary (`codex sandbox`
with the generated permission profile, macOS seatbelt):

```
generated_images/t1/img.png status=0 "PNGDATA"     # agent can read the saved image
auth.json status=1 "cat: ...: Operation not permitted"  # rest of CODEX_HOME stays denied
cp status=0                                          # agent can copy the image into the workspace
```

- `pnpm exec tsc --noEmit -p packages/paperclip-runner`: no errors in
changed files (only pre-existing errors from the unbuilt
`@paperclipai/paperclip-eval-kernel` package).
- `pnpm verify` exits 2 (changes outside resolved test graphs); required
CI supplies the broad coverage.
- **Test scope.** The red/green tests assert the launch configuration:
the disable flag, the thread config, the sandbox grant, and the
effective `image_generation` state that the pinned Codex binary reports.
They do not drive a model. So they do not directly observe the reported
"asks for a second key" behavior. The link from the flag to that
behavior comes from the pinned Codex 0.160.0 bundled instructions: the
built-in `image_gen` tool "Does not require `OPENAI_API_KEY`", and the
fallback `scripts/image_gen.py` CLI "Requires `OPENAI_API_KEY`". The
runner removes `OPENAI_API_KEY` from the shell.
- Not run: a live paid image generation with an API-key Codex agent.
There is no Codex staging company.

## Risks

- Image generation uses the agent's OpenAI account. Images cost money on
API-key billing. This is the expected behavior when the user asks for an
image.
- The agent can read images it (or an earlier thread of the same Codex
home) generated. It cannot read other files in `CODEX_HOME`.
- Codex's built-in `view_image` tool was already enabled, so the trusted
process already reads image files for the model. This change does not
add a new kind of file access to the provider.
- If a future Codex release changes the image save folder, the agent can
still see the image inline but cannot copy it. The N-12 binary test pins
the feature state for the current Codex version.

### Security policy change (needs maintainer sign-off)

**Why it was disabled.** #12364 ("isolate Codex security configuration")
disabled host apps, plugins, memories, multi-agent behavior, and image
generation as one group. Its goal was a closed launch configuration that
does not depend on ambient Codex defaults. The PR records no specific
threat for image generation. This PR keeps the other features disabled.

**Why re-enabling it is safe.**
- The tool is a hosted tool. It runs at OpenAI inside the same model
request that the trusted app-server already makes. It adds no new
network endpoint, process, or shell capability.
- No credential moves. `OPENAI_API_KEY` stays out of the shell, argv,
and serialized config (the N-12 test asserts this).
- The new sandbox grant is read-only and covers only
`CODEX_HOME/generated_images`. The rest of `CODEX_HOME` (`auth.json`,
config, sessions) stays denied. The real `codex sandbox` check above
confirms this. In plan mode the grant is also read-only.
- Codex's `view_image` tool was already enabled. So the trusted process
already sends local image content to the provider.
- Controller-gateway evals keep the tool off (`image_generation =
false`), because that gateway admits no hosted tools.

**Credentials and billing.**
- API-key path: the hosted tool uses the agent's existing OpenAI API key
through the app-server. OpenAI bills image generation to that API
account at image-model rates.
- ChatGPT-subscription path: the tool uses the agent's existing ChatGPT
sign-in. Use counts against that ChatGPT plan's limits. There is no
separate API bill.
- These two points follow from how Codex sends hosted tools in the
provider request. I did not observe them in a live paid run.
- Open question: Paperclip cost tracking uses provider token usage. I
did not confirm that image-generation charges appear in Paperclip's
per-agent cost and budget totals. If they do not, budget hard-stops
could undercount image spend.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with
tool use (shell, file edits, 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-06 11:48:54 -06:00
DottaandClaude Opus 5.5 a0cba70f18 fix(runner): continue restart-interrupted ACPX and OpenCode turns (#19501)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs keep their provider conversation when the control plane
restarts
> - A local Codex turn lost in a restart already continues once in the
same conversation (#15297)
> - ACPX (Claude and the other ACPX profiles) and OpenCode took a
different path: the runner closed the lost turn as a provider failure
> - The task then blocked with "Provider turn failed", and the user's
queued follow-up stayed unread
> - A restart must not need a user action when continuing is safe
> - This pull request gives ACPX and OpenCode the same recoverable
restart-loss cause and the same single continuation as Codex
> - The benefit is that work interrupted by a controller restart
finishes on its own

## Linked Issues or Issue Description

Refs #15297 (Codex continuation), #19399 (restart-interrupted turn
hook), #19471 (harness fix used for the live reruns).

**What happened?**

After a control-plane restart during an active native ACPX Claude turn,
the task ended `blocked` with `native_provider_terminal_failed: Provider
turn failed`. The queued follow-up comment was not consumed. No
continuation started. The next run then failed with
`native_session_cleanup_quarantined`. A native OpenCode run killed
mid-turn ended the same way (`turn.failed` with `error: null`).

**Expected behavior**

When the old provider process is gone and the conversation can be
restored, continue the task once in the same conversation. When that is
not safe, record an honest restart cause instead of a generic provider
failure.

**Steps to reproduce**

1. Start a native ACPX Claude task on a local runner.
2. Submit a follow-up comment while the turn is active.
3. Restart the control plane while the turn is still running.
4. Run `pnpm test:e2e:runner -- --max-automatic-retries 0 --id
everyday-workflows.runner-acpx-claude.local.recover-controller`.

**Root cause**

- `runner-core/src/acpx_provider_backend.rs`
`restore_session_if_needed`: a saved `turn_active` state always closed
the session. It recorded `turn.failed` with only a top-level `code:
acpx_active_turn_recovery_closed` and no `error` object. It also emitted
`run.terminal`.
- The runnerd transport forwards only `error`, so the controller saw
`error: null` and reported generic `provider_turn_failed`.
- `runner-core/src/provider_backend.rs` gave the recoverable
`provider_turn_lost_on_restore` cause only to `provider_label ==
"codex"`. OpenCode shares that backend.
- `nativeRestartInterruptedTurnId` accepted only `codex_app_server`. The
runtime continuation gate accepted only `provider.kind === "codex"`.
- Two follow-on defects appeared once the continuation ran live:
- The restored ACPX provider rejected the controller's result for the
lost turn's open semantic call. That call never settled.
- The restored provider's MCP request ids restart. The controller keys
semantic calls by `hash(runId, callId)`. The continuation's first
Paperclip tool call reused an earlier id, was treated as already
answered, and hung. After the result was accepted,
`runner_settlement_incomplete` failed the run.

## What Changed

- ACPX runner: for an active turn lost to a runner restart, prove that
the original provider lifetime lost its fence quorum. Then restore the
same conversation and record `turn.failed` with `error: { code:
"provider_turn_lost_on_restore", recoverable: true }`. Keep the run open
for the controller's continuation. An accepted result, an unanswered
runtime request, or an active goal keeps the old close behavior. So do
an unproven lifetime fence and a failed restore.
- ACPX runner fallback close: add a structured `error`
(`acpx_active_turn_recovery_closed`, not recoverable, with an honest
restart message), so the cause survives the transport.
- ACPX runner: track open semantic calls durably. On restore, retire the
lost turn's open calls. Acknowledge their controller results as
`retired` and do not send them to the new provider.
- ACPX sidecar: when the sidecar restores an existing session, prefix
forwarded semantic `callId`s with a per-process namespace
(`r<hex>:<id>`). Fresh sessions keep raw ids.
- OpenCode runner: give OpenCode the same recoverable cause and open run
as Codex (`restart_lost_turn_is_continuable`). Cursor Cloud stays
excluded because its agent runs remotely.
- Runner TS: `nativeRestartInterruptedTurnId` accepts `acpx_runtime` and
`opencode_server`. The runtime continuation gate accepts `acpx` and
`opencode`. All other Codex safety rules are unchanged: one
continuation, admitted dead-runner restart only, no compaction, goal,
semantic result, or pending runtime request.
- Docs: `doc/execution-semantics.md` and `doc/run-log-events.md`.

## Verification

Red, then green, at each layer:

- Rust `tests/acpx_warm_attachment.rs`
`restores_the_conversation_after_losing_an_active_turn_to_a_runner_restart`.
Red: status `closed`. Then red on the lost call's result: "tool result
does not match a pending provider call". Green after the fix.
- Rust `tests/native_provider_backend.rs`
`restart_lost_opencode_turn_records_a_recoverable_cause`, with the
qualified OpenCode launch fixture. Red: `error` was `null`. Green after
the fix.
- Rust unit test
`restart_lost_turns_continue_only_for_locally_restored_providers`
(Codex, OpenCode yes; Cursor Cloud no).
- TS `native-session-runtime.test.ts` "continues a restart-lost
acpx_runtime / opencode_server turn once". Red:
`NativeProviderTerminalFailure provider_turn_lost_on_restore`. Green:
exactly one continuation, which does not resend the task.
- TS `acpx-runtime-sidecar.test.ts`: a restored provider's call ids are
namespaced and resolve.
- Server `native-runner-restart-recovery.integration.test.ts`: a
checkpointed `acpx_runtime` or `opencode_server` restart interruption is
claimed as `resume_dead_runner`. Red without the driver gate. Green with
it.
- Two `native_provider_backend.rs` tests asserted the old
close-on-restore result. They now assert the new contract. Their real
invariant still holds: the delivered response is never replayed.

Suites run locally on the final revision:

- runner-core: `cargo test --no-fail-fast acpx`, `--test
native_provider_backend` (38), `--test acpx_warm_attachment`, `--test
codex_provider restart_lost`
- runner TS: `native-session-runtime`, `runnerd-restart-continuation`,
Codex driver recovery, contracts, and `acpx-runtime-sidecar` (all pass)
- server: `native-runner-restart-recovery.integration.test.ts` (80
passed)
- `pnpm --filter "@paperclipai/paperclip-runner..." build` (all
generated-contract checks pass)

Live runner E2E (local, real Claude via ACPX, macOS arm64):

- `everyday-workflows.runner-acpx-claude.local.recover-controller`: PASS
twice in a row on the final runner code. The restart crossed an active
turn. The task finished `done`, the queued follow-up was consumed, the
ZIP download was verified, and there were no failure codes.
- `stock-harness.runner-acpx-claude.local.continuity-restart`
regression: PASS.
- Earlier failing reruns are kept as evidence of the follow-on defects:
one harness `kill EPERM` while stopping the server (unrelated to this
change), one hang on the colliding call id, and one
`runner_settlement_incomplete`.

## Risks

- A continuation can run more work in the restored conversation. Its
prompt requires inspecting state before repeating an uncertain action,
as for Codex.
- The ACPX restore waits up to 5 s for the original provider lifetime
fence before it falls back to the old close.
- The ACPX provider state file gains two fields with defaults. Older
saved states still load. A downgraded runner cannot read state that a
newer runner wrote with these fields.
- Restored ACPX sessions now forward prefixed call ids. Only fresh
sessions keep raw ids. Live proof covers Claude. Codex-ACP, Pi, Cursor,
Copilot, Grok, Kimi, and Antigravity share this path, but this PR proves
them only by unit tests and code review.
- OpenCode is covered by the runner fixture and gate tests. The live
OpenCode restart-chaos cell was not rerun here.
- `runner-core`
`unconfirmed_suspension_state_becomes_recoverable_only_after_cleanup_and_suspend`
is flaky when run in parallel. It also fails with the new path disabled,
so it predates this change.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with
tool use, code execution, 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-06 11:39:29 -06:00
Nicky LeachandClaude Opus 5.5 ff1d1dea5d fix(tools): reject non-tool connections before any write (#19492)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Apps and tools connect to Paperclip through tool connections
> - `POST /api/companies/:companyId/tools/connections` creates a tool
connection through `createConnection`
> - The request schema accepts a channel purpose and the chat and voice
transports, but `createConnection` cannot store them
> - The database then rejects the row, so the caller gets a 500 and a
stray tool application stays behind
> - This pull request rejects those requests with a 422 before any
write, and stores `config` as `transportConfig` when the caller leaves
`transportConfig` out
> - The benefit is a clear error, no leftover rows, and consistent
connection rows

## Linked Issues or Issue Description

No issue exists. The new test typecheck in #18739 found this problem.
#18739 listed it as a possible product problem and did not change it.

**What happened?**

1. `createConnection` never writes `input.connectionPurpose`, so every
row gets the database default `tool`.
2. The schema allows `transport: "chat_sdk"` and `transport: "voice"`.
The database check `tool_connections_channel_transport_check` does not
allow these transports on a `tool` row.
3. No error mapping exists for a check violation, so the route returns
500.
4. When the request has no `applicationId`, the service inserts a
`tool_applications` row first, outside the connection transaction. That
row stays after the failure.
5. The schema gave `transportConfig` a default of `{}`. The service
stores `input.transportConfig ?? config`, so it stored `{}` instead of a
copy of `config`.

**Expected behavior**

The route returns 422 for a purpose or transport that it cannot create,
and writes no rows. When the request leaves `transportConfig` out, the
row stores `config` there.

**Steps to reproduce**

1. Send `POST /api/companies/:companyId/tools/connections` with `{
"name": "x", "transport": "chat_sdk" }`.
2. See the 500 response.
3. See the new `tool_applications` row for the company.

## What Changed

- `server/src/services/tool-access.ts`: `createConnection` rejects a
purpose other than `tool`, and the `chat_sdk` and `voice` transports,
with 422. The check runs before any lookup or write. Channel connections
still come from their own services.
- `packages/shared/src/validators/tool-access.ts`: `transportConfig` is
optional and has no default. The update schema already removed defaults,
so it does not change.
- `server/src/__tests__/tool-access-service.test.ts`: added tests for
the three rejected inputs (no rows written), and for a request that
sends only `config`.

## Verification

- `pnpm exec vitest run server/src/__tests__/tool-access-service.test.ts
server/src/__tests__/generic-mcp-connection.test.ts
server/src/__tests__/tool-connection-health-work.test.ts
server/src/__tests__/openapi-routes.test.ts
server/src/__tests__/tool-access-routes.test.ts
packages/shared/src/validators`: 27 files, 995 tests pass.
- `cd server && pnpm typecheck`: passes. This includes the source layers
and the three test-tree shards.
- Not run locally: the full `pnpm test:run`, the root `pnpm build`, and
the UI typecheck. Required CI covers them.

## Risks

- Low risk. A client that sends a channel purpose or a chat or voice
transport to this route now gets 422 instead of 500. The request failed
before this change too.
- A client that sends only `config` now gets the same value in
`transportConfig`. Every reader checks `config` first, so this changes
no behavior.
- The OpenAPI output for this body no longer shows a default for
`transportConfig`.

## Model Used

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

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [ ] 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-06 17:30:24 +00:00
DottaandPaperclip 0d03610d2f fix(opencode): export bounded tool argument diagnostics (#18699)
## Thinking Path

> - Paperclip manages work across agent runtimes.
> - Company observability sends allowed records to the operator's OTLP
receiver.
> - Native OpenCode exports tool results but omits submitted argument
diagnostics.
> - Exact argument values are excluded by the current capture policy.
> - Safe type information can distinguish malformed envelopes without
copying private values.
> - This change exports a fixed diagnostic shape from authenticated
dispatch and schema-rejection events.
> - Operators can compare rejected and accepted calls under the existing
capture controls.

## Linked Issues or Issue Description

**What happened?**

OpenCode tool spans omit argument diagnostics. The exporter reads native
tool events. The authenticated MCP boundary has the submitted arguments,
but its dispatch item events do not create tool spans. Calls rejected by
its schema check do not emit dispatch items.

**Expected behavior**

With tool details and tool content enabled, Paperclip tool spans include
the method, diagnostic schema version and allowed argument field types.
Exact values and arbitrary field names stay excluded.

**Steps to reproduce**

1. Configure a company OTLP receiver and enable native OpenCode export.
2. Enable tool details and tool content.
3. Submit a rejected Paperclip tool call and then a valid call.
4. Inspect the input diagnostics and outcome of each span.

Related work: #17989 proposes metadata-only native export. #18098 added
the normalized content exporter. This change repairs argument
diagnostics in that exporter.

## What Changed

- Export `paperclip.opencode.tool-diagnostics.v1` as `braintrust.input`
for authenticated Paperclip tool calls.
- Include `tools/call`, the argument type and types for a fixed list of
envelope fields.
- Use dispatch IDs for Paperclip tool spans. Keep separately named
`opencode tool ...` provider records for failures before dispatch. Mark
each span source.
- Preserve schema-rejected calls as failed items without invoking the
controller handler. Keep authentication and the closed tool catalog.
- Bound schema-rejection error text in stored events and transcripts
with the existing redaction and 8 KiB limit. Keep the provider response
unchanged.
- Preserve the controller opt-in, tool-detail and tool-content switches,
redaction, limits and run identity.
- Add MCP-to-receiver regression coverage and update the capture policy
and capability maps.

### Hypothesis and thesis for a fix

The exporter drops the dispatch diagnostic signal. Raw input omission is
intentional. The repair belongs in the OpenCode driver and exporter.
Shared protocol, tool permissions and task completion stay unchanged.

The regression sends schema-rejected, handler-rejected and valid
argument envelopes through a fixture OpenCode subprocess and the
authenticated MCP bridge. It checks the downstream rejection and
receipt, completion event and actual local OTLP request. The baseline
has no dispatch spans. The candidate has separate spans with safe type
diagnostics. Two warm runs and disabled capture use the same path.

Source: [OpenCode diagnostic
pattern](https://www.braintrust.dev/app/Paperclip/p/Paperclip%20Staging/patterns/f172172e-9c65-453f-bb3a-7c6ba4f5a400).

## Verification

- Baseline: `a92f616cd0a457c8ba85d994b24e1f92c2e4d1de`. The new
integration regression fails because tool spans are absent. The same
regression passes with this change.
- Exporter and MCP bridge suites: 37 tests pass. Three final driver
controls pass. Ten selected controls also passed before the final
provider-record repair. Runner TypeScript typecheck and build pass after
the review repair.
- The review regression failed with two spans before the repair. It
passes with three spans after the repair. Unauthenticated and unassigned
calls do not reach the rejection observer. A broken observer cannot
change the schema rejection.
- The full OpenCode/exporter/MCP run passes 94 tests and fails one
existing final-answer selection test. The same failure occurs in an
untouched baseline checkout. It is not claimed as a pass.
- A further filtered run reached the default five-second timeout in two
existing continuation tests during concurrent repository checks. All
five selected dispatch, warm-session and continuation controls pass with
a 15-second test deadline. The earlier timeouts remain recorded.
- `node scripts/check-maps.mjs` and `git diff --check` pass.
- Runner TypeScript checks and build pass. Full repository typecheck and
build stop at Rust because `cargo` is not installed. The full repository
test command was stopped before workspace handoff. No full-suite pass is
claimed. Local source commits:
`c48a8af1c186e415c802fa0ac95209b16b4a3122` `6744991f6`, and `a8ccf4693`.
The remote branch is based on newer master
`4814d1ab691c176170510a93a175dafdc9e021c7`. All initial eight changed
files match between those bases before the patch.
- The integration provider and controller are fixtures. The OTLP
receiver is a real local HTTP server. A separate real OpenCode 1.18.34
probe opened a session. The remote free model returned `Free usage limit
reached`. No tool call or receipt was produced. No live recovery is
claimed.

- Review follow-up: a real MCP request with 12,000 invalid array
elements produced a 264,908-character event error before the repair. The
same regression now stores 8,192 characters in both events and
transcript. The MCP error receipt stays unchanged and no handler runs.
All 37 exporter/MCP tests and 12 selected driver/exporter/MCP controls
pass. Runner TypeScript checks and build pass. Local follow-up commit:
`64d4fa583`.
- The previous remote head passed Runner, typecheck, build and E2E. Its
unchanged attention-retention shard failed twice with 15-second
timeouts, then cleanup deadlock and duplicate fixture IDs. Both affected
retention suites passed all 70 tests locally unchanged. The new head
needs fresh CI and review; no full CI pass is claimed.

- Current-master qualification (2026-11-06, merge `2959ecdb8d`):
normally merge master `53645a0104`, including the existing test-only
retention-fixture repair from #18774 reused by #18770. Keep the OpenCode
diagnostics feature and upstream durable MCP and completion-feedback
behavior. No test deadline or assertion changed. The three old CI
timeouts did not reproduce locally: the four completed-blocker-history
cases passed on the old head in 29.52 seconds, then passed on the merged
head in 13.23 seconds. The fixture rollback/provenance regression also
passed. All 116 exporter, MCP bridge and OpenCode driver fixture tests
passed. Runner TypeScript checks and both TypeScript compilation targets
passed; ACPX profiles match the release manifest and protocol-contract
checks pass. These checks use local PostgreSQL, HTTP and process
fixtures. No hosted provider call or deployed recovery is claimed. Fresh
CI and review are pending for this merge.

- Map-scope review follow-up (`61e662e179`): remove only the inherited
diagnostics paragraphs from the company capability maps. This change
adds logging detail within an existing capability. `node
scripts/check-maps.mjs` and `git diff --check` pass. Production and test
inputs are unchanged from the qualified merge; fresh CI and review are
pending for the final head.

- Test compiler follow-up (`6675b46377`): reproduce CI TS2722 with `tsc
-p packages/paperclip-runner/tsconfig.test.json --noEmit`. The harness
contract makes transcript retrieval optional. The OpenCode bounds
regression now fails explicitly if that capability is absent, then
inspects the narrowed method. The same complete Runner test compiler
project passes, and the actual local schema-rejection fixture passes
with all receipt and 8 KiB assertions retained. Production inputs are
unchanged. Fresh final-head CI and review are pending.

- Final-head qualification: `6675b4637743aa269e5214756b158e1e0a4561a5`
passes all 56 applicable CI checks (three skipped, one neutral review).
[CI
result](https://github.com/paperclipai/paperclip/actions/runs/42253119577).
Greptile gives 5/5 with no open findings. Hosted task recovery remains
pending verified rollout.

## Risks

- Authenticated spans use dispatch IDs. Provider records use separate
names and a source attribute. Consumers must choose the source they need
to avoid counting two observations of the same tool action.
- Safe types cannot reconstruct nested arguments or explain every
value-level validation failure.
- Export remains best effort and bounded. Lost native frames no longer
remove dispatch diagnostics, but abrupt process termination can still
lose a turn.
- Full local Rust checks and a deployed product journey are not yet
verified. The pull request is unmerged and not deployed.

## Model Used

OpenAI GPT-6 through Codex, with reasoning, tool use and code execution.
The exact runtime variant and context-window size are not exposed to
this session.

## Checklist

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

> - Paperclip preserves agent work across failed runs and server
restarts.
> - A remote workspace must have a durable save before its source can be
released.
> - Portable capture normally writes a Git mirror and a full archive
inside the sandbox.
> - A full sandbox can prevent capture before the provider's streaming
download starts.
> - Retrying that write cannot create the required free space.
> - This change transfers a checked standalone source and seals its
archive on the controller.
> - The source remains retained unless the existing durable save
completes.

## Linked Issues or Issue Description

Related: #18644 reduced temporary capture space. #18172 added the
provider's disk-free directory transfer. This repair connects portable
capture to that transfer when the archive itself cannot be created.

**What happened?**

Workspace capture returns `remote_workspace_sandbox_disk_full` after
provider work has finished. File recovery repeats the same sandbox
write.

**Expected behavior**

Recover a stable standalone workspace without requiring another full
archive on the source disk. Preserve staged changes, untracked work,
confined links and the original source.

**Steps to reproduce**

1. Create a standalone Git repository with a commit, staged changes and
an untracked result.
2. Use the existing real-command transport fixture. Inject ENOSPC into
the sandbox helper write or archive write.
3. Run portable capture. Before this change it rejects the helper
failure.
4. With the change, restore the saved archive after removing the fixture
source. Check the commit, staged content, untracked result and Git
connectivity.

## What Changed

- Attempt one controller-side capture after an exact sandbox disk-full
failure.
- Read a source inventory without installing a helper. Use the existing
provider bulk transfer with confined links and runtime exclusions.
- Compare the transferred inventory with source fingerprints before and
after sealing.
- Reject copied Git pointers, alternates and administrative symlinks.
Parse config as data without includes; allow only portable keys before
controller Git normalization.
- Preserve socket exclusions. Retain the source and remove incomplete
controller staging after failure.
- Update execution semantics and focused regression coverage. No
database migration or new execution authority.

## Verification

- TDD red on base `2f35eb1080`: the new full-sandbox test failed with
`remote_workspace_sandbox_disk_full` before production edits.
- Green: 202 transport and archive tests passed before the additional
Git configuration guards. At head `8541086cb5`, all 116 transport tests
and the PostgreSQL durability acceptance pass. The final `e24e173ec9`
exclusion guard passes those 116 cases plus its new Git-history
regression across the full run and focused rerun. Environment: macOS
arm64, Node 26.4.0; real Node/Git commands and filesystem transfer.
- Review regressions were also red before their fixes: an internal Git
config symlink bypassed the check, and `core.attributesFile` could
select a controller path. Git administrative symlinks and nonportable
config now fail before normalization. The focused safety selection
passes 11 cases.
- Cases cover helper and archive ENOSPC, commit/staged/untracked
continuity after source removal, confined links, sockets, changed
source, corrupt or extra copied files, unsafe Git metadata, retained
diagnostic text and failed download.
- Real PostgreSQL acceptance passes: normal workspace save recovers one
sandbox ENOSPC, commits the encrypted snapshot, clears its writer, loses
the original allocation, then independently reloads the saved result
twice. The first acceptance attempt used an incorrect expected status
(`available`); the documented state is `committed`. No production change
was needed for that correction.
- Review regressions for untracked Python/Node environments and
excessive Git roots failed before their fixes. Dependency exclusions now
match normal capture. Controller parsing starts only after
whole-tree/repository budgets and layout checks; refspec/settings
budgets and a shared two-minute verification/normalization deadline
bound further work. The excessive-root test verifies zero Git
subprocesses.
- Real provider-boundary red/green: Daytona direct tar and
capacity-fallback PTY transfers first dropped `._result.txt`. The new
optional mapping contract preserves all user names and excludes only
current transfer scratch. Kubernetes implements the same contract.
Explicit caller exclusions and archive validation remain in force.
- Standalone Linux qualification on Node 24.22.2/Debian bookworm: fresh
shared/SDK build; Daytona and Kubernetes typecheck and build; Daytona
file-sync/capacity/diagnostics 68/68 pass; Kubernetes file-sync 31/31
pass, including the actual `/proc` case. Qualification was refreshed
again on the final `be6f7feacc` source tree: current shared/SDK rebuilt;
all 815 staged source/config/manifest hashes match the delivered tree
before and after testing. An initial container run as root bypassed a
read-only file test; the same source passes as uid 1000. Installing zstd
enabled eight initially skipped cases.
- A further review regression proved `.git/pyvenv.cfg` could exclude Git
history as a dependency. The inventory now handles `.git` before
dependency filtering, matching normal capture. The full capture/restore
test verifies the original commit and saved work. The first green
attempt incorrectly compared command-result object identity; correcting
the test to compare stdout passed with no production change.
- Further red/green regressions cover read-only private-copy cleanup, a
regular file replaced by a FIFO before open, case aliases of Git
administrative metadata, and permission preservation under restrictive
umask. Nonblocking no-follow file handles verify identity before and
after reading. Cleanup changes only private controller directories.
Provider extraction preserves ordinary permissions, refuses special mode
bits before writing, and does not adopt source ownership, ACLs,
attributes or flags. Final macOS Daytona tests pass 65/65; the final
Linux qualification above was repeated on these exact provider sources.
- Focused strict TypeScript compilation of the inventory and archive
modules passes.
- The first sandboxed archive run failed three Unix socket fixtures with
EPERM. The permitted run outside that filesystem sandbox passed all
tests.
- Initial full local server typecheck could not resolve the unbuilt
Runner package and produced cascading errors. Exact-head `e24e173ec9` CI
typecheck, build, Runner and browser checks pass. That CI attempt failed
two server shards: the unchanged retained-cleanup backlog test timed out
at 15 seconds and passes locally unchanged; ten cache-reclaim fixture
oracles failed because directory fallback consumed their sealed-archive
counters/faults. Final commit `caed4e4f83` isolates the intended
unavailable-directory boundary while preserving archive counts, recovery
status, integrity and no-second-reclamation assertions. Those ten
failures reproduced locally before the fixture edit. The full affected
file passes 394/395; its sole other HTTP socket-hang-up case passes
unchanged in an isolated rerun. This is combined coverage of all 395
cases, not a single complete green run. The final branch integrates
master `53645a0104` at `98ea07225592930cbb0d18bcf250bc64f6bb13b4`,
preserving upstream workspace phase timings. After integration: 117
transport cases, the PostgreSQL durability acceptance, the existing
object-storage/timing regression, and 12 affected cache cases pass. The
expanded recovery file now has 421 cases. Final-head
`be6f7feacc45c1e32a26a2417e973f1e3752fd8d` CI passes all 55 applicable
checks (three skipped, one neutral review), including the expanded
recovery suite, typecheck, builds and Runner coverage. [CI
result](https://github.com/paperclipai/paperclip/actions/runs/42251345554).
- Provider exclusion regressions were red through four real Linux
transfer paths: Daytona direct archive, direct safe-link repair,
capacity PTY repair, and Kubernetes streaming. A root socket and nested
regular file share a basename; literal glob-character files/subtrees
also survive. Preserve mode now anchors tar exclusions as literal
root-relative paths and escapes `find -path` pruning. Default callers
keep glob semantics. Receipt-count regressions were also red before
correcting preserved-output counting. All 31 Kubernetes and 68 Daytona
tests, builds and typechecks pass on the final source with 815 matching
inputs.
- The final semantics documentation explicitly records special mode
bits, the stricter config allowlist (including innocuous unlisted keys),
and provider qualification limited to Daytona/Kubernetes; these address
the optional current-head review observations without broadening
controller Git authority.
- No Daytona credential is available in this session. Provider transfer
is a local filesystem boundary in these tests. Hosted task recovery and
rollout remain unverified.

## Risks

- Recovery reads the source repeatedly and transfers the standalone
tree. It can take longer than normal capture.
- Linked worktrees, initialized submodules, Git administrative symlinks,
nonportable Git configuration, special mode bits and alternate object
stores remain retained if the normal capture cannot finish. This
fallback does not follow their pointers onto the controller.
- A provider without usable directory export, a changed source, or
insufficient controller space cannot produce a receipt. The original
source stays retained.
- The server, SDK and affected provider plugins must be built from this
revision together. An older provider can fail inventory verification on
names it omits; it cannot publish an incomplete save. No merge,
publication or deployment is included.

## Model Used

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

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub references)
- [x] My branch name describes the change 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-06 11:00:32 -06:00
Nicky LeachandClaude Opus 5.5 55ea9cfa1b fix(runner): match retained cleanup and Codex close types to their contracts (#19495)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The Paperclip Runner runs native agent sessions, such as Codex,
behind typed contracts
> - #18839 started to typecheck the Runner tests, and it listed contract
types that do not match their runtime use
> - First, retained runnerd cleanup accepts any backend kind, but
cleanup works only when that kind is `"runner"`
> - Second, `CodexHarnessSession.close` makes its input optional, but
the `HarnessSession` contract requires a reason
> - This pull request narrows both types to match their contracts
> - The benefit is that the compiler now rejects a cleanup proof that
would silently release nothing

## Linked Issues or Issue Description

No issue exists. #18839 listed both items under its product questions
and did not change them.

**What existing behavior does this improve?**

1. `completeRetainedNativeSessionCleanup` builds its release key from
`proof.backend.kind`. The quarantine key comes from the backend
descriptor's `kind`, and every production backend uses `"runner"`. If
the two kinds differ, the function returns 0 and the quarantine stays.
`RetainedRunnerdMaintenanceInput` and `RetainedRunnerdCleanupProof`
typed `backend.kind` as any string.
2. `HarnessSession.close` takes `{ reason: string; force?: boolean }`.
`CodexHarnessSession.close` took `input?: { reason: string }`.

**Current behavior**

The only production caller passes `backend: { kind: "runner", name:
"codex_app_server" }`. All production `close` calls pass a reason. So
nothing fails today.

**Proposed behavior**

The types state the same rules as the contracts.

**Reason and benefit**

A future caller that passes another kind gets a compile error, not a
quarantine that stays without notice.

**Breaking changes**

None for production callers. Both types are narrower, and every current
caller already passes the narrow values.

## What Changed

- `packages/paperclip-runner/src/live/runnerd-codex-transport.ts`:
`backend.kind` is `"runner"` in `RetainedRunnerdMaintenanceInput` and
`RetainedRunnerdCleanupProof`. `name` stays a string.
-
`packages/paperclip-runner/src/drivers/codex/codex-harness-session.ts`:
`close` takes `{ reason: string; force?: boolean }`, as the contract
does. The body still reads `input?.reason`, because a generated test
script calls `close()` with no arguments. Codex closes the same way with
or without `force`.

- `packages/paperclip-runner/src/live/runnerd-contract-types.test.ts`:
new type test. The Runner test typecheck fails if either type accepts
another backend kind, or if the Codex `close` parameters differ from the
contract. With the master types, this file has 4 type errors.

Not in this pull request: #18839 also listed that
`PrpEvent.normalizedSessionId` is required in the type but optional in
the event schemas. A separate pull request handles it in
`validatePrpEvent`.

## Verification

- `pnpm exec vitest run src/live/runnerd-contract-types.test.ts` (in
`packages/paperclip-runner`): passes 2 tests.
- `tsc --noEmit` passes in `packages/paperclip-runner` for
`tsconfig.json`, `tsconfig.surfaces.json`, `tsconfig.test.json` and
`tsconfig.surfaces.test.json`.
- `cd server && pnpm typecheck`: passes. The server builds the
maintenance input with the `"runner"` literal.
- In `packages/paperclip-runner`, after `pnpm run test:typescript:prep`:
`pnpm exec vitest run
src/live/runnerd-codex-transport-settlement.test.ts
src/drivers/codex/codex-protocol-cleanup.test.ts
src/drivers/codex/codex-app-server-driver.lifecycle.test.ts` passes 3
files and 43 tests.
- Not run locally: the full Runner `pnpm test` (including Rust tests)
and the root `pnpm build`. Required CI covers them.

## Risks

- Low risk. The change affects types only, plus one comment in the Codex
session. Runtime behavior does not change.

## Model Used

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

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [ ] 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-06 16:54:09 +00:00
Nicky LeachandClaude Opus 5.5 21a1e78852 feat(environments): add managed plugin environment rows (#19490)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A managed instance gets its instance-level environments from the
`PAPERCLIP_MANAGED_CONFIG` document
> - That document cannot declare a plugin environment yet, so a managed
instance cannot get a plugin-provided environment without manual setup
> - The full change is split into three pull requests. This first one
adds the plugin environment rows and their write floor, and nothing
calls them yet
> - The second pull request lets the managed config declare plugin
entries and provisions them at boot. The third sets the instance default
once and advertises the new managed-config capability
> - The benefit is a smaller review for each step, and no instance
changes behavior until the last step lands

## Linked Issues or Issue Description

**Subsystem affected**
Server environments (managed-config environment provisioning,
environment routes) and the board UI's adapter Test target.

**Problem or motivation**
A managed instance cannot get a plugin environment from its managed
config. Company settings cannot create one either. So each managed
instance that needs one requires manual setup.

**Proposed solution**
Over three pull requests, let an `environments` entry in the managed
config declare `driver: "plugin"`. Boot then provisions one locked row
for each plugin entry, and makes one of them the instance default once
on new instances. This pull request adds only the row layer: create or
adopt a marked row, archive it, find it, and keep tenants from editing
it.

**Alternatives considered**
Manual setup on each instance, and a new control-plane endpoint. Both
need a step outside the deploy. The managed config already delivers the
sandbox entry.

**Roadmap alignment**
This extends the existing managed-config `environments` section, which
already provisions the managed sandbox. It adds no new product surface.

## What Changed

- New `server/src/services/managed-plugin-environments.ts`, used through
new `environmentService` methods (`ensureManagedPluginEnvironment`,
`archiveManagedPluginEnvironment`, `findManagedPluginEnvironment`):
- The ensure validates the driver config through
`normalizeEnvironmentConfig` and
`validatePluginEnvironmentDriverConfig`, the same path an
operator-created plugin environment takes.
- It marks the row `managedByPaperclip: true` and
`managedPluginEnvironment: "<pluginKey>:<driverKey>"`.
- It adopts an existing unmarked row for the same plugin and driver
instead of adding a second one, so a hand-made row keeps its agents.
- A row that already matches gets no write. Changes write activity log
entries.
- Archive sets an active marked row to `archived`. The next ensure
reactivates it.
- Migration `0437`: unique index
`environments_managed_plugin_environment_idx` on `metadata ->>
'managedPluginEnvironment'`. This is the only schema change of the three
pull requests. Snapshot `0432` is pruned to keep the five newest.
- `server/src/routes/environments.ts`:
`isPlatformProvisionedEnvironment` also covers the new marker (write
floor and redaction). `managedPluginEnvironment` is a reserved marker
key, so a client cannot stamp it on its own row. Nothing provisions
marked rows yet, so the marker-clear escape hatch stays open for them.
The next pull request refuses it while the managed config declares the
row's entry.
- `ui/src/lib/adapter-test-environment.ts`:
`resolveManagedSandboxEnvironmentId` requires `driver === "sandbox"`.
The plugin row also carries `managedByPaperclip`, so the old "not local"
check could take it for the managed sandbox.

## Verification

- `cd server && npx vitest run
src/__tests__/managed-plugin-environments.test.ts` (embedded Postgres):
- The ensure creates the marked row through the plugin validator. A
second ensure writes nothing and logs nothing.
- A hand-made row for the same plugin and driver is adopted. It keeps
its id and its agents.
  - Archive, then ensure, reactivates the row.
- The ensure refuses while the plugin has no running worker, and refuses
a name that another environment holds.
  - The unique index allows one row per marker.
- `npx vitest run src/__tests__/environment-routes.test.ts`: on a
cloud-managed instance, edit, env-var edit, status change and delete of
a marked plugin row are refused, including for instance admins. The
marker alone keeps the floor. A client cannot stamp the marker. The
marker clear is allowed.
- `npx vitest run src/__tests__/environment-service.test.ts`: passes.
- `cd ui && npx vitest run src/lib/adapter-test-environment.test.ts`:
the plugin row is never taken for the managed sandbox. Under
managed-sandbox-only, an agent on the sandbox tests in the sandbox, and
an agent on the plugin environment tests there.
- `pnpm --filter @paperclipai/server typecheck`, `pnpm --filter
@paperclipai/ui typecheck` and `pnpm --filter @paperclipai/db
check:migrations` pass.
- I did not run the full `pnpm test:run`, `pnpm -r typecheck` or `pnpm
build`. CI covers them. `pnpm verify` asks for broader verification
because of the migration.

## Risks

- Migration: one new partial unique index on a small table. No row
carries the marker before this change, so the index cannot fail on
existing data.
- No caller uses the new ensure yet, so no instance changes behavior.
- The UI change affects only lists that hold a non-sandbox row with
`managedByPaperclip`. Today that is only the local row, which the old
check already excluded.

> 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 (file edits, shell, 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-06 16:52:43 +00:00
DottaandClaude Opus 5.5 819420a60a fix(runner): give a cold OpenCode restore the 60 s admission budget (#19487)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native OpenCode agents run through a local runner. After a
control-plane restart, a replacement runner restores the saved provider
session.
> - The replacement runner does run preparation, `opencode serve`
startup and session resume inside one `run.attach` command.
> - The controller waits only 30 s for that command. A fresh start gets
separate bounds for `run.prepare` and `session.open` for the same work.
> - On a loaded host, a cold OpenCode restore took 10-38 s. When it took
more than 30 s, the continuation failed with
`runner_local_connect_failed: PRP command run.attach timed out`.
> - This pull request gives a cold OpenCode restore the same 60 s
cold-admission budget that Pi already uses.
> - The benefit is that a task that waited on a question across a
restart continues when the user answers, without a failed run.

## Linked Issues or Issue Description

**What happened?**
A native OpenCode agent asked a structured question. The control plane
restarted while the task waited. The user answered. The continuation run
failed with `native_session_recovery_failed: Error:
runner_local_connect_failed: PRP command run.attach timed out`, about 35
s after the runner transport connected. This happened in 1 of 2 runs of
the
`local-session-integrity.runner-opencode.local.structured-question-restart-resume`
runner E2E cell.

**Expected behavior**
The continuation restores the saved OpenCode session and finishes the
task. A restart must not cause a failed run.

**Steps to reproduce**
1. Run `pnpm test:e2e:runner -- --max-automatic-retries 0 --id
local-session-integrity.runner-opencode.local.structured-question-restart-resume`
on a host with other load.
2. The case asks a question, restarts Paperclip and answers the
question.
3. When the cold restore takes more than 30 s, the continuation run
fails at `run.attach`.

**Paperclip version or commit**
`master` at 0252421d5c.

Evidence from the runner spans (`runner.session.startup` of the
continuation run): 31.0 s (passed, close to the bound), 38.0 s (failed),
14.0 s and 10.1 s (passed). The first (fresh) runs took 4.9-13.7 s.

## What Changed

- `coldAdmissionTimeoutMs` returns 60 s for a cold OpenCode runner
process. Live adoption (a warm runner) keeps 30 s. Other providers keep
30 s, except Pi, which already had 60 s.
- The unit test for the cold admission budget covers the OpenCode cold
and live cases.

## Verification

- Red before the fix: `cd packages/paperclip-runner && npx vitest run
src/live/runnerd-codex-transport.test.ts -t "cold process admission
budget"` failed on `coldAdmissionTimeoutMs("opencode", undefined, true)`
(30000, expected 60000).
- Green after the fix: the same command passes.
- Live runner E2E on macOS arm64 with OpenRouter
(`openrouter/deepseek/deepseek-v4-flash-0731`), local runner, at the
fixed revision. Command: `pnpm test:e2e:runner --
--max-automatic-retries 0 --id
local-session-integrity.runner-opencode.local.structured-question-restart-resume`.
- Before the fix: 1 of 2 campaign runs failed at `run.attach` (restore
38.0 s). The passing run restored in 31.0 s. Two more local runs passed
(restore 14.0 s and 10.1 s).
- After the fix: 4 of 4 runs that reached the restart passed (restore
6.2 s, 11.3 s, 5.1 s and 11.8 s). One more run failed before the restart
because the model sent malformed `request_human_input` arguments. That
failure is not related to this change.
- Limitation: host load was lower during the after-fix runs, so no
restore took more than 30 s. The unit test proves the new bound.
- Other failures in `runnerd-codex-transport.test.ts` are unchanged on
`master` in this local environment (the fake Rust fixture binary is not
built locally). CI builds it.

## Risks

- Low risk. The change only makes a cold OpenCode admission wait longer
before it reports a failure. A runner that is really stuck now fails
after 60 s instead of 30 s.
- A fresh OpenCode start now shares one absolute 60 s deadline for
`session.open` and provider identity, the same as Pi. Fresh OpenCode
starts took 5-14 s.
- Inner bounds stay in place: the runner still bounds each provider
request (30 s) and OpenCode health checks (10 s).

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code with tool use (shell, file edits). Reasoning mode 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
- [ ] 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-06 10:33:51 -06:00
Nicky LeachandClaude Opus 5.5 e1332b997f fix(plugins): align tool run context and interaction input types (#19493)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Plugins add tools and issue-thread interactions through
`@paperclipai/plugin-sdk`
> - The SDK types are the public contract between the host and plugin
authors
> - Two parts of that contract do not match what the host does at
runtime
> - First, `ToolRunContext.projectId` says it is always a project UUID.
The tool gateway sends an empty string when a run has no project. The
direct tool route rejects such runs with 400
> - Second, three interaction helpers ask for the parsed output type, so
plugin authors must pass fields that the host fills in with defaults
> - This pull request makes the types and both tool paths agree with the
runtime
> - The benefit is that agents with no project can run plugin tools, and
plugin authors can omit the fields that have defaults

## Linked Issues or Issue Description

Fixes #3273

Related: #3274 removes the `projectId` requirement from the route. That
change alone is not enough today: the route now also looks up the
project to check its company, and that lookup needs a value. This pull
request also skips that lookup when the run has no project. It gives
plugins one value for "no project" on both tool paths.

The new test typecheck in #18739 and #18839 also listed both problems as
possible product problems.

## What Changed

- `server/src/routes/plugins.ts`: `POST /api/plugins/tools/execute`
accepts a `runContext.projectId` that is missing, `null`, or an empty
string. It passes an empty string to the plugin, the same value that the
tool gateway sends. It checks the project's company only when a project
ID is present. A `projectId` that is not a string returns 400.
- `server/src/routes/openapi.ts`: the request schema marks
`runContext.projectId` as optional and nullable.
- `packages/plugins/sdk/src/types.ts`:
- The `ToolRunContext.projectId` comment says the value is an empty
string when the run has no project.
- `suggestTasks`, `requestConfirmation` and
`requestCheckboxConfirmation` take `CreateIssueThreadInteractionInput`,
as `askUserQuestions` already does. The host parses this input with the
schema, so the defaults apply.
- `packages/plugins/examples/plugin-kitchen-sink-example/src/worker.ts`:
the example passes `undefined` instead of an empty project ID to
`issues.create`.
- `doc/plugins/PLUGIN_SPEC.md` (section 13.10): documents the empty
project ID.
- Tests:
- `server/src/__tests__/plugin-routes-authz.test.ts`: tests a run with
an empty, missing, or `null` project ID, which skips the project lookup.
Also tests that a project ID that is not a string returns 400.
- `packages/plugins/sdk/tests/testing-actions.test.ts`: removes the cast
that the output type required.

## Verification

- `pnpm exec vitest run server/src/__tests__/plugin-routes-authz.test.ts
server/src/__tests__/openapi-routes.test.ts
packages/plugins/sdk/tests/testing-actions.test.ts`: 3 files, 81 tests
pass.
- `cd server && pnpm typecheck`: passes. This includes the source layers
and the three test-tree shards.
- `tsc --noEmit` passes for `packages/plugins/sdk` (`tsconfig.json` and
`tsconfig.test.json`) and for the kitchen-sink example.
- Not run locally: the full `pnpm test:run` and the root `pnpm build`.
Required CI covers them.

## Risks

- Low risk. The direct tool route now accepts calls that it rejected
before. It still checks that the agent and the run belong to the
company. With no project ID, there is no project to check.
- A plugin that reads `runCtx.projectId` can get an empty string on both
tool paths. The tool gateway already sent this value before this change.
Only the documentation is new.
- The SDK parameter types are wider. Existing plugin code still
compiles.

## Model Used

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

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [ ] 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-06 16:25:43 +00:00
Nicky LeachandPaperclip 5e62d8ee82 Report the native private host-control protocol version (#19488)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native Runner executes agent work and reports its binary contract
through build metadata.
> - Unix Runner already supports a private host-control socket for park,
claim, and retire requests.
> - A host cannot identify that protocol from the current metadata.
> - This PR adds an explicit version field for that existing private
protocol.
> - Hosts can verify the selected binary before opening the private
channel.

## Linked Issues or Issue Description

Refs #19140 and #19129, which added the existing native lifecycle and
private channel.

**What existing behavior does this improve?**

The `paperclip-runnerd --build-metadata` response.

**Current behavior**

The native binary implements the private fd 4 protocol on Unix. Its
metadata has no field that identifies this protocol. Recovery versions
describe different contracts.

**Proposed behavior**

Report `privateHostControlVersion: 1` on Unix builds. Omit the field on
other builds. Keep ordinary artifact checks and runtime behavior
unchanged.

**Reason and benefit**

A host can verify protocol support on the selected binary before using
the channel. This field does not mean a provider is ready to park or
that a host implements public release/resume or qualified sleep/wake.

**Breaking changes**

The JSON response gains one optional field on Unix. Current artifact
parsers and launch checks accept extra fields. Binary contract version 2
stays unchanged.

## What Changed

- Add the Unix-only private host-control metadata version.
- Check the exact version and non-Unix absence in metadata unit and real
CLI tests. Keep hosted parking and public release/resume fields absent.
- Add a regression test that proves ordinary artifact parsing accepts
and ignores the new field.
- Document the field, exact-version checks, and separate provider and
host qualification.

## Verification

- On macOS arm64 with Rust 1.97.1, passed narrow offline, locked Cargo
tests for the metadata unit, six existing private-protocol cases,
wrong-fd CLI rejection, and real CLI metadata output.
- Passed `cargo fmt --manifest-path
packages/paperclip-runner/runner/Cargo.toml --all -- --check`.
- Passed `vitest run src/evals/runnerd-artifact.test.ts` from the Runner
package: 5 tests. Initial collection could not resolve Ajv in the fresh
worktree. Connecting existing local dependencies resolved that setup
failure; no source change was needed.
- `git diff --check` passed. Reviewed the diff for secrets, personal
data, and private references.
- GitHub CI passed on commit `0790af690afaff10f3b69fd9130b5be1c5bab38c`:
[main PR
workflow](https://github.com/paperclipai/paperclip/actions/runs/42248006277),
[Docker Runner
check](https://github.com/paperclipai/paperclip/actions/runs/42248006207),
and [Windows
Runner](https://github.com/paperclipai/paperclip/actions/runs/42248006518).
Windows tests cover omission of the field on non-Unix builds. All 61
current-head check runs settled without failure; the latest status
context passed.
- [Standard Greptile
review](https://github.com/paperclipai/paperclip/pull/19488#issuecomment-6689260747)
scored that same commit 5/5 with no actionable findings or unresolved
threads.
- No live provider or host sleep/wake test was run for this
metadata-only change.

## Risks

The main risk is treating protocol support as permission to park a
provider or as proof that a host supports release/resume. The
documentation keeps those checks separate. No caller enables the channel
from this field in this PR. Current parsers check named fields or
project known fields, so the optional field does not change ordinary
artifact checks. No migrations, new dependencies, runtime flags,
checkpoints, or provider pins.

## Model Used

OpenAI GPT-6.1 Sol (`gpt-6.1-sol`), High reasoning, through Codex with
tool use and code execution. Independent agent review checked the
metadata boundary and compatibility. The context window size is not
exposed by this session.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [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-06 08:18:03 -08:00
DottaandClaude Opus 5.5 e2f0fc9e7d fix(runner): end the detached Codex controller's event stream so graceful shutdown exits promptly (#19483)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The control plane runs native Codex turns through the Paperclip
runner, and a restart must hand those turns to the next controller
> - On a graceful shutdown (SIGTERM), the server detaches each native
Codex session and leaves the runner alive for restart recovery
> - After the detach, the shutdown waited the full 30 s "bounded
heartbeat execution finalizer drain" every time
> - A supervisor with a 30 s graceful window (for example the runner-e2e
supervisor) then sent SIGKILL, so a graceful restart became a hard kill
> - This pull request ends the detached controller's event stream, so
the old run execution unwinds immediately
> - The benefit is that graceful restarts and deploys exit in about one
second and stay graceful

## Linked Issues or Issue Description

**What happened?**
Every graceful shutdown with an active or restart-suspended native Codex
run waited 30 s before exit. The server log showed `graceful heartbeat
run drain complete` with the run in `restartSuspendedRunIds`, then
`bounded heartbeat execution finalizer drain timed out` 30 s later.
Under a supervisor with a 30 s grace period, the server got SIGKILL.

**Steps to reproduce**
1. Start a native Codex run (runner-e2e cell
`everyday-workflows.runner-codex.local.recover-controller`).
2. Stop the server with SIGTERM while the turn is active
(`PAPERCLIP_RUNNER_E2E_CONTROLLER_STOP_SIGNAL=SIGTERM`).
3. See that the server does not exit until the 30 s drain timeout, or
until SIGKILL.

**Expected behavior**
The server exits promptly. The detached run is left for restart
recovery.

**Root cause**
- `drainRunningRunsForShutdown`
(`server/src/services/heartbeat/recovery.ts`) calls
`detachNativeSessionsForRestart` for the native run. This calls
`CodexHarnessSession.detachControllerForRestart`.
- `CodexHarnessSession.detachControllerForRestart`
(`packages/paperclip-runner/src/drivers/codex/codex-harness-session.ts`)
detached the transport but did not close the session `eventQueue`.
- The transport closed its notification queue. `pumpNotifications` then
returned normally and did not close the session queue.
- `executeNativeSession` waited on `eventIterator.next()` for an event
that could never arrive. The heartbeat execution promise (registered at
`server/src/services/heartbeat/queue.ts` in
`activeRunExecutionPromises`) never settled.
- `drainActiveRunExecutions` awaited that promise until the 30 s bound
in `drainRunExecutionFinalizersForShutdown`.

Temporary diagnostics confirmed this: during the hang, the only pending
entry was the `queue.ts` run execution, in the `execution.runtime`
phase, with no pending wakes. Other drivers (Cursor Cloud, external
runnerd) already end their event stream on detach.

## What Changed

- `CodexHarnessSession.detachControllerForRestart` now clears and closes
the session event queue after the transport detaches. The detached
consumer gets end-of-stream. The executor then throws
`NativeControllerDetachedForRestartError`, and heartbeat leaves the run
for restart recovery.
- Buffered events are not delivered to the detached consumer. The next
controller replays them from the runner journal.
- Pending runtime requests stay open for the adopting controller (no
`cancelPendingRequests`).
- If the transport detach fails, the queue stays open. The existing
uncertain-ownership path is unchanged.
- Added a regression test: after detach, the session event iterator
ends, and a late provider notification does not reach the detached
consumer.

## Verification

- Red, then green: `npx vitest run
src/drivers/codex/codex-app-server-driver.lifecycle.test.ts` in
`packages/paperclip-runner`. Before the fix, the new test failed with
`hung`. After the fix, all 22 tests pass.
- `npx vitest run src/drivers/codex src/backends`: 27 files, 457 tests
pass.
- `tsc --noEmit` for `@paperclipai/paperclip-runner` passes.
- Live: `PAPERCLIP_RUNNER_E2E_CONTROLLER_STOP_SIGNAL=SIGTERM pnpm
test:e2e:runner -- --max-automatic-retries 0 --id
everyday-workflows.runner-codex.local.recover-controller`
- Before (unchanged code, reproduced locally): `Paperclip did not stop
within 30000ms; sending SIGKILL`, `server exited code=null
signal=SIGKILL`.
- After, run 1: PASS. Drain complete with the run in
`restartSuspendedRunIds`, then `server exited code=0 signal=null` within
the same second. The next server resumed the run and the task completed.
  - After, run 2: PASS. Same clean exit, no SIGKILL.
- `pnpm verify` returned exit 2 (broader verification required).
Required CI supplies that coverage.
- Note: 21 tests in `src/live/runnerd-restart-continuation.test.ts`,
`runnerd-controller-outage.test.ts` and
`runnerd-codex-transport-warm-attachment.test.ts` fail on this machine
with and without this change. They use the transport directly, not
`CodexHarnessSession`.

## Risks

- Low risk. The change only affects a session after its controller
detaches for restart. That controller has no authority to settle the
run.
- The 30 s finalizer bound stays in place. It still applies when a run
execution is past the provider stream, for example while it persists an
accepted result or settles a workspace. Those durable writes must finish
(or reach their recovery records) before exit, so waiting for them is
correct.
- Dropped buffered events: the old consumer could not persist them
anyway after the transport closed. The next controller replays from the
runner journal.

## Model Used

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

## Checklist

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

> - Paperclip is the open source app people use to manage AI agents for
work
> - Runner, plugin, and Playwright tests verify how those agents run,
connect, and appear in the board
> - Most package tsconfigs exclude tests, and Vitest strips types, so a
type error in those tests fails nothing
> - The tests drifted to 803 real type errors, and some of those errors
hid assertions that checked nothing
> - paperclipai/paperclip#18739 and paperclipai/paperclip#18738 check
the server test tree and four test projects, but not these files
> - This pull request adds a required check for these test files, fixes
every error, and fails CI if a test file is in no program
> - The benefit is that a type error in any test file outside
`server/src` now fails CI

## Linked Issues or Issue Description

Refs #18739
Refs #18738

**What happened?**

517 test files were in no program that required CI checks. A type error
in those files did not fail CI.

**Expected behavior**

CI fails on a type error in any test file.

**Steps to reproduce**

Add `export const value: number = "text";` to a test file under
`packages/paperclip-runner/src` on master. CI stays green, because
`tsconfig.json` excludes test files and Vitest strips types.

**Measured gap**

Each row is a program with its tests included, on one thread. File and
error counts are for head `7d4b5e907a`. Memory peaks were measured
before the two latest merges of master.

| Group | Test files | Real errors | Peak |
| --- | ---: | ---: | ---: |
| `paperclip-runner` Node source | 290 | 644 | 1.09 GiB |
| `paperclip-runner` surfaces | 9 | 12 | 0.42 GiB |
| `paperclip-runner` devtools and examples | 14 | 0 | 0.68 GiB |
| `sandbox-providers/kubernetes` | 21 | 65 | 0.44 GiB |
| 8 other sandbox providers and `bridge-template` | 32 | 4 | 0.34 GiB |
| `plugins/sdk` | 22 | 15 | 0.41 GiB |
| `plugin-workspace-diff` | 5 | 0 | 0.37 GiB |
| `plugin-orchestration-smoke-example` | 1 | 1 | 0.15 GiB |
| `kv-demo-mcp-server` | 3 | 0 | 0.13 GiB |
| `packages/db` scripts | 1 | 0 | 1.01 GiB |
| 8 Playwright suites under `tests/` | 52 | 25 | 1.72 GiB |
| `tests/runner-acceptance` | 3 | 0 | 0.58 GiB |
| `tests/e2e`, through layer declarations | 63 | 37 | at most 1.59 GiB |
| `ui` `AttentionInteractionResolver.test.tsx` | 1 | 0 | n/a |
| **Total** | **517** | **803** | |

27 of the 803 errors came from master while this pull request was open.
Each merge of master brought new test code that no check had read.

Errors that were config errors, not real errors:

- One TS2688 for each provider, because the root install does not
install provider dependencies.
- 9 in the Runner Node program (TS7016 and TS6059).
- 2 in `tsconfig.browser.json`. TypeScript 7 removed `baseUrl`, so that
config was invalid.
- 213 in the Playwright suites (`@/*` aliases and Vite types).
- About 60 in `tests/e2e` (import extensions and aliases).

`packages/plugins/create-paperclip-plugin` has no gap. Its tsconfig
excludes its test, but `cli/tsconfig.json` reaches that test through an
import. `server/test-baselines` also has no gap, because
`tests/lifecycle-baseline` reads it.

## What Changed

- Added `scripts/typecheck-test-programs.mjs` and its tests. The
`Typecheck + Release Registry` job runs it. It checks each program in
`PROGRAMS` on one thread.
- The script fails if a tracked `*.test.ts(x)` or `*.spec.ts(x)` file
outside `server/src` is in no program. The compiler file list decides
this, because `include` drops `foo.test.tsx` when `foo.test.ts` exists.
- Added a `tsconfig.test.json` to each uncovered package, and
`tests/tsconfig.playwright.json`.
- Added `tests/e2e` to `PROJECTS` in
`scripts/typecheck-test-projects.mjs`, with a `tsconfig.json` and a
`tsconfig.layers.json`.
- Added `scripts/typecheck-sandbox-providers`. It is a small workspace
that installs provider SDKs from one lockfile. I did not add a lockfile
inside each provider, because
`scripts/build-standalone-public-packages.mjs` uses a frozen install
when one exists, and that changes what a release resolves.
- Corrected `packages/paperclip-runner/tsconfig.browser.json` for
TypeScript 7.
- Set `rootDir` to `..` in
`packages/paperclip-runner/tsconfig.test.json`, because one Runner test
imports from `packages/adapter-utils/src`. The program does not emit
files, so this changes no build output.
- Moved the Kubernetes provider from `vitest ^3.2.4` to `^4.1.8`, the
version the other providers use. Vitest 3 requires `tinypool ^1.1.1`,
which has two critical advisories (GHSA-85c8-ppgw-ccpr and
GHSA-5gmw-xhrv-c9v3). The first patched version is 2.1.2. Its tests have
the same result on both versions: 189 passed, and the same 12
`file-sync` tests failed.
- Moved the Kubernetes provider from `zod ^3.24.2` to `^4.6.6`, and gave
its two `z.record` calls a key schema. This is the only production
change, and it stays in this pull request. See "Production change for
review".
- Renamed `AttentionInteractionResolver.test.ts` to
`AttentionInteractionResolver.replace.test.ts`, so TypeScript reads the
`.test.tsx` beside it.
- Fixed all 803 errors, with changes to test files and test support
only. The fix for `contracts/native-execution.test.ts` is the same as
the fix in paperclipai/paperclip#18739, so those two changes merge
cleanly.
- Updated `doc/DEVELOPING.md`.

This pull request does not edit `server/scripts/typecheck-layers.mjs`.
paperclipai/paperclip#18739 and paperclipai/paperclip#18738 have both
merged, and this branch includes them.

### Production change for review

The new lockfile made dependency review flag `zod@3.25.76` as a
high-severity advisory (GHSA-mrhx-c7xc-m3w5). The first patched version
is 4.6.6, so 3.x has no fix. The published Kubernetes plugin uses zod to
validate operator config. The zod 3 source does not compile against zod
4, so a typecheck-only override cannot fix this.

The existing tests do not prove this change, because they also passed
with the invalid one-argument `z.record`. So I parsed 45 inputs with the
same schema on zod 3.25.76 and zod 4.6.7:

| Result | Inputs | Detail |
| --- | ---: | --- |
| Identical | 23 | Includes defaults, valid config, and a `__proto__`
key |
| Same outcome, new error wording | 21 | For example, `Invalid url` is
now `Invalid URL`. `validateConfig` shows this text to operators. |
| Different outcome | 1 | `jobTtlSecondsAfterFinished: 2 ** 60` was
valid and is now rejected. The new limit is `Number.MAX_SAFE_INTEGER`. |

I did not test this against a real cluster.

The decision is to keep this change in this pull request. Since then,
paperclipai/paperclip#17364 has moved the rest of the repository to zod
4.6.x, so the Kubernetes plugin now matches it.

### Behavior change for review

`reducedMotion` under `use` is not a Playwright option. It had no effect
in 11 configs and 2 `test.use` calls, so those suites ran with
`no-preference`. The options are now under `contextOptions`, so reduced
motion now takes effect. `app-shell-design.spec.ts` asserts a real
transition, so it now asks for `no-preference` itself.

The baseline manifest has `snapshotCount: 0`, so this invalidates no
published baseline. A frozen trial reference PNG can differ.

### Findings

These are product questions. I did not change them, and I did not hide
them with casts:

1. **`PrpEvent` and its schema disagree.** The TypeScript type in
`src/protocol/replay-contract.ts` requires `normalizedSessionId`. The
`required` list in `protocol/schemas/event.schema.json` does not include
it. `validatePrpEvent` accepts events that the type forbids.
2. **`requestCheckboxConfirmation` is stricter than its runtime.** It
uses the output type, so plugin authors must pass `continuationPolicy`
and `allowDeclineReason`, although the schema gives them defaults.
`z.input` would fix this.
3. **`RetainedRunnerdMaintenanceInput.backend.kind` is a free string.**
The quarantine domain comes from `descriptor.kind`, but the release uses
`proof.backend.kind`. If they differ,
`completeRetainedNativeSessionCleanup` returns 0 and the quarantine
stays. The production caller passes `"runner"` today, so this is not a
live bug.
4. **`CodexHarnessSession.close(input?)` is optional, but the contract
requires `{ reason }`.**

These are tests that verified nothing. I corrected them, and they pass:

- `backends/native-instruction-measurement.test.ts` filtered on
`placement === "core"`, which is not a valid placement. It measured an
empty tool list. It now measures 14 tools.
- `drivers/acpx/codex-acpx-driver.test.ts` checked for `usage.updated`,
which is not an event type.
- The `hostFixture()` helper in `drivers/acpx/runtime-host.test.ts`
ignored two overrides, so
`expect(prepareSandbox).not.toHaveBeenCalled()` could not fail.
- `live/live-session.test.ts` set `agent: "pi"` before a spread that
overwrote it.
- `native-distribution-integrity.test.ts` passed an AbortSignal where
`openCommand` expects `{ signal }`.

These are gaps that this pull request does not close:

- **The root lockfile still has some old zod versions.**
paperclipai/paperclip#17364 moved the direct zod dependencies to 4.6.x.
`pnpm-lock.yaml` still resolves `zod@3.25.76`, `4.1.11`, and `4.4.3`,
which are below the patched 4.6.6.
`packages/paperclip-runner/scripts/pi-distribution/package.json` pins
`zod` to `3.25.76`, and some agent SDKs resolve `4.4.3`. Dependency
review does not flag them, because this pull request does not add them.
- **Dead GitHub coverage.** `chat-adapters-ui-providers.spec.ts`
returned early for GitHub, so four later `provider === "github"`
branches never ran. I removed them. They included the private key file
and paste match, and the reconnect flow. Nothing asserts those behaviors
in that spec now.
- **Provider tests do not run in CI.** The root `vitest.config.ts` lists
only Daytona. The other 8 providers, `bridge-template` included, are
typechecked now, but CI does not run their tests.
- **One `packages/db` test never runs.**
`scripts/clean-poisoned-claude-sessions.test.ts` is outside the
`include` of `packages/db/vitest.config.ts`.
- **`plugin-orchestration-smoke-example` does not collect.**
`@paperclipai/shared` is not linked in its `node_modules`. It passes 3
of 3 with an alias.

## Verification

Run these commands:

```sh
node scripts/typecheck-test-programs.mjs --low-memory
pnpm --filter @paperclipai/server typecheck
node scripts/typecheck-test-projects.mjs --low-memory --reuse-declarations
node --test scripts/typecheck-test-programs.test.mjs
```

Results on macOS (arm64), Node 26.12. CI uses Node 24, and its
`Typecheck + Release Registry` job also passes. At head `7d4b5e907a`,
the test programs, `tests/e2e`, and the script tests pass again with 0
errors. The times and peaks below come from the run before the two
latest merges of master:

| Check | Result |
| --- | --- |
| Test programs, `--low-memory` | Exit 0, 0 errors, 42 seconds, peak
1.78 GiB |
| Server layers, low memory | Exit 0, 0 errors, 38 seconds, peak 3.41
GiB |
| Test projects, including `tests/e2e` | Exit 0, 0 errors, 13 seconds,
peak 1.59 GiB |
| Script tests | 12 passed |
| Injected type error in each of 23 places | Each one failed its program
with TS2322 |
| Coverage check | 0 unchecked files, 10 seconds |

I did not run this in a 4 GiB container. The peaks above are `maximum
resident set size` for the largest single process.

**Runtime check of the fixes.** Types do not prove that a test still
works, so I ran the suites before the two latest merges of master:

| Suite | Result |
| --- | --- |
| Runner, full suite | 4,885 passed, 3 failed, 13 skipped |
| Sandbox providers, 10 packages | 939 passed, 12 failed, 36 skipped |
| Plugin SDK | 200 passed |
| `kv-demo-mcp-server` | 12 passed |
| Renamed UI test and its sibling | 4 passed |

All 15 failures exist without these fixes or come from the local
machine:

- **3 Cursor Cloud timeouts.** `artifact-delivery.test.ts` has 1 and
`cursor-cloud-driver.test.ts` has 2. They time out in a full local run,
and all 133 tests in those files with `codex-security-config.test.ts`
pass when run alone. They fail in the same way with those two files at
their unedited versions.
- **12 Kubernetes `file-sync.test.ts` failures.** They fail in the same
way before and after the fixes. The first failure is that
`/proc/self/fd` does not exist on macOS.

After each of the two latest merges of master, I ran only the Runner
test files that I changed:

| Merge | Result |
| --- | --- |
| `885c249cb9`, 8 files | 534 passed |
| `7d4b5e907a`, 7 files | 672 passed, 16 failed |

The 16 failures are in the two `source-reconciliation` test files. They
fail in the same way with master's unedited versions of those files. 15
of them fail with `ACPX sidecar exited at initialize`, so I think my
local build of the Rust and sidecar binaries is out of date. I did not
rebuild them to check. CI builds them from scratch, and all Runner jobs
pass in CI at `7d4b5e907a`.

**Not run:**

- `tests/e2e` and the 8 Playwright suites need a server, so most of them
have only compile checks. `playwright test --list` loads 308 tests from
the main e2e config.
- `packages/plugins/sandbox-providers/kubernetes/test/integration` needs
a cluster, so it has only compile checks.
- I did not run the full `pnpm test:run` or root `pnpm build`. Required
CI runs them.

**Reduced motion.** I ran 6 motion-sensitive Storybook specs in system
Chrome with a temporary config, once with `reduce` and once with
`no-preference`. Both runs had 45 passed and the same 5 failed. So
reduced motion did not change those results. The 5 failures do not
depend on motion. At least one comes from the browser, because system
Chrome prints `oklch(55.6% 0 0)` where the test expects `oklch(0.556 0
0)`. I did not run the other Storybook specs. The `storybook-visual`
label runs that workflow.

## Risks

- **A new lockfile to maintain.** A change to a provider `package.json`
now fails the typecheck job until someone refreshes
`scripts/typecheck-sandbox-providers/pnpm-lock.yaml`. The failure
message gives the command.
- **The job has a new install.** The step downloads provider SDKs, so it
depends on the npm registry. It uses `--frozen-lockfile` and
`--ignore-scripts`.
- **The local install replaces provider `node_modules`.** A contributor
with a standalone provider install must reinstall that provider after
running the script.
- **Reduced motion can change Storybook results.** See "Behavior change
for review". That workflow is not part of required CI.
- **Kubernetes plugin validation changed.** A release of this plugin
ships zod 4. Error wording changes, and very large integers are now
rejected. See "Production change for review".
- **Master moves fast.** Each merge of master brought new test code with
type errors, 27 so far. After this merges, the check reports those
errors in the pull request that adds them.

## Model Used

- Provider: Anthropic Claude, through Claude Code
- Model: Claude Opus 5.5 (`claude-opus-5-5`)
- Capabilities: tool use, code execution, and subagents for the type
fixes in separate files

## 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)
- [ ] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived
details. The app generated the branch name.
- [ ] I have run tests locally and they pass. See the failures explained
in Verification.
- [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-06 07:33:59 -08:00
DottaandClaude Opus 5.5 0252421d5c fix(opencode): isolate the native runner probe's OpenCode home (#19469)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Before a native OpenCode agent can run a task, agent setup runs a
harness test. The test sends one "Respond with hello." prompt through
`opencode run`.
> - The native OpenCode driver gives every OpenCode process its own
`HOME` and XDG data, cache, and state directories. The harness test did
not. It used the host user's `HOME`, so it shared
`~/.local/share/opencode/opencode.db` with every other OpenCode process
on the host.
> - OpenCode keeps one SQLite database for each data directory. When two
OpenCode processes start at the same time on one database, one process
can fail in about one second with `Error: Unexpected error / database is
locked`.
> - The harness test then reports `opencode_hello_probe_failed`. Three
failures pause agent verification, so the agent's first task stays
queued and never starts.
> - This pull request gives each local native harness test its own
temporary OpenCode home, the same as the native driver.
> - The benefit is that a native OpenCode agent passes setup when other
OpenCode processes run on the same host, and its first task starts.

## Linked Issues or Issue Description

No public issue exists. This is the bug description.

Refs #14393 (open, related): it isolates the OpenCode data directory for
the legacy `opencode_local` execution path. This pull request covers a
different path: the native runner harness test.

**What happened?**
Two native OpenCode runner end-to-end cells failed before any provider
turn and before their planned control-plane restart:
`stock-harness.runner-opencode.local.continuity-restart` ("Timed out
waiting for chat turn settles to waiting") and
`local-session-integrity.runner-opencode.local.structured-question-restart-resume`
("Timed out waiting for pending user question"). In both cells, the
server log shows `Agent lifecycle operation failed ... phase: verifying
... failureCode: harness_test_failed` three times. The run stayed
`queued` with `startedAt: null`. The second cell failed two attempts 2
seconds apart, which is shorter than one successful hello turn.

**Steps to reproduce**
1. Run several native OpenCode harness tests at the same time with the
host `HOME` (or run one while another OpenCode process starts).
2. Some `opencode run` processes exit 1 within about one second with
`database is locked`.
3. Agent verification records `harness_test_failed`, and after three
failures it pauses.

**Expected behavior**
The harness test uses an isolated OpenCode home, the same as the native
runtime that it qualifies. Other OpenCode processes on the host do not
affect it.

## What Changed

- `packages/adapters/opencode-local/src/server/test.ts`: for a local
`paperclip_runner` probe, create a temporary home and set `HOME`,
`XDG_DATA_HOME`, `XDG_CACHE_HOME`, and `XDG_STATE_HOME` inside it. Also
set `OPENCODE_DISABLE_MODELS_FETCH` and
`OPENCODE_DISABLE_DEFAULT_PLUGINS`, as the native driver does, so a
fresh cache does not fetch a catalog or install default plugins. Remove
the directory after the probe.
- The legacy `opencode_local` adapter keeps the host home, because its
`opencode auth login` credentials live there. Remote (sandbox) probes do
not change.
- `packages/adapters/opencode-local/src/server/test.remote.test.ts`: add
a regression for two concurrent native probes (each gets its own data
directory, and the directory is removed), and a sibling case that the
legacy adapter keeps the host home.

## Verification

- Red, unit: the new concurrent-probe test failed on the unfixed code
(`expected 'user' to match /^paperclip-opencode-native-probe-home-/`).
It passes with the fix. `cd packages/adapters/opencode-local && npx
vitest run src/server` gives 89/89 passed. `tsc --noEmit` passes.
- Red, real process: the unfixed `testEnvironment` against the real
pinned OpenCode 1.18.34 and OpenRouter, 12 concurrent probes, 3 rounds:
2 of 36 probes failed with `opencode_hello_probe_failed` (`database is
locked`). A plain shell reproduction with 6 concurrent `opencode run`
processes on the shared home: 2 of 6 failed with `database is locked`.
- Green, real process: the fixed `testEnvironment`, same 12 x 3 rounds:
36 of 36 passed with `opencode_hello_probe_passed`.
- End-to-end reruns of both failing cells: two rounds at this revision
with the real pinned OpenCode and OpenRouter (`pnpm test:e2e:runner --
--max-automatic-retries 0 --id <cell>`).
`stock-harness.runner-opencode.local.continuity-restart`: 2 of 2 passed.
`local-session-integrity.runner-opencode.local.structured-question-restart-resume`:
round 1 passed. Round 2 passed agent verification, asked the question,
and crossed the restart. After the user answered, the continuation run
failed with a different error: `runner_local_connect_failed: PRP command
run.attach timed out`. That failure is after the harness test and is not
changed by this pull request. It is tracked as a separate signature. In
all four reruns, agent verification passed on the first attempt.

## Risks

- Low risk. The change applies only to local native OpenCode harness
tests. The native runtime already uses an isolated home, so the test now
matches the runtime that it qualifies.
- A native probe no longer reads OpenCode credentials from the host
user's home. This is intended: native OpenCode agents receive provider
credentials through the agent environment, and the native driver does
not read the host home either.
- Each probe starts with a cold cache. With the catalog fetch and
default plugins disabled, a probe took 4 to 6 seconds in local tests,
the same as before.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code with tool use (shell, file edits, 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
- [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-06 09:27:13 -06:00
Nicky LeachandPaperclip 26071353d2 feat(plugins): define settled task release and resume (#19468)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Environment plugins provide Runner tasks through the public SDK.
> - A settled turn can leave resources that a later run can reuse.
> - Core needs a shared contract to end active use and request the same
logical session later.
> - The host must retain control of capacity and report when reuse is
unavailable.
> - This pull request adds an optional release and resume contract with
strict validation and documentation.
> - The benefit is a common boundary for later host and Core integration
across Codex and ACPX.

## Linked Issues or Issue Description

**Subsystem affected**

packages/plugins — plugin SDK and its environment task documentation.

**Problem or motivation**

The SDK has stop and completion operations. It has no explicit contract
to release active use after a settled turn and later request the same
logical session without promising retention or the same process.

**Proposed solution**

Add a versioned optional capability, release for the identified session,
and resume with fresh transient authentication. Define two explicit
resume outcomes: resumed or unavailable. Keep authorization, safe
retries and ordinary attachment requirements explicit.

**Alternatives considered**

A provider-specific command would couple Core to one host. Reusing stop,
completion or archive would change their existing meaning. A new
checkpoint format or Runner mode is unnecessary.

**Roadmap alignment**

This extends the existing plugin and sandbox boundaries in ROADMAP.md.
It adds no Core caller, provider implementation or runtime activation.

**Additional context**

The duplicate search found #19371, which fixes Core failure cleanup for
a session resume owner. Its scope is separate. Related recovery work in
#19157 and admission work in #19196 remain unchanged.

## What Changed

- Add strict release and resume operation/result schemas and optional
releaseResumeVersion: 1.
- Reuse the existing task identity, inferred SDK exports and worker RPC
types.
- Define settled release, host retention policy, atomic reuse
reservation, fresh authentication, retries that cannot release a newer
owner and ordinary recovery.
- Add schema and serialized worker RPC tests, including legacy replies
and failure cases.

## Verification

- SDK typecheck: `node_modules/.bin/tsc -p
packages/plugins/sdk/tsconfig.json --noEmit` passed.
- From `packages/plugins/sdk`, `../../../node_modules/.bin/vitest run
--config vitest.config.ts tests/environment-task-release-resume.test.ts
tests/environment-tasks.test.ts tests/environment-task-error.test.ts
tests/environment-task-error-code.test.ts tests/worker-rpc-host.test.ts`
passed: 94 tests across five suites.
- `git diff --check` passed.
- [Full GitHub
CI](https://github.com/paperclipai/paperclip/actions/runs/42238743059)
passed on `a55376801c91af29b68a13304d085706e4d7ed6f`. Standard Greptile
scored that head 5/5 with no comments or unresolved review threads.
- Full regressions run in GitHub CI, per the requested verification
scope. No VM, deployment or live provider test was run. These tests
prove the SDK boundary, not host retention or live attachment.

## Risks

- This is an optional contract. Runtime behavior stays unchanged until
later host and Core work uses it.
- Older strict Core schemas reject the new description field. Providers
must coordinate capability advertisement with a compatible Core.
- Host implementations must enforce settlement, exact authority,
exclusive claims and safe retries. Unknown outcomes remain errors. An
unavailable result never proves physical termination.

> 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

OpenAI GPT-6.1 Sol (`gpt-6.1-sol`), high reasoning, with repository
inspection, code editing and local test tools. Context window size was
not exposed by the 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-06 07:23:28 -08:00
DottaandPaperclip 6ec4180ea1 fix: preserve ordinary diagnostic text during environment secret redaction (#19461)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Run diagnostics help operators find the cause of a failed agent run.
> - The failure reporter treated unknown environment settings as
credentials.
> - It replaced every use of those values in diagnostic text.
> - A flag set to `1`, `2`, or `safe` removed parts of dates, model
names, versions, and normal words.
> - This pull request preserves explicit ordinary settings and keeps
unknown values private.
> - Operators can read the failure details while real credentials stay
masked.

## Linked Issues or Issue Description

**What happened?**

Run-failure contexts contained redaction markers inside timestamps,
model names, driver versions, and the word "safely". Provider failure
diagnostics used the same broad environment-value rule.

**Expected behavior**

Ordinary flags, counts, paths, regions, and mode labels leave diagnostic
text readable. API keys, URL passwords, and declared secret bindings
remain masked.

**Steps to reproduce**

1. Set ordinary environment settings to `1`, `2`, and `safe`.
2. Report a failed native run with a timestamp, `gpt-6.1-sol`,
`phase6-v2`, and "safely" in its details.
3. Inspect the run failure report. Before this fix, unrelated fragments
are replaced.

Related work: #13668 addresses a separate model-ID false positive.
#11311 and #15524 address credential capture at other boundaries. This
change keeps the existing command scanner and registered-secret
replacement.

## What Changed

- Add a shared environment selector in adapter-utils. Keep unknown
values private. Exempt explicit ordinary process settings, typed flags
and counts, mode labels, and key selectors.
- Use the same selector for run-failure reports and inherited provider
failure environments. Keep one policy for both consumers.
- Keep configured provider bindings private because that API does not
carry separate secret-reference metadata.
- Preserve short declared secrets, unknown opaque values, encoded
credentials, and inherited Bedrock tokens. Add regressions for ordinary
`SORT_KEY` values and both Bedrock consumers.
- Add screenshot-shaped regressions, real database reporter coverage,
and credential tests.
- Document the policy in `doc/observability.md`. The optional monitoring
gate stays in place.

## Verification

Linux, Node v25.9.0, pnpm 9.15.4. The published tree matches the tested
local tree `cb236f072cca195e4c2cfb4034c7cc06ea7adbc5`.

- Before the fix, four diagnostic regressions failed. The output
reproduced the damaged timestamp, model, version, and word.
- Review regressions: six tests failed on the first revision. They
proved the missed Bedrock token, opaque inherited-value exposure, and
key-selector false positive. All pass after the correction.
- 555 focused tests pass on the final source tree.
- `cd server && pnpm exec vitest run
src/services/__tests__/run-failure-diagnostics.test.ts
src/__tests__/redaction.test.ts src/__tests__/redact-sensitive.test.ts
src/__tests__/run-secret-redaction.test.ts`: 206 passed.
- `cd server && pnpm exec vitest run
src/services/__tests__/run-failure-report.test.ts`: 289 passed, with
real embedded PostgreSQL.
- `pnpm exec vitest run
packages/adapter-utils/src/command-redaction.test.ts
packages/adapter-utils/src/acpx-engine/terminal-session-failure.test.ts`:
60 passed.
- `pnpm --filter @paperclipai/adapter-utils typecheck`: passed.
- `git diff --check`: passed.
- The first reporter attempt could not load the unbuilt plugin SDK. It
passed after `pnpm --filter @paperclipai/plugin-sdk ensure-build-deps`.
- The filtered install omitted the CLI tsx loader, so `pnpm verify
--explain` could not start. The equivalent verifier entry points ran
with the installed server loader and returned exit 2: broader CI
coverage is required for the observability document.
- Full local build/typecheck and browser suites were not duplicated.
[Final-revision
CI](https://github.com/paperclipai/paperclip/actions/runs/42231412547)
passed on `013960a4c6fa6279e1f28b0f6af5f036d7cad5a7`, attempt 1. This
includes typecheck, build, general/server/chat tests, serialized suites,
Runner checks, eight E2E shards, and the canary dry run.
- [Greptile
review](https://github.com/paperclipai/paperclip/pull/19461#issuecomment-6686592783)
scored this head 5/5 with zero new findings. All four inline threads are
resolved. The security rescan and Snyk status passed. The PR has no
merge conflicts.

## Risks

- Ordinary-setting exemptions need a known name or a name/value type
rule. Unknown opaque values remain private. Declared secret bindings and
recognizable credentials override these exemptions.
- A real short secret still masks matching fragments. Removing that
protection would expose the secret.
- Existing stored redaction markers cannot restore their original text.
This fix applies when new diagnostics are produced.
- No migration, dependency, workflow, authorization, or default
monitoring changes.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. This change is a focused bug
fix.

## Model Used

OpenAI GPT-6, through Codex, with shell execution and file tools. The
host does not expose the exact runtime variant or context window size.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 09:01:55 -06:00
Devin Foley e82210386f fix(workspaces): tolerate interrupted managed clone staging beside nested repositories (#19374)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A task workspace can hold managed nested repositories in
`.paperclip-repositories/<name>`. Sandbox and SSH runs read each nested
repository before they stage the workspace.
> - The server stages a new nested clone in
`.paperclip-repositories/<checkout>.clone-XXXXXX` and renames it when
the clone is complete. If the process stops during the clone, this
staging directory stays.
> - Workspace preparation keeps `.clone-` entries on purpose. But the
snapshot reader and the SSH listing reject every directory name that has
a dot. One leftover staging directory makes every later sandbox or SSH
run on that workspace fail with `Invalid project repository directory`.
> - This pull request makes both readers skip only server-owned
`<name>.clone-<suffix>` directories. Sandbox and SSH staging also
exclude these directories from the upload and from the restore.
> - The benefit is that an interrupted clone no longer blocks all later
runs on the workspace. Other unexpected entries still fail closed.

## Linked Issues or Issue Description

Refs #17382

**What happened?**
A hosted staging run failed before the agent started. The error was
`Error: Invalid project repository directory` from
`readManagedWorkspaceRepositories`, through `readGitWorkspaceSnapshot`
in `prepareSandboxManagedRuntime`. The server had restarted about five
minutes before the run. The run was an automatic retry.

**Expected behavior**
A leftover clone staging directory from an interrupted clone does not
stop later runs. The sandbox or SSH host does not receive the partial
clone. The restore does not delete or change the host staging directory,
because a concurrent preparation can own it. An unexpected directory
that is not a staging directory still fails closed.

**Steps to reproduce**
1. Create a Git workspace with a managed nested repository at
`.paperclip-repositories/backend`.
2. Create the directory `.paperclip-repositories/backend.clone-Ab12Cd`
with a `.git` directory and a file in it. This is the state after an
interrupted clone.
3. Call `readGitWorkspaceSnapshot` on the workspace, or start a sandbox
or SSH run on it.
4. Before this change, the call throws `Invalid project repository
directory`.

**Additional context**
`materializeManagedProjectWorkspace` creates the staging directory with
`fs.mkdtemp(\`${cwd}.clone-\`)`. Its cleanup does not run when the
process stops. `prepareProjectRepositoryWorkspaces` already skips
entries that contain `.clone-` when it retires stale entries. #17382
moved the common nested path to shared worktrees, but the fallback path
still stages in place. #18534 protects nested `.git` directories during
copy-back. It does not cover staging directories.

## What Changed

- `git-workspace-sync.ts`: Add `PROJECT_REPOSITORY_STAGING_PATTERN`
(`^[a-zA-Z0-9_-]+\.clone-[a-zA-Z0-9_-]+$`).
`readManagedWorkspaceRepositories` skips a directory that matches it.
Add `listProjectRepositoryStagingPaths`, which lists these directories.
- `ssh.ts`: `listLocalProjectRepositories` skips the same directories.
`prepareWorkspaceForSshExecution` excludes the staging paths from the
upload when no completed repository exists, and returns them as
`excludedPaths`.
- `remote-managed-runtime.ts`: Add `excludedPaths` to the SSH workspace
baseline excludes. The restore does not download them, and the merge
does not change them on the host.
- `sandbox-managed-runtime.ts`: Add the listed staging paths to the
workspace archive excludes and to the restore excludes.
- New test file `managed-clone-staging.test.ts`:
- The snapshot and the SSH listing skip the staging directory and keep
the nested repository.
- An unexpected entry such as `backend.backup` still fails in both
readers.
- A sandbox round trip does not ship the staging directory, restores the
nested repository change, and keeps the host staging directory.
- An SSH round trip with only a staging directory does not ship it,
restores a tracked-file change, and keeps the host staging directory and
its `.git/HEAD`.

## Verification

- `cd packages/adapter-utils && PATH=$PATH:/usr/sbin npx vitest run
src/managed-clone-staging.test.ts`: 4 pass, with a real sshd.
- Red: with the source changes reverted, the staging tests fail. Before
the change, the reader fails with the exact production message `Invalid
project repository directory`. Before the SSH fix, the SSH round trip
failed with `expected 'present' to be 'absent'`. Without the restore
exclude, the sandbox restore deletes the host staging directory.
- `npx vitest run src/ssh-fixture.test.ts
src/remote-managed-runtime.test.ts src/ssh-command-managed.test.ts
src/managed-clone-staging.test.ts src/git-workspace-sync.test.ts
src/sandbox-managed-runtime.test.ts`: 218 pass. `ssh-fixture.test.ts`
ran all 25 tests with a real sshd. 3 tests in
`git-workspace-sync.test.ts` fail (two "rejects a checkout during
integration" cases and "does not inherit a poisoned GIT_CONFIG_GLOBAL").
They fail in the same way on unmodified master in the test sandbox, so
they are environment failures.
- `npx tsc --noEmit -p packages/adapter-utils`: passes.

## Risks

- Low risk. The skip matches only the exact staging name shape that the
server creates. Every other unexpected name still fails closed.
- The excludes apply only to directories that exist when staging starts.
A staging directory that appears during the run is not in the baseline,
so the restore does not touch it.
- A user directory with a name like `name.clone-x` in
`.paperclip-repositories` is now ignored and not rejected. This
directory belongs to the server, so this is the expected behavior.

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

## Model Used

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

## 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)
2026-11-06 06:48:13 -08:00
Devin FoleyandClaude Opus 5.5 65c5d8d600 fix(claude-local): start approve-all sandbox ACP sessions in bypassPermissions (#19430)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The `claude_local` adapter runs Claude through the ACP engine
(claude-agent-acp) inside remote sandboxes
> - Paperclip seeds the sandbox Claude settings with
`permissions.defaultMode: "default"`, so Claude asks acpx for permission
before every MCP call and every Bash call that is not read-only
> - In the default `approve-all` mode, acpx approves every request. The
permission round trip gives no access control, but it is the step where
tool calls stall
> - Intermittently, the Claude CLI never sends that permission request.
The call stays `pending` until the 30-minute stalled-tool watchdog ends
the turn (`acpx_tool_stalled`)
> - This pull request starts approve-all sandbox sessions in
`bypassPermissions`, so Claude does not use the permission round trip at
all
> - The benefit is that sandbox runs no longer lose about 35 minutes per
stall, and `approve-reads` and `deny-all` agents keep their current
permission behavior

## Linked Issues or Issue Description

Refs #16688
Refs #16769

**What happened?**
Sandbox `claude_local` ACP runs ended with `acpx_tool_stalled`: "A
pending tool call (kind other) received no update for 30 minutes, so the
run ended the turn instead of waiting for the execution timeout." The
diagnostics from #16688 and #16769 show the shape. The stalled call got
its `pending` update and its input updates. Then its permission request
never reached acpx. Finished calls of the same kind in the same run did
send a request. The stall occurred on MCP calls and on Bash calls, in
the main agent and in background subagents. Calls that need no
permission (ToolSearch, read-only Bash) never stalled.

**Expected behavior**
In `approve-all` mode, a tool call must not wait on a permission round
trip that acpx always approves.

**Steps to reproduce**
1. Run a `claude_local` agent with the ACP engine on a sandbox target,
with the default `approve-all` permission mode.
2. Make many MCP or network Bash calls during the turn.
3. Sometimes a call stays `pending` with no permission request. The
watchdog then ends the turn after 30 minutes.

**Agent adapter(s) involved**
Claude Code (`claude_local`, ACP engine, sandbox target).

## What Changed

- `claude-config.ts`: `prepareClaudeConfigSeed` accepts
`permissionDefaultMode` (`"default"` or `"bypassPermissions"`). The
sanitized seed writes that value into `permissions.defaultMode`. In
bypass mode the seed writes `settings.json` also when the host has no
`settings.json`. Hooks, MCP servers and host permission rules are still
removed.
- `claude-config.ts`: `overrideRemoteClaudeLocalPermissionMode` sets
`bypassPermissions` in the **sandbox copy** of the workspace
`.claude/settings.local.json`. The engine writes that file with
`defaultMode: "default"`, and the local tier overrides the user seed.
Before workspace copy-back, the override is reset in the sandbox, so
copy-back never treats it as an agent edit. After copy-back, if the host
file still has `bypassPermissions`, only `defaultMode` is put back. Host
and agent edits to other settings are kept.
- `acp.ts`: the sandbox managed-home seam reads the run's effective ACPX
permission mode. If the mode is `approve-all`, the seam seeds
`bypassPermissions`, applies the workspace override above, and sets
`IS_SANDBOX=1` through the existing `claudeSandboxPermissionEnv` helper.
This lets the bypass apply when the sandbox user is root, the same rule
as the CLI lane. The mode resolution is the same as the engine's: an
absent value or `"default"` becomes `approve-all`. An unrecognized value
keeps `"default"`.
- These keep `defaultMode: "default"` and do not change:
`approve-reads`, `deny-all`, the CLI lane, the Test probes, managed AI
connections, and an operator `CLAUDE_CONFIG_DIR`.
- Tests: two seed tests in `claude-config.test.ts`. In `acp.test.ts`: an
end-to-end executor test for five permission modes (absent,
`approve-all`, `default`, `approve-reads`, `deny-all`) that checks the
mode Claude actually gets during the turn and that the host settings
keep `default`, and a test that host and agent edits to the workspace
settings survive copy-back without `bypassPermissions`.

## Verification

- `pnpm exec vitest run packages/adapters/claude-local`: 393 passed, 1
skipped (before the copy-back commits); full CI is green on the current
head.
- Regression proof: with the new tests and the old `acp.ts` and
`claude-config.ts`, the seed tests and the absent, `approve-all` and
`default` cases fail (`expected 'default' to be 'bypassPermissions'`).
The `approve-reads` and `deny-all` cases pass on both versions, because
their behavior does not change.
- `pnpm exec tsc --noEmit` in `packages/adapters/claude-local`: clean.

## Risks

- Behavior change for sandbox `claude_local` ACP runs in `approve-all`
mode (the default): Claude does not ask permission for tools. Paperclip
already approves every request in this mode, so the access outcome does
not change. Claude's bypass-immune safety checks still go through
`canUseTool`.
- The seed directory is content-addressed, so the bypass seed and the
default seed are different snapshots and do not overwrite each other.
- Agents that set `approve-reads` or `deny-all` keep the permission
round trip. The stall can still occur for them.

## Model Used

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

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] 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-06 06:44:43 -08:00
DottaandPaperclip 21d2a577bb feat: show and change routine responsible users (#19128)
## Thinking Path

> - Paperclip helps people manage AI agents and their work.
> - Routines create tasks for repeated work.
> - Each routine has a responsible person.
> - The Overview did not show this person or permit a change.
> - People need to see who will be responsible for automatic tasks.
> - This pull request adds a small name picker beside Default agent.
> - Authorized people can adopt a routine or transfer it to an active
company member.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

The routine Overview and the routine update API.

**Subsystem affected**

UI, server, and shared contracts.

**Current behavior**

The routine stores a responsible user. The Overview does not show that
person. The update API cannot change that person.

**Proposed behavior**

Show Responsible user beside Default agent. Click the name to search
active company members. Save only the new person and the current
revision ID.

**Reason and benefit**

People can see who owns future automatic tasks. An authorized person can
adopt a routine without making a new routine.

**Breaking changes**

The detail response adds an optional actor-specific field. The update
API adds an optional responsible user ID. Existing requests keep their
current behavior.

Related: #17368 covers local Board ownership migration. This change adds
the routine control and explicit transfer.

## What Changed

- Show a quiet, searchable person picker beside Default agent.
- Explain that automatic tasks use this person. Manual runs use the
person who starts them.
- Permit human operators with `tasks:assign`, instance administrators,
and local operators to transfer responsibility.
- Check active company membership inside the update transaction.
- Save a revision and record the old and new person in the activity log.
- Keep existing task responsibility and ownership during revision
restore.
- Show errors for failed saves and stale revisions.
- Add tests for permissions, company boundaries, transfers, inheritance,
and picker states.
- Keep the picker disabled until saved data is refreshed. Refresh the
activity list after a transfer.
- Capture the actual previous person under the update row lock for the
activity record.
- Use shared query error handling and retain cached people during a
connection failure.

## Verification

- Focused checks cover 145 tests in 5 files. The first post-fix run
passed 144 tests. One existing Fireflies route test timed out during the
first import while the UI typecheck also ran. The same test passed in
the pre-fix run. The isolated route suite then passed all 21 tests with
no source change.
- The new tests reproduce slow detail refreshes, activity refresh, and
two concurrent transfers without a base revision. The concurrency test
uses PostgreSQL row locks.
- UI and server TypeScript checks passed at commit `1df9587eb`.
- The UI production build passed for the updated head.
- The branch includes master through `26391707f`.
- Design token gates, query-error policy, and Git whitespace checks
passed. The query-error policy reports zero findings.
- Local browser checks passed on desktop and mobile for the initial
feature commit. Search, selection, reload persistence, and revision
creation passed. No page errors or horizontal overflow were found. The
follow-up does not change the normal layout.
- Earlier full local checks could not finish: Cargo was absent, and
concurrent full test/build checks exceeded the 8 GiB memory limit. These
attempts are not passes.
- The first CI run failed on two error-rendering policy findings. That
failure is fixed locally. The three findings from the first Greptile
review are fixed and their threads are resolved.
- Greptile reviewed `1df9587eb2d3e034c2769b1f6955ac52323c95bb` and
scored it 5/5. It confirms all three prior findings are addressed. There
are no open review threads.
- [CI attempt
1](https://github.com/paperclipai/paperclip/actions/runs/42046369415/attempts/1)
passed 53 checks. The server 8/12 shard failed only because the existing
`history-http` runtime fixture did not become ready within 10 seconds
(99 refused connections). The fixture and runtime source are unchanged
from the merged master. That case passed locally on the same PR head
with no source change. Its full 75-test suite also passed locally. The
cause of the CI failure remains unknown.
- [CI attempt
2](https://github.com/paperclipai/paperclip/actions/runs/42046369415/attempts/2)
passed on `1df9587eb`: 54 successful checks, 3 skipped, and 1 neutral.
No failed or pending checks remained. The original fixture-readiness
failure remains recorded above.
- A later review found that viewers with an explicit `tasks:assign`
grant could see an editable picker even though writes were denied.
Commit `8b95ee707` now mirrors active-membership and viewer restrictions
in the GET capability flag. Instance-admin and local-operator behavior
is preserved.
- Three membership regressions failed on the incorrect editable flag
before the fix. After the fix, all 43 routine-route and company-access
cases passed (`--testTimeout 60000` accommodated this sandbox's cold
route imports; no repository timeout was changed). The first pre-fix run
timed out during the cold import in one case; the second pre-fix run
reproduced all three assertion failures.
- Layered server typechecking and whitespace checks passed after the
permission fix.
- [Fresh
CI](https://github.com/paperclipai/paperclip/actions/runs/42057752313)
passed on `8b95ee7075417527687b4a86cbaf63ba87408364` without a retry.
Greptile reviewed this exact head, scored it 5/5, and added no new
findings. All four review threads are resolved. The security status is
also successful.
- The separate Paperclip Review check is still queued because its
assigned review worker requires runtime recovery. This is the remaining
check before completion; the CI workflow itself passed.

## Risks

- A transfer changes the person used for future automatic tasks.
Existing tasks keep their current person.
- Only people can transfer responsibility. The server checks the
permission again on every write.
- A stale revision rejects the save. The user must reload before
retrying.
- No database migration is required.

## Model Used

OpenAI Codex, based on GPT-6, with reasoning, tool use, and code
execution. The exact runtime model ID and context window are not exposed
to this session.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green (CI passed; separate Paperclip
Review check pending)
- [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-06 05:47:30 -06:00
DottaandClaude Opus 5.5 7e3caa7bac fix(tree-control): resume work a pause gate refused after release (#19421)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The board can pause an issue tree. A pause stops work. Resume or
release must continue that work once.
> - PR #19394 made release re-dispatch a run that a pause Stop
cancelled.
> - That rule covers only a run that an operator Stop ended and that the
hold snapshot named.
> - A scheduled retry that comes due during a pause is not stopped. The
dispatch gate cancels it with `issue_paused`. The release did not see it
as owed work, so the task stalled with no run.
> - This pull request makes every gate that refuses work for a pause
record the covering hold. Release then re-dispatches that work exactly
once.
> - The benefit is that a paused task with a parked retry or a queued
run continues after release, with no manual wake.

## Linked Issues or Issue Description

Refs #19394

**What happened?**
A child task had a `workspace_busy` scheduled retry. The board paused
the parent tree. The retry came due during the pause. The
scheduled-retry gate cancelled it with `issue_paused` ("Scheduled retry
suppressed because the issue is held by an active subtree pause hold").
The board released the hold. Four minutes later the child was still
`in_progress` with no run, no execution blocker, and no new activity.

**Expected behavior**
Release (or `mode: "resume"`) re-dispatches the refused work exactly
once. No reconciliation and no user message are necessary.
Stranded-issue recovery does not add a second run.

**Steps to reproduce**
1. Give an assigned child task a parked `workspace_busy` scheduled
retry.
2. Pause the parent tree.
3. Let the retry come due while the pause is active. The gate cancels it
with `issue_paused`.
4. Release the pause. No run starts for the child.

**Paperclip version or commit**
master at 4d49a31286 (includes #19394).

**Agent adapter(s) involved**
Not adapter-specific (core bug).

## What Changed

- `issue-tree-control.ts`: add `ISSUE_PAUSED_ERROR_CODE` and
`pauseSuppressionResult()`. A gate records `treeHoldSuppression: {
holdId, rootIssueId }` on a run that it refused for a pause.
- `findOwedPauseResume` now treats the latest run as owed when it is an
`issue_paused` cancellation under a pause hold. It uses the recorded
hold. For an unmarked run (a scheduled retry cancelled before this
change), it uses a snapshot pause that was active at cancellation. The
operator Stop rule does not change.
- Scheduled-retry gate (`run-dispatch/adapters/postgres.ts`,
`cancelSuppressedRetryInTx`): locks the pause row `FOR SHARE` and
records the covering hold. A concurrent release either sees the refusal,
or the retry sees no pause and runs normally.
- Queued-run claim gate (`heartbeat/queue.ts`): cancels with
`issue_paused` and the hold marker, after the agent start lock is
released. Before, the cancel re-entered that lock from inside it, and
the agent's queue stalled until the 30-second stale-lock timeout. After
the refusal is saved, the gate re-checks the hold and resumes the task
itself if release already finished (`resumeIfPauseAlreadyReleased`).
- Release (`issue-tree-resume.ts`, route): revisits the snapshot members
and every task with a run refused for that hold, also when `wakeAgents`
is set. This covers tasks created under the tree after the snapshot and
tasks moved out before release.
- `packages/db`: partial index
`heartbeat_runs_tree_hold_suppression_idx` (migration 0436, `CREATE
INDEX CONCURRENTLY`, registered as a top-level migration) for that
lookup.
- Docs: `doc/execution-semantics.md` (gate-refused work, race handling,
non-assignee and pre-fix scope) and `doc/DATABASE.md` (index).

## Verification

- Red before the fix, green after: `pnpm --filter @paperclipai/server
exec vitest run src/__tests__/issue-tree-pause-resume-run.test.ts` (21
tests).
- "resumes a parked retry the pause suppressed exactly once after
release / resume / legacy": before, `expected [] to have a length of 1`
(no resumed run). After, one `issue_tree_resumed` run with
`interruptedRunId` = the suppressed retry, no execution blocker, and the
task finished. `reconcileStrandedAssignedIssues` added no run during the
hold or after release.
- "resumes a queued run the claim gate refused during the hold exactly
once": before, the test timed out at 15 s (start-lock re-entry). After,
the run is cancelled with `issue_paused` and the hold marker, and
release re-dispatches it once.
- "resumes a refused retry on a task moved out of the paused tree before
release" and "... on a task outside the snapshot when release also wakes
members": review findings, one resume each.
- "leaves a due retry parked when the pause is released while the gate
refuses it": the test waits until the gate blocks on an uncommitted
release. The retry is not cancelled and runs once. Without the `FOR
SHARE` lock the gate does not wait and the test fails.
- "resumes work a gate refused after the release that should have
resumed it had finished": the late-refusal re-check resumes once, and a
repeated check does not dispatch again.
- "does not resume a paused-code cancellation that no pause hold
covered": negative case.
- Related suites pass locally: `src/__tests__/issue-tree*`,
`src/modules/run-dispatch`, `src/services/heartbeat/queue.test.ts`,
`heartbeat-dependency-scheduling`, `heartbeat-workspace-busy`, and
`packages/db` `client.test.ts`.
- I did not run a live staging check here. Staging APIs are not
reachable from this environment.

## Risks

- Migration 0436 adds a partial index on `heartbeat_runs`. It is built
concurrently and covers only cancelled runs with the marker, so it is
small.
- The scheduled-retry gate now takes a `FOR SHARE` row lock on the pause
hold before it cancels. Release updates that row, so the two serialize
briefly. Release does not lock issues or runs, so the lock order cannot
deadlock with the gate's issue-then-run locks.
- The queued-run claim gate now records `issue_paused` in place of the
generic `cancelled`. Stranded recovery already classifies `issue_paused`
as non-retryable. The pause resume owns that work, so recovery does not
add a second run.
- Scope limits (documented): a non-assignee run refused by a pause is
not replayed, the same as non-assignee wakes that admission drops during
a hold. Among pre-fix rows, only scheduled retries carry `issue_paused`,
so only those fall back to the snapshot. A pre-fix row whose hold is
already released does not self-recover; the staging task that showed
this bug needs one manual wake, or a new pause and release.

> 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 (Anthropic), model ID `claude-opus-5-5`, used through
Claude Code with tool use (shell, file edits, 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
- [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-06 03:03:15 -06:00
DottaandClaude Opus 5.5 39096a55a4 fix(runner): answer lease renewal outside the PRP frame chain and name lease loss (#19419)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native runs in a managed sandbox talk to the server over one
authenticated PRP connection. The runner (`paperclip-runnerd`) renews
its connection lease on that connection.
> - The server serializes every inbound PRP frame through one promise
chain per connection. Before this change, `lease_renew` waited in that
chain behind earlier events. An event whose business commit stalls (for
example the commit of a tool event) therefore also stalls the lease
reply. The runner then stops polling its provider (ACK backpressure) and
exits with "active connection lease expired" when the lease runs out.
> - When that happens, the server sees only a socket close. The run log
and the terminal error say "transport loss", not "lease expired".
> - A follow-up from #19407 asked for renewal to be handled outside the
chain and for lease loss to be named.
> - This pull request answers `lease_renew` (and `pong`) as soon as the
frame is decrypted. It keeps in-order decryption and the order of
state-changing frames. It also records why a lease was lost and reports
it as `runner_connection_lease_expired`.
> - The benefit is that a slow event commit can no longer starve lease
renewal, and a lease loss is visible in the run log and error code.

## Linked Issues or Issue Description

Refs #19407

**What happened?**
On staging, native runs with ACPX providers (Grok, Kimi, and Pi with
OpenRouter/DeepSeek) ran a long foreground shell command that printed
nothing (a 15–20 minute `sleep 60` loop). Each run went silent soon
after `tool.execution.started`. The command finished in the sandbox, but
the run never got the completion and stayed `running` until someone
cancelled it. It happened in 3 of 3 attempts. The same task on Codex
kept reporting activity and succeeded.

**What did you expect to happen?**
A quiet tool call keeps its runner connection and its lease. If the
connection or lease is lost, the run log and the error code say why.

**Steps to reproduce**
1. Assign a native ACPX agent a task that runs a foreground shell loop
with no output for longer than 2 minutes.
2. Observe that the run goes silent and does not finish after the
command ends.

**Root cause analysis**
- The ACPX path emits no PRP frames during a silent tool call. The
runner has no timer, and the agent sends no `tool_call_update`. Codex
keeps the connection busy, because its model polls the running command.
Silence is the only adapter-specific factor that I found. I found no
ACPX code path in runnerd that blocks the main loop: polls use a 1 ms
timeout, and synchronous sidecar requests run only for server commands.
- The staging build (`de1143448b`) did not include #19407. That build
had no server pings. With a 60-minute connection lease (the default for
`runner-prp-coordinator` and `DurablePrpCodexTransport`), the runner
renews only every 30 minutes. A fully silent connection that goes
through the sandbox preview proxy therefore carried no traffic at all. A
route that the proxy drops without a FIN then becomes half-open forever.
#19407 added 20-second pings and a 90-second termination, which closes
that window.
- The lease hypothesis from #19407 is real but secondary. The 60-second
lease applies only when the TTL is not set. Renewal waited behind any
slow event commit, and the server could not report lease loss. This pull
request fixes both.

## What Changed

- `DurablePrpControlPlane.attachWireConnection` decrypts each secure
frame when it arrives, in counter order. Frames that arrive before the
channel is secure, or behind such a frame, still decrypt in the chain.
- `lease_renew` and `pong` are handled at once, outside the
per-connection chain. Admission against persisted authority is
unchanged. Events, command results, and handoff frames keep their
serialized order.
- `#renewLease` persists the renewed expiry with `save()`, so an event
write that is already staged is absorbed into the same write. Before,
the renewal used `commit()`, which would invalidate that staged event
and close a healthy connection when renewal runs concurrently.
- New `connectionLeaseFailure` getter (exported type
`DurablePrpConnectionLeaseFailure`). It records
`connection_lease_expired` when an authenticated connection is refused
or closes with an expired lease. A new authenticated connection clears
it. A credential revoked by a warm handoff is not reported, because a
replacement runner owns the run.
- `connectRunnerPrpIngress` reads that reason after a close. It logs
`state=reconnecting failure=runner_connection_lease_expired` in the run
log. If reconnect grace runs out, it fails with that code instead of
`runner_ingress_unavailable` or `runner_ingress_auth_failed`, and keeps
the reconnect error as the cause.
- `packages/paperclip-runner/docs/durable-recovery.md` documents the new
behavior.

## Verification

- `pnpm --filter @paperclipai/paperclip-runner exec vitest run
src/control-plane` (220 passed).
- New: "answers lease renewal while an earlier event commit is still in
flight". It was red before the fix (no `lease_renewed` while
`onCommittedEvent` waits), and it is green now. It also checks that the
event later commits and ACKs, and that the renewed expiry survives.
- New: "keeps a staged tool-progress write when a lease renewal lands
mid-write". It holds a file-backed `tool.execution.progressed` write at
`fsync`. It is red with the old `commit()` renewal (the event is dropped
and the connection closes), and green with `save()`.
- New: "reports an expired connection lease instead of closing
silently". It was red before the fix and is green now.
- `pnpm --filter @paperclipai/server exec vitest run
src/realtime/runner-prp-outbound.test.ts
src/realtime/runner-prp-ws.test.ts
src/services/native-runtime/runner-prp-coordinator.test.ts` (all
passed).
- New: "names an expired runner connection lease in the run log and
terminal error when reconnect is refused/unauthenticated". It was red
before the fix and is green now.
- `src/live/runnerd-codex-transport.test.ts` has the same result with
and without this change with a locally built `paperclip-runnerd`: the
same 48 tests fail locally on both, for environment reasons, and the
other 207 pass.

## Risks

- Lease renewal now runs concurrently with an in-flight event handler.
The handler builds its state candidate at commit time, and `save()`
absorbs a staged event write. A new regression test covers the
file-backed race. External checkpoint persistence commits events
synchronously, and the renewal reply still waits for the checkpoint
flush.
- Renewal still stays silent while a warm handoff or a terminal
lifecycle command is pending. The runner treats any other control frame
inside those fences as fatal, so this pull request does not change that
behavior.
- No protocol, schema, or contract-digest change. The runner is
unchanged.

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

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code with tool use (shell, file edits, test runs) and a read-only
exploration subagent on the same model.

## 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-06 02:01:01 -06:00
DottaandClaude Opus 5.5 90b8190c33 fix(native-runtime): detect half-open runner ingress and project Stop for live runs (#19407)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native runs in a managed sandbox talk to the server through a
provider-ingress WebSocket. The server opens this socket to the runner
(`paperclip-runnerd`) through the sandbox preview proxy.
> - A staging run ran a 20-minute foreground shell command that printed
nothing. After the first second, no run events arrived. The run stayed
"Working" for over 35 minutes. No watchdog acted, and the run projection
said `canStop: false`.
> - The server only detected a lost runner connection from a `close` or
`error` event. It never sent pings. A half-open route, for example a
proxy that drops the connection without a FIN, never emits `close`. The
run then waits forever and the 7-minute reconnect grace never starts.
> - A long silent tool call is legitimate. The fix must tell "quiet but
alive" apart from "nothing alive". A protocol ping does that: runnerd
answers every ping with a pong while its provider waits on the tool.
> - This pull request pings authenticated runner connections and
terminates the socket after 90 seconds of total silence. The existing
reconnect grace and transport-failure path then recover the run. It also
makes the execution projection report `canStop: true` for every queued
or running run.
> - The benefit is that a run with a dead runner connection now ends in
a bounded time instead of hanging. API readers also see the Stop that
the board already offers.

## Linked Issues or Issue Description

**What happened?**
A native-runner run (managed sandbox, OpenCode with an OpenRouter model)
ran a foreground shell command for about 20 minutes with no output. The
last run event came at about T+1s. The command finished on disk, but the
run never got another event and stayed `running` / "Working" for over 35
minutes. Its projection had `canStop: false` and `permittedActions:
["inspect_run"]`. A board cancel through the API worked.

**Expected behavior**
The server detects a dead runner connection and recovers or fails the
run in a bounded time. A runner that is alive but quiet stays attached.
The projection reports that a live run can be stopped.

**Steps to reproduce**
1. Start a native run in a managed sandbox.
2. Make the agent run a long foreground command that prints nothing.
3. Let the preview route drop the WebSocket without a close frame.
4. Before this fix, the run stays "Working" forever.

## What Changed

- `server/src/realtime/runner-prp-outbound.ts`: after PRP
authentication, `WsJsonWireConnection.monitorLiveness` sends a WebSocket
ping every 20 s. Any inbound frame or pong counts as liveness. After 90
s with nothing inbound, it terminates the socket. That emits an ordinary
`close`, so the PRP authority and the reconnect loop use the existing
7-minute grace and then fail the transport. If the server's own event
loop stalls, the window restarts instead of blaming the runner. The 90 s
bound is longer than runnerd's own 60 s connection lease, which already
requires runnerd to read the socket.
- `server/src/services/execution-projection.ts`: `canStop` is now `true`
for every `queued` or `running` run. The old failed-native-retry rule is
unchanged.
- `server/src/services/execution-projection.ts`: a queued run now shows
"Waiting for previous run to stop" (cause `previous_run_settling`) when
a predecessor that admission checks (the task execution owner, the
retried run, or the agent's latest settled run on the task) still has
pending cleanup by `hasPendingRunCleanup`. Before, a turn queued after a
Stop showed only "Queued" with no reason while the stopped run's
controller still waited on the dead runner. That in-process wait
(`claimQueuedRun` settling-owner gate) also made Retry a no-op. The
liveness bound above makes that controller settle. Its existing cleanup
then calls `startNextQueuedRunForAgent`, which starts the queued turn.
- `server/src/services/run-cancellation.ts` and
`server/src/modules/run-dispatch/adapters/postgres.ts`: a user-stopped
native run whose runner was unreachable can later settle as `failed`
(`native_provider_terminal_failed`) when its transport fails closed. The
dispatch gate's message deferral required a `cancelled` source and
compared the message with `finishedAt`. The user's follow-up message was
therefore marked `skipped`, and the task asked for a second message. The
new `recordedUserStop` helper honors the recorded user Stop on such a
failed source, so the gate saves the message for the deferred-receipt
worker.
- `packages/shared/src/types/execution-projection.ts`: documents the
`canStop` meaning.
- `doc/execution-semantics.md`: documents the ingress liveness bound and
the new `canStop` meaning.

## Verification

- Red then green: `runner-prp-outbound.test.ts` "detects a half-open
authenticated ingress and fails the run within a bounded window". On the
old code it failed because no ping was sent and the socket was never
terminated. It now passes and the active-run `failure` rejects with
`runner_ingress_unavailable`.
- Safety: "keeps a quiet but live runner attached through a long silent
tool call" checks that a runner answering pings stays attached for 20
simulated minutes with no termination and no failure.
- Red then green: `execution-projection.test.ts` "offers Stop for every
live run, including a long-silent native turn". On the old code it
failed with `canStop: false`.
- Red then green: `execution-projection-recovery.test.ts` "names the
wait when a stopped predecessor still holds the task's runner" (real
Postgres). On the old code the successor projected a plain "Queued". It
also checks that the wait clears on lease release and ignores another
agent's run.
- Red then green: `workspace-recovery.test.ts` "continues a message sent
after Stop when the stopped run later failed closed". On the old code it
failed 3 of 3 runs with the receipt `skipped`. It now passes, and the
full file passes twice (392 tests). The test now pins the message after
the recorded Stop. Before, the fixture could write both in the same
millisecond, which made the sibling variants flaky.
- Passed: run-cancellation, explicit-native-continuation, run-dispatch
and run-control suites (622 tests).
- Passed: `npx vitest run src/realtime/
src/services/execution-projection*.test.ts
src/__tests__/heartbeat-run-privacy-routes.test.ts`, the
native-runner-execution-driver tests, and the plugin-runner-task tests.
- Passed: `pnpm --filter @paperclipai/server typecheck`.
- I read runnerd (`runner-core/src/durable/transport.rs`) and confirmed
it answers `Ping` with `Pong` in its non-blocking read loop.
- Not run: a live managed-sandbox reproduction. Staging and Daytona were
not reachable from this environment. `plugin-runner-probe.test.ts` needs
a local cargo build of runnerd, and it did not start in this worktree.

## Risks

- A false termination is possible if a runner fails to answer pings for
90 s while it is still alive. runnerd already loses its own connection
lease after 60 s without reading, so this bound does not add a stricter
requirement. A terminated live runner reconnects within the existing
grace.
- If the preview proxy drops WebSocket control frames, ping alone does
not prove liveness. Inbound PRP frames (runnerd's lease renewals, about
every 30 s) still count, so a live runner stays attached.
- `canStop` now means "the board can Stop now" for live runs. The UI
already shows Stop for running and queued runs without reading this
hint, so the UI does not change.
- Out of scope: the inbound runner WebSocket server
(`runner-prp-ws.ts`), and a lost OpenCode SSE event while the runner is
alive. A stopped run whose controller hangs for a reason other than a
dead transport still holds the queued successor. The projection now
names that wait, but this PR does not force-release it, because the
controller has no termination evidence. A queued reason for the agent
concurrency cap is also not covered. The wait label calls the admission
predicate (`hasPendingRunCleanup`) for up to three predecessor runs per
queued run. This adds a few reads to projections of queued runs only.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use (shell,
file edits) 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-06 00:52:01 -06:00
DottaandClaude Opus 5.5 a4bb059f59 fix(heartbeat): retry transient sandbox provider lease refusals and explain the block (#19412)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A task runs in an execution environment. A managed sandbox
environment gets a lease from a sandbox provider (for example Daytona)
before the agent starts.
> - When the provider answered sandbox create with a temporary error
(HTTP 502), the run failed as the generic `setup_failed`.
> - `setup_failed` is the fail-closed code for unknown setup errors. It
gets no automatic retry, so the task went to `blocked` at once. The task
showed only "Couldn't finish this attempt".
> - A user had to find and press Retry. The Retry then worked, which
shows that the failure was temporary.
> - This pull request classifies temporary provider refusals during
lease acquisition from structured fields. It retries them within the
existing bounded bootstrap budget, and it explains the block when the
retries are used up.
> - The benefit is that a short provider outage no longer stops a task.
When the outage continues, the task shows the cause and a Retry that
works.

## Linked Issues or Issue Description

**What happened?**
A task was assigned to an agent that uses a managed Daytona sandbox
environment. Sandbox creation returned `HTTP 502`, and the cleanup of
the provisional sandbox could not be confirmed. The run failed with
`setup_failed` and the error `Failed to acquire lease for environment
"Paperclip Sandbox" (sandbox): Daytona sandbox creation failed (HTTP
502); cleanup could not be confirmed for paperclip-create-…`. Three
seconds later the task was `blocked`. No automatic retry was scheduled.
The task showed no specific reason. A manual Retry started a new run
that completed normally.

**Expected behavior**
A temporary provider error (HTTP 5xx, 429, timeout) before the agent
starts must retry automatically with bounded backoff. The task must show
why it waits. If the retries are used up, the task must show the cause,
a next action, and a Retry that works on the first click.

**Steps to reproduce**
1. Configure an agent with a sandbox environment.
2. Make the provider return HTTP 502 on sandbox create.
3. Assign a task to the agent.
4. See that the run fails with `setup_failed` and the task goes to
`blocked` with no retry.

**Paperclip version or commit**
`master` at `612da1e8e0`.

## What Changed

- Plugin SDK: an `environmentAcquireLease` error now carries a closed
diagnostic envelope (HTTP status or a closed errno code) across the
worker RPC. It takes the status of the create call only, not of the
compensating cleanup lookup. The existing creation-cleanup ownership
envelope is unchanged. New module: `environment-acquire-error.ts`. The
resume diagnostic helpers are shared.
- Server: new `EnvironmentProviderUnavailableError` and code
`environment_provider_unavailable`. The environment run orchestrator
sets it as the cause of `lease_acquire_failed` only when structured
evidence shows a transient status (408, 425, 429, 500, 502, 503, 504, or
a closed connection errno). It never parses message text. Saved-sandbox
resume and stopped-worker refusals keep their own codes.
- Setup failure (`failRunSetup`): a fresh, pre-dispatch run records
`environment_provider_unavailable` with a `providerFailure` (phase
`startup`, retryability `transient`). The run uses the bounded bootstrap
retry (30 s, then 60 s). An accepted interaction continuation keeps its
own continuation retry, as for `plugin_worker_unavailable`.
- Recovery: the new code is registered with the other bootstrap codes.
It is not sent to the generic continuation retry. It does not create an
execution-reconciliation hold, because no provider work started. On
exhaustion the stranded notice has the title "Sandbox provider was
temporarily unavailable" and a specific next action.
- UI: `ProviderFailureNotice` labels the `sandbox` provider as "Sandbox
provider". The task notice shows the provider failure, "Paperclip will
retry automatically" while a retry is scheduled, and Retry after
exhaustion.
- Docs: `doc/execution-semantics.md` describes the policy and the
sandbox ownership rule.

## Verification

Red before the fix, green after the fix:

- `server/src/__tests__/heartbeat-process-recovery.test.ts` › "transient
sandbox provider refusal during lease acquisition":
- "retries automatically and runs exactly one successful successor
without user action": the real heartbeat. The lease runtime returns a
Daytona 502 once, then delegates. Before the fix: one `failed` run and
no successor. After the fix: one `scheduled_retry` successor with no
hold and a `retry_scheduled` presentation. After promotion, exactly one
successor succeeds.
- "explains the block with a working Retry once automatic retries are
used up": three refusals. The task is `blocked` with a "Sandbox provider
was temporarily unavailable" notice. The presentation is `{ state:
"failed", canRetry: true, providerFailure: { httpStatus: 502 } }`, and
no execution-reconciliation hold exists. One `requestTaskRetry` is
accepted, and its run succeeds.
- `server/src/services/heartbeat/run-completion.test.ts` › "routes a …
sandbox provider lease refusal through bounded bootstrap recovery": the
real orchestrator wrapping. Only the fresh, structured 502 is retried. A
message-only 502, a 400, a run after provider dispatch, and a retained
native run stay `setup_failed`.
- `packages/plugins/sdk/tests/environment-acquire-error.test.ts`: the
real worker RPC. The test checks the create status (not the 404 cleanup
lookup), the errno, no provider text, the unchanged cleanup envelope,
and that forged envelopes are rejected.

Commands:

```sh
pnpm --filter @paperclipai/plugin-sdk exec vitest run tests/environment-acquire-error.test.ts tests/environment-creation-cleanup.test.ts tests/environment-resume-error.test.ts
cd server && npx vitest run src/__tests__/heartbeat-process-recovery.test.ts src/services/recovery/ src/services/heartbeat/run-completion.test.ts src/services/heartbeat/retries.test.ts src/services/legacy-execution-recovery.test.ts src/services/task-interruption-presentation.test.ts src/services/task-retry.test.ts src/__tests__/environment-runtime.test.ts src/__tests__/heartbeat-retry-scheduling.test.ts
cd packages/plugins/sandbox-providers/daytona && npx vitest run
pnpm --filter @paperclipai/plugin-sdk typecheck && pnpm --filter @paperclipai/server typecheck
```

I did not run a live Daytona 502 on staging. Re-check on staging after
deploy (see Risks).

## Risks

- Sandbox leaks or double creation: each retry is a new run with a new
durable creation record and a new provider name
(`paperclip-create-<operationId>`). The failed attempt stays owned by
its own record, and the existing creation reconciler settles it. A retry
never reuses or abandons it. While a provisional sandbox waits for
reconciliation, up to two more sandboxes can be created for the same
task. This is the same as a manual Retry today.
- Plugins built with an older SDK do not send the envelope. Their
refusals stay `setup_failed`, which is the current behavior.
- An in-process error with `status`/`statusCode` 500–504 or a connection
errno during lease acquisition is now retried twice. The budget is
bounded and shared with the other bootstrap codes.
- An RPC timeout on the host side is not classified by this change.

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

## Model Used

- Claude Opus 5.5 (Anthropic), model ID `claude-opus-5-5`, used through
Claude Code as an agent with tool use (shell, file edits, 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
- [ ] 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-06 00:08:44 -06:00