Commit Graph
240 Commits
Author SHA1 Message Date
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
DottaandClaude Opus 5.5 da9a4cb96c fix(native): retire a finished local Runner that stopped before suspending (#19597)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The native Runner path runs each per-turn agent turn through a
runnerd process, and the next turn must prove that the previous Runner
is finished before it takes over the saved session root
> - Under load, a local turn can have its result accepted while its
`runner.suspend` command does not finish, so the Runner's own receipt
stays unsuspended
> - The retirement proof that handles the same case for sandbox Runners
refused every local Runner with `target_not_sandbox_runner`
> - So the next turn failed with `runner_state_identity_mismatch:
prior_owner_terminal_state_indeterminate` and its session root was
quarantined
> - This pull request extends the retirement to a local Runner when the
prior Runner process is proven stopped by exact PID and start time
> - The benefit is that a follow-up turn (for example, after a user
answers a question) continues instead of failing

## Linked Issues or Issue Description

Refs #19525 (keeps the accepted result in the same incident; this is the
next run).

**What happened?**

In the runner E2E cell
`local-session-integrity.runner-acpx-claude.local.structured-question-restart-resume`,
the first turn asked a question and its result was accepted. The Runner
then logged `runner did not prove durable suspension before checkpoint`
(`runner.suspend` pending, Runner lifecycle `ready`). After a server
restart and the answer, the next run failed with
`runner_state_identity_mismatch:
prior_owner_terminal_state_indeterminate;
retirement_refused=target_not_sandbox_runner`, and its root was
quarantined.

**Expected behavior**

When the prior local Runner is provably stopped and every other
retirement proof holds, the next run retires that owner as a continuity
break and continues. If the stop cannot be proven, the run keeps the
named rejection.

**Steps to reproduce**

1. Run a local per-turn native turn whose `runner.suspend` does not
finish after its result is accepted (host load average ~40).
2. Start the next turn for the same session.
3. See `retirement_refused=target_not_sandbox_runner`.

## What Changed

- `server/src/services/native-runtime/native-restart-recovery.ts`: add
`proveNativeLocalRunnerStopped`. It uses the existing restart-recovery
checks: the recorded Runner PID is dead or now belongs to a process with
a different start time; its recorded process group is not alive; each
recorded provider process is dead or recycled. A missing PID, start time
or process group, a live recorded process, or a start time that cannot
be compared is not proof. When settlement already cleared the run's
process fields, the saved `native.local_process_stopped` receipt is the
Runner proof. Extract `nativeProviderProcessIdentities` (checkpoint plus
run events) so restart recovery and this proof read provider PIDs the
same way.
- `server/src/services/native-runtime/native-session-executor.ts`
(`retireFinishedPerTurnRunnerdOwner`, `readSettledRunnerdProjection`): a
local target is now eligible. For a local Runner, the projection accepts
the Runner's own unsuspended receipt for the same identity and
unfinished Runner lifecycle commands (`runner.suspend`, `runner.drain`,
`runner.shutdown`) only. All other proofs are unchanged: committed
result, released lease, no recovery state, stopped controller, delivered
terminal, other commands complete, tool inputs answered, quarantine
copies. The process proof runs before retirement and again just before
the archive.
- `doc/execution-semantics.md`: describe the local case.
- Tests: seven integration cases in
`native-recovered-owner-followup.test.ts` and ten unit cases in
`native-restart-recovery.test.ts`.

## Verification

- Red before the fix: the four new integration cases failed with
`retirement_refused=target_not_sandbox_runner`, which is the incident
error.
- Green after the fix: `pnpm exec vitest run
src/services/native-runtime/native-recovered-owner-followup.test.ts`
35/35. The new cases prove that a stopped Runner is retired (root
archived under `continuity-breaks`, no quarantine), and that a live
recorded Runner (`runner_process_alive`), a missing identity
(`runner_process_identity_missing`), and a pending non-lifecycle command
(`journal_commands_unsettled(turn.start.pending)`) keep the rejection.
All existing sandbox cases still pass, including "keeps an unsuspended
Runner receipt decisive" for sandbox targets.
- `native-restart-recovery.test.ts` unit cases: recycled PID, live
process, unknown start time, live group, live provider, missing PID or
start time.
- `native-session-executor.test.ts` + `native-restart-recovery.test.ts`:
1190 pass. `native-runner-restart-recovery.integration.test.ts` +
`native-restart-recovery-isolation.test.ts` (shared provider-PID
extraction): 81 pass. `tsc --noEmit -p server/tsconfig.json`: pass.
- Live: `pnpm test:e2e:runner -- --max-parallel 1
--max-automatic-retries 0 --id
local-session-integrity.runner-acpx-claude.local.structured-question-restart-resume`,
3 runs at eec57dd5ed: 3/3 PASS. The host load average was ~15 (the
incident had ~40), and `runner.suspend` finished in all three runs, so
these runs show no regression but did not exercise the new retirement
path. The integration cases cover that path.

## Risks

- The new path lets a follow-up archive a local root whose Runner
receipt is unsuspended. This is safe only if the Runner is stopped, so
the proof uses exact PID and start-time identity and fails closed on any
uncertainty. `ps` start times have one-second precision; a sub-second
difference is treated as the same process (not proof).
- A zombie-only process group on macOS can report as alive. That keeps
the rejection, as before.
- No quarantine behavior changes. A refused retirement still quarantines
the root as before.

## Model Used

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

## Checklist

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

> - Paperclip is the open source app people use to manage AI agents for
work
> - The native Runner path runs an agent turn through a runnerd process
that the control plane owns
> - A restart of the control plane must keep the task going without a
click, if that is safe
> - A graceful shutdown closes the HTTP listener first and then drains
the heartbeat runs
> - A native run that was between provider dispatch and runnerd backend
setup in that gap failed with `local_runtime_api_listener_unavailable`,
so the drain did not see it as running and did not suspend it for the
next boot
> - This pull request makes that failure a restart detach: the run stays
running with its claim, the drain suspends it, and the next controller
adopts it
> - The benefit is that a SIGTERM in this window no longer spends a
failure and a same-run retry, and the task continues by itself

## Linked Issues or Issue Description

Refs #19530 (the same restart window, before provider dispatch).

**What happened?**

In a restart chaos replay (ACPX Claude, local runner), a
restart-recovered run got SIGTERM while
`createRunnerdBackendWithinSessionClaim` built the runnerd backend.
`resolveLocalRuntimeApiUrl()` threw
`local_runtime_api_listener_unavailable` because shutdown had already
closed the listener. The run settled `failed`. The drain then logged
`restartSuspendedRunIds: []`. The next boot used the one bootstrap
retry. A second SIGTERM hit the same window, and the task stopped at a
manual Retry.

**Expected behavior**

A run that this process cannot continue because it is shutting down is
handed to the next controller. It is not settled as failed.

**Steps to reproduce**

1. Start a native Runner task and restart the server (K1) so that
startup recovery resumes the run.
2. Send SIGTERM while the resumed run sets up its runnerd backend.
3. See the run fail with `local_runtime_api_listener_unavailable` and
the drain report no suspended runs.

## What Changed

- `server/src/runtime-api.ts`: `resolveLocalRuntimeApiUrl()` throws the
new `LocalRuntimeApiListenerClosedError` when this process had a
listener and closed it (the `listening` flag is false or the `close`
event fired). The error message stays
`local_runtime_api_listener_unavailable`, so the existing classifiers do
not change. A listener that never started still throws the plain error.
- `server/src/services/native-runtime/native-session-executor.ts`: for a
local Runner, the startup failure handler treats
`LocalRuntimeApiListenerClosedError` like a restart detach. It stops
lease renewal, keeps the claim and throws
`NativeControllerDetachedForRestartError`. Heartbeat already keeps such
a run `running`. A remote Runner keeps the same-run bootstrap retry from
#19462, because startup recovery has no process evidence to adopt a
claimed remote startup
(`native-runner-restart-preparation.integration.test.ts` still covers
that path).
- `doc/execution-semantics.md`: describe the new rule.
- Tests: a heartbeat regression that closes the real runtime API
listener before backend setup, then drains and restarts; a unit test for
the closed versus never-started listener; two `vi.mock` factories keep
the real module exports.

## Verification

- Red before the fix: `pnpm exec vitest run
src/__tests__/heartbeat-native-restart-suspension.test.ts -t listener`
failed with `restartSuspendedRunIds: []` and the run `failed`
(`local_runtime_api_listener_unavailable`, `bootstrap_retry`).
- Green after the fix: the same test passes. The drain suspends the run,
the run stays `running` with `awaiting_runner_reattach`, and the next
controller claims it and reaches the backend once.
- `pnpm exec vitest run
src/__tests__/heartbeat-native-restart-suspension.test.ts
src/http/server.test.ts
src/__tests__/server-startup-feedback-export.test.ts
src/services/native-runtime/native-session-executor.test.ts
src/services/native-runtime/native-recovered-owner-followup.test.ts`:
1333 tests pass. One timing test in `native-session-executor.test.ts`
failed once under host load average ~30 and passed alone.
- `pnpm exec tsc --noEmit -p server/tsconfig.json`: pass.
- Live, restart chaos harness, local ACPX Claude (`claude-sonnet-5`),
`--rc06-masked`, this branch at a35ec8b845 (before the remote-scope
commit 5dc63f24fb, which does not change the local path). The original
replay schedule no longer reached the window, because the task now ends
before the second fault. So I moved the faults earlier: K1 at 10 s, then
K2 as soon as the recovered server is ready.
- K1 + K2, 2 runs: both runs PASS with **R1 pass**. Each drain logged
`restartSuspendedRunIds: [<run>]` (before: `[]`). The next boot
classified the run `resume_dead_runner` and the same run completed. No
`local_runtime_api_listener_unavailable` failure.
- K1 + K2 + K2, 2 runs: each drain suspended the run. The fourth boot
blocked the run with `native_restart_recovery_blocked`, because three
restart claims used the three-attempt recovery budget. Retry then
completed the task (R3 pass). That budget count is a separate open item.
This pull request does not change it.

## Risks

- Low. The new path applies only after this process closed its own
listener, which happens only during shutdown. The run then waits for the
drain or the next boot, as for the other restart detaches.
- If shutdown does not reach the drain (for example SIGKILL after the
listener closes), the run keeps its live claim until the claim expires.
Then startup recovery handles it as a hard restart. This is the same as
any run that is running at a hard kill.

## Model Used

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

## Checklist

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

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs on Daytona start a `paperclip-runnerd` process in a
sandbox. The local control plane keeps the runner's controller state in
an encrypted Postgres checkpoint.
> - When the control plane restarts, restart recovery classifies each
live native run and decides to reattach, resume, bootstrap again, or
hold the task.
> - A restart chaos wave killed the control plane twice while a Daytona
runner was connecting. The run was claimed and the sandbox runner was
launched, but `session.open` never completed. No provider evidence
existed.
> - Recovery waited correctly for the old 60 s controller lease to
expire. Then the classifier had no rule for a claimed remote bootstrap.
It returned `ambiguous_remote_recovery_evidence` and blocked the task as
"Workspace repair required".
> - The controller writes each command to the Postgres checkpoint before
it sends it. After the lease expires, that checkpoint is the complete
record of what reached the sandbox runner.
> - This pull request lets recovery prove from that checkpoint that no
provider work exists, and then start the same-run bootstrap again.
> - The benefit is that a restart during Daytona runner startup
continues automatically instead of stopping the task.

## Linked Issues or Issue Description

**What happened?**

A restart during Daytona remote runner startup (native ACPX Claude, two
SIGKILLs about 20 s apart) left the task blocked. The run history showed
`remote_controller_lease_live` twice, then
`ambiguous_remote_recovery_evidence` with disposition `blocked`. The
task showed "Workspace repair required"
(`native_restart_recovery_blocked`) and did not continue.

**Expected behavior**

When the durable controller record proves that the runner received only
bootstrap commands, recovery continues the run with a fresh bootstrap.
Any evidence of provider work keeps the hold.

**Steps to reproduce**

1. Start a native run on a Daytona environment.
2. Kill the control plane after the sandbox runner connects and before
`session.open` completes.
3. Restart the control plane and wait for the old controller lease to
expire.
4. See that restart recovery blocks the task with
`ambiguous_remote_recovery_evidence`.

Root cause:
`server/src/services/native-runtime/native-restart-recovery.ts`
`classifyNativeRunnerRecoveryEvidence`, remote branch. It admitted only
an unclaimed bootstrap (`remote_execution_never_claimed`, #16349) or a
run with a provider checkpoint. A claimed bootstrap with no provider
evidence fell through to `ambiguous_remote_recovery_evidence`.

The decrypted checkpoint of the failed run held `connectionCount: 1`,
`run.prepare` delivered once and pending, `session.open` pending and
never delivered, no committed events, and `ackedSourceSeq: 0`.

Refs #16349, #19524

## What Changed

- `native-bootstrap-proof.ts`: move the incomplete-bootstrap predicates
from the executor into a small pure module. Restart recovery and the
executor now use the same proof. No behavior change.
- `native-runner-checkpoint.ts`: add
`nativeRunnerCheckpointProvesIncompleteBootstrap`. It reads only a
checkpoint with the bootstrap parts (header, cursor, `command:1`,
`command:2`), checks the run, runner instance, and session identity, and
applies the shared proof. A remote runner journal is not on this host,
so the proof uses the controller record alone.
- `native-restart-recovery.ts`: for a classic sandbox run whose owner
lease has expired, with a durable checkpoint, no session checkpoint, no
provider evidence, and no staged attachment, claim
`bootstrap_incomplete` with the reason
`remote_bootstrap_proven_incomplete` when the checkpoint proof holds. At
three provider attempts, block with
`execution_recovery_budget_exhausted`.
- `native-session-executor.ts`: add
`restoreRemoteBootstrapCheckpointForRecovery`. For a
`bootstrap_incomplete` recovery on a sandbox target, it restores the
Postgres checkpoint over the local external-state marker. The existing
incomplete-bootstrap proof then checks the restored state before it
quarantines the root and bootstraps again.
- `doc/run-log-events.md`: document the new disposition reason.

## Verification

- Red, then green: `cd server && npx vitest run
src/services/native-runtime/native-runner-restart-recovery.integration.test.ts
-t "remote bootstrap"`. Before the fix, the proven case and the budget
case returned `blocked` / `ambiguous_remote_recovery_evidence`.
- New cases: the proven claimed bootstrap gets `bootstrap_incomplete`
(attempt kept, generation +1, run still `running`, no recovery action).
The hold stays for a queued `turn.start`, an acknowledged event, adopted
state, no checkpoint, and a spent budget. The executor helper restores
the checkpoint only for a bootstrap recovery on a classic sandbox and
only over a marker for the same run.
- Regression set: `native-runner-restart-recovery.integration`,
`native-session-executor`, `native-restart-recovery`,
`native-bootstrap-retry.integration`, `heartbeat/recovery`: 1383 passed,
14 skipped.
- Live Daytona replay of the failing chaos task (double SIGKILL during
remote runner startup), 3 runs: REPLAY_RESULTS

## Risks

- A fresh bootstrap acquires a new sandbox. The old sandbox keeps its
lease until run settlement releases all run leases. The old runner gets
no turn command, because only the controller issues one and the new
controller never connects to it.
- If `session.open` reached the old runner, the old provider session can
exist without a turn. No provider work or tool call can happen in it.
- Any checkpoint shape other than the bootstrap-only shape keeps the
previous fail-closed hold.

## Model Used

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

## Checklist

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

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Core owns Runner tasks through a lease-bound plugin owner.
> - The SDK already defines release and resume in #19468.
> - The owner still expects an accepted result for both operations, so
it cannot use those SDK results.
> - This pull request adds both methods behind the existing capability,
warm-task and account checks.
> - It uses the SDK parser to verify the result and task ID, and
preserves the owner's cancellation checks.
> - This prepares the owner boundary for later lifecycle integration. It
does not start automatic release or resume.

## Linked Issues or Issue Description

**Problem or motivation**

The plugin Runner owner cannot forward the existing SDK release/resume
pair. Its local result mapping rejects their valid results. Later
lifecycle code needs both operations on the same checked task owner.

**Proposed solution**

Pass the existing description capability to the owner. Add explicit
release and resume methods and use the SDK's operation-result parser.
Require warm-task support and a selected account. Reject calls and late
receipts after owner cancellation or detachment.

**Alternatives considered**

Adding automatic release here would also require connection quieting,
settled-event drain, pending-ticket retry ownership and safe disposal.
Keep those changes together in the next integration. Do not add a new
flag, schema, timer or lifecycle mode here.

Related: #19468. I checked ROADMAP.md and found no duplicate planned
item. An open-PR title search for release/resume found no overlapping
pull request.

## What Changed

- Add the capability-gated owner facade for Codex and ACPX. Resume
returns only `resumed` or `unavailable`.
- Use the SDK parser for exact task-ID and operation-result validation.
- Pass `releaseResumeVersion` from the checked description and test that
it does not cause an automatic operation.
- Cover account-preserving owner rebind, missing prerequisites,
malformed results, ambiguous errors, cancellation and late replies.
- Document the caller's remaining settlement, connection, ticket and
attachment duties.

## Verification

Passed locally on Node 26.12.0 with the pinned workspace dependencies:

- In `server/`: `pnpm exec vitest run
src/services/native-runtime/plugin-runner-task.test.ts` — 74 tests
passed.
- In `server/`: `pnpm exec vitest run
src/services/native-runtime/native-session-executor.test.ts -t 'uses a
task-only'` — 3 cases passed; 1,160 unrelated cases were not selected.
- Built the real workspace SDK dependencies and Runner TypeScript. A
focused strict TypeScript program for the owner and its tests passed.
- `git diff --check` and the added-line secret/PII scan passed.

The first root test command selected a nonexistent Vitest project name
and ran no tests. Running the test from the server package fixed the
command. The first focused TypeScript setup could not find Node types;
pointing it to the package's installed type definitions fixed that
setup. The full regression suite and release checks run in GitHub.

## Risks

The facade has no automatic caller. Existing warm timeout, bootstrap,
outbound connection and pool behavior stay in place. No plugin
capability is advertised or task control channel enabled by this pull
request.

Future integration must serialize release with new work and disposal. It
must quiet the owned connection, keep the exact pending ticket after an
ambiguous reply, authenticate the source again and finish ordinary run
attachment before new work. This change does not qualify provider
parking, cold recovery, retention policy or deployment.

## Model Used

OpenAI GPT-6.1 Sol, exact model ID `gpt-6.1-sol`, High reasoning. Used
code execution and targeted tests. The context-window size was 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 (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:42:32 -08:00
DottaandClaude Opus 5.5 07ddf9a5c2 fix(native): keep accepted results when runner close cannot prove suspension (#19525)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native runs persist an accepted result first. The Runner then closes
and suspends its session.
> - Under heavy host load, the Runner close can fail to prove a durable
suspension after the result is accepted.
> - The executor then kept its finalization lease. The heartbeat
finalizer got `native_finalization_lease_busy`, and the run ended
`failed/adapter_failed`.
> - The provider work was complete. Only Runner bookkeeping failed, so
the run must not fail.
> - This pull request hands the exact lease to finalization and
completes the run from its accepted result.
> - The benefit is that an accepted result stays accepted, independent
of host load.

## Linked Issues or Issue Description

**What happened?**

A native ACPX Claude run asked a structured question. The control plane
accepted its result (`Waiting for…`) and recorded `run.terminal` with
`runTerminalState=succeeded`. The Runner close then took about 8.6 s.
Its `runner.suspend` command ended `failed`, but the durable Runner
state was `suspended` with a matching identity. The close threw
`NativeSessionCloseUnrecoverableError`. The executor emitted
`native.post_result_failure` (`category: runner_settlement_incomplete`,
`resultAlreadyAccepted: true`) and rethrew `native_session_lease_lost`.
Its finalization lease stayed held (its TTL is 20 minutes).
`finalizeNativeRun` got `native_finalization_lease_busy`, and the run
ended `failed/adapter_failed`. No restart was in progress. Host load
average was about 40.

**Expected behavior**

After the control plane accepts a result, finalization bookkeeping must
not fail the run. The run settles from its accepted result. Repair of
the Runner checkpoint stays with the existing next-run admission rules.

**Steps to reproduce**

Run `pnpm exec vitest run
src/services/heartbeat/runtime-execution.test.ts -t "accepted local
result"` in `server/`. The test uses the real executor, the real
control-plane result commit, and embedded PostgreSQL. The provider
boundary accepts a result and then throws
`NativeSessionCloseUnrecoverableError`. On the base revision, the
finalization row keeps the executor lease (`phase:
workspace_finalizing`) and the finalizer reports `lease_busy`.

**Paperclip version or commit**

Base `ff1d1dea5d`. The incident was on `819420a60a`.

## What Changed

- `native-session-executor.ts`: after the rejected failure write proves
an accepted result under the exact, unexpired lease, the executor
releases that lease to finalization. It uses the same exact-authority
predicates as the normal release. Before, the lease stayed held until
its TTL expired.
- `runtime-execution.ts`: for `runner_settlement_incomplete` on a local
workspace (no workspace sync and no remote workspace authority),
Paperclip records `workspace_finalize` as succeeded, finalizes the
accepted result, and returns it to the ordinary completion path.
- New `native-accepted-result-settlement.ts`: rebuilds the adapter
result from the durable accepted result row, its terminal, the run's
native session, and the accepted usage checkpoint. The result carries
the closed `nativePostResultFailure` diagnostic. It contains no provider
text.
- Other post-result categories (for example `io_not_found`) keep their
workspace-failure handling. They now also reach finalization at once,
instead of after the lease TTL.
- `doc/execution-semantics.md`: describes the settlement.

## Verification

- Red, then green: `settles an accepted local result when runner close
cannot prove suspension` and `keeps workspace failure handling for other
accepted-result failures on a local workspace` in
`server/src/services/heartbeat/runtime-execution.test.ts`. Both failed
on the base revision with the executor lease still held. Both pass with
this change.
- Safety case `settles an accepted local result ...
(workspace-check-failed)`: when the `workspace_finalize` check fails,
the run keeps the existing failure path. Case `terminal-unreadable`:
when the accepted result cannot be rebuilt, only a failed
`workspace_finalize` record is written.
- `pnpm exec vitest run
src/services/heartbeat/runtime-execution.test.ts`: all pass (some cases
time out at 30 s only under host load above 40; they pass on a narrow
rerun). This includes the 14 existing post-result diagnostic and
authority cases (expired, replaced owner, generation, attempt, boot,
pid, process start, stopped, lock busy, and others).
- `pnpm --filter @paperclipai/server typecheck`: pass.
- Live cell
`local-session-integrity.runner-acpx-claude.local.structured-question-restart-resume`
(real ACPX Claude, local runner): 3 runs at host load average about 40.
Runs 1 and 2 passed. Run 3 hit the fault naturally: the first run
emitted `native.post_result_failure` (`runner_settlement_incomplete`)
and now ended `run succeeded` (before this change it ended
`failed/adapter_failed`). The cell then failed later, in the resume run
after the answer: `runner_state_identity_mismatch:
prior_owner_terminal_state_indeterminate;
retirement_refused=target_not_sandbox_runner`. That is a separate
follow-up (see Risks).

## Risks

- A changed lease handoff. The executor releases the lease only after
the rejected failure write proves an accepted result under the exact
owner, attempt, controller generation, controller identity, and an
unexpired lease. A stale or replaced owner does not release anything. A
failed release is bounded and best effort. The old behavior then
applies.
- The run now completes as `succeeded` while the local Runner checkpoint
is not proven suspended. The next run on the same session then meets the
existing fence: the continuity-break retirement
(`retireFinishedPerTurnRunnerdOwner`) accepts only sandbox Runners, so a
local Runner is refused with `target_not_sandbox_runner` and its state
root is quarantined. This PR does not change that fence. Extending the
retirement to local Runners needs its own proof that the local Runner
process stopped, and is a follow-up.
- Settlement happens only when the accepted result and its terminal are
rebuilt first, and the `workspace_finalize` record then succeeds. A
failed branch or plugin workspace check keeps the failure path.
- Synced and remote workspaces are not changed. They keep their
copy-back retry path.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code 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

Related: #19126 added the closed post-result diagnostic that this change
uses.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 12:36:34 -06:00
DottaandClaude Opus 5.5 beaa9a622d fix(native): re-bootstrap a runner killed before session.open (#19524)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs start a local `paperclip-runnerd` process. The control
plane keeps a durable state root for each session.
> - When the control plane restarts, restart recovery classifies each
live native run. A run whose runner died before any provider identity
existed gets the `bootstrap_incomplete` disposition. Recovery then
quarantines the old state root and starts the bootstrap again.
> - The executor accepts that disposition only when the durable state
root proves that no provider work started. The old proof required
`connectionCount === 0` and every command `pending`.
> - A restart chaos wave killed the server about 2 s after runnerd
started. The runner had connected and completed `run.prepare`, but
`session.open` was still pending. No event was committed, so no provider
session or turn existed.
> - The proof rejected that root. The run failed with
`runner_state_identity_mismatch: bootstrap_not_proven_incomplete`, and
the task stopped with `native_continuation_requires_reconciliation`.
Nothing continued automatically.
> - This pull request widens the proof to the exact window where
provider work cannot have started, and keeps every other shape
fail-closed.
> - The benefit is that a restart during native runner bootstrap
continues automatically for every native provider.

## Linked Issues or Issue Description

**What happened?**
A control plane restart that lands after the native runner connected and
acknowledged `run.prepare`, but before `session.open` completed, fails
the run with `adapter_failed` and `runner_state_identity_mismatch:
bootstrap_not_proven_incomplete`. Startup recovery chose
`bootstrap_incomplete` (controller generations 2 and 3), but the
executor proof in `migrateRunnerdStateRootForExecution` refused it. The
durable control-plane state had `connectionCount: 1`, `run.prepare`
completed, `session.open` pending, no committed events, and
`ackedSourceSeq: 0`. The runner journal had `nextSourceSeq: 1`, an empty
outbox, and the provider state was `prepared` with no provider session
ID.

**Steps to reproduce**
1. Start a native task (seen with OpenCode on a local runner; the window
applies to every native provider).
2. Kill the control plane about 2 s after runnerd starts, after
`run.prepare` completes and before `session.open` completes.
3. Let the control plane restart.

**Expected behavior**
Recovery quarantines the state root and starts the bootstrap again. The
task continues without a user action.

**Actual behavior**
The run fails and the task is blocked with
`native_continuation_requires_reconciliation`.

## What Changed

- `runnerdStateIsIncompleteBootstrap` (native-session-executor.ts) now
also accepts a connected root when all of these are true: no committed
event, `ackedSourceSeq === 0`, only `run.prepare` and `session.open`
commands, `session.open` not completed, `run.prepare` pending or
completed, no warm transition or staged attachment, and the bootstrap
was not adopted.
- When `runner/runner-state.json` exists, the runner journal must agree:
empty outbox, `nextSourceSeq === 1`, `ackedSourceSeq === 0`, and no
completed `session.open` or other processed command. An unreadable
runner journal stays fail-closed. This covers events that the runner
emitted but the controller never committed.
- The remote "claim an untouched session" path keeps the old
never-connected proof (`runnerdStateIsUnconnectedBootstrap`), so its
behavior does not change.
- Tests: a red test with the production state shape (connected,
`run.prepare` completed, `session.open` pending), plus fail-closed
counter-tests for a completed `session.open`, committed or acknowledged
events, a turn command, a failed `run.prepare`, an adopted bootstrap, a
warm transition, an uncommitted runner event, a runner-completed
`session.open`, a runner-processed turn, and a torn runner journal.

## Verification

- Red before the fix: `pnpm exec vitest run
src/services/native-runtime/native-session-executor.test.ts -t "native
incomplete-bootstrap evidence"` failed on "allows a fresh bootstrap
after connect and run.prepare when session.open never completed"
(`expected false to be true`). Green after the fix: 13/13.
- The proof run against the kept durable state root from the failing
wave (read only) returns `true` after the fix.
- Related suites: `native-session-executor.test.ts` and
`native-restart-recovery.test.ts` filtered to
`bootstrap|restart|remote|unstarted|untouched`: 301 passed.
- `tsc --noEmit -p server/tsconfig.json` passes.
- Live restart chaos proof: see the comment on this PR.

## Risks

- The proof now accepts a root where the runner received `session.open`
but did not complete it. The runner can have started a provider process
or created an empty provider session in that window. Recovery only
proceeds when restart classification already proved that the runner
process and its group are dead and no known provider process is alive.
No provider turn can exist, because the controller never issued a turn
command. The worst case is an orphaned empty provider session, not
repeated work.
- Every state with committed or acknowledged events, a completed
`session.open`, a turn, a warm transition, an adopted bootstrap, or an
unreadable runner journal still fails closed.
- Out of scope: when the proof fails legitimately, the task offers the
existing execution-blocker Retry. In the failing wave the issue
interruption also showed a budget wait (`userAction: budget`). That is
the RC-06 accounting issue and belongs to a separate fix.

> 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`), used through Claude
Code with tool use (shell, file edits, test execution). Default context
window. Extended reasoning was available.

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 12:30:50 -06:00
DottaandClaude Opus 5.5 e680fa6814 fix(native): retry startup interrupted by a closed runtime API listener (#19462)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs prepare a remote workspace, then start a runner and a
provider session
> - A graceful server shutdown closes the HTTP listener before it drains
runs
> - A run that is still preparing then asks for the local runtime API
origin, and the lookup fails with
`local_runtime_api_listener_unavailable`
> - Recovery did not know this code. It treated the failure as an
ambiguous provider state and blocked the task behind a board-owned
action, although no runner or provider process existed
> - This pull request classifies the closed listener as a definite
pre-session failure
> - The benefit is that a deploy no longer strands a preparing task. The
same run gets a bounded bootstrap retry that the next server process
claims

## Linked Issues or Issue Description

**What happened**
A native Codex task on a remote sandbox spent about ten minutes in
workspace preparation. A deploy started a graceful shutdown during that
time. When preparation finished, the executor called
`resolveLocalRuntimeApiUrl()` to build the tool authority. The listener
was already closed, so it threw
`local_runtime_api_listener_unavailable`. No runner backend or provider
session had started. The failure classifier returned the generic
`native_session_interrupted` code. `nativeProviderRecoveryEvidence` then
returned `ambiguous_state`. The run became a terminal failure, the task
became blocked, and the recovery action told the board to resolve an
ambiguous session and to not open a replacement session. The same
signature occurred on three other runs during an earlier deploy.

**Expected behavior**
A failure that occurs before any runner or provider process exists is
safe to retry on the same run. The task must stay in progress with a
scheduled, bounded bootstrap retry. If a provider event or checkpoint
exists, recovery must stay operator-owned.

**Steps to reproduce**
Start a native run. Close the server's HTTP listener before the executor
creates the runner backend. Observe a `terminal_failure` with
`recoveryMode: "ambiguous_state"` and a blocked task. The new
integration test reproduces this with the real
`resolveLocalRuntimeApiUrl()` and a real Postgres database.

**Environment**
Server native runtime, remote sandbox target, `codex_app_server` driver.
Related recovery work: #19249, #19304, #19305.

## What Changed

- `nativeSessionFailureSourceCode` returns a new
`local_runtime_api_listener_unavailable` source code for this error.
- `nativeProviderRecoveryEvidence` treats that code as definitely
pre-session. The existing guard stays: the run must have no checkpoint
and no durable provider event.
- The chat failed-run retry accepts the code in its list of ordinary
pre-session failures, so an exhausted startup retry can still be retried
from chat.
- New integration tests in `native-startup-retry.integration.test.ts`
cover the retry path and the operator-owned path when a provider event
exists.

## Verification

- Red before the fix: the new test failed with `phase:
"terminal_failure"`, `recoveryMode: "ambiguous_state"`, `recoverable:
false` and a board-owned recovery. This matches the failed run on the
deployed instance.
- Green after the fix: `cd server && npx vitest run
src/services/native-runtime/native-startup-retry.integration.test.ts`
passes 26 of 26 tests. The retry test asserts `retryable_failure`,
`bootstrap_retry`, backoff timing, an agent-owned `resume_native_run`
wake, and a claim through `claimNativeSessionResumptions`.
- Safety case: when a durable `turn.started` event exists, the same
error stays `terminal_failure` with `ambiguous_state` and a blocked
task.
- `npx vitest run
src/services/native-runtime/native-session-executor.test.ts -t
"listener|classif|pre-session"` passes.
- Not run: a live deploy that interrupts a preparing sandbox run. The
test uses the real listener lookup and the real recovery tables, but not
a real sandbox.

## Risks

- Low risk. The change only affects runs that fail with this exact error
and have no checkpoint and no provider event.
- The retry uses the existing bootstrap budget of three attempts with
backoff. Repeated failures still stop and hand the task to the board.
- The GitHub callback bridge that started in the sandbox before the
failure is a per-attempt helper. The existing cleanup path releases it.

> 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, 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 08:27:59 -06:00
DottaandClaude Opus 5.5 6ca7b151f7 feat: compact an agent's context from the task Properties panel (#19378)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A task's Properties panel shows the assignee agent's context window
(paperclipai/paperclip#19273)
> - The board can see that the context is nearly full, but it cannot act
on it
> - A full context makes the agent compact at an unplanned time, often
in the middle of work
> - This pull request adds "Compact now" to the Context hover card
> - The benefit is that the board can compact the agent's context
between runs, at a time it chooses

## Linked Issues or Issue Description

Refs paperclipai/paperclip#19273

**Problem**
The Context row shows how full the agent's context window is. The board
cannot ask the agent to compact it.

**Proposed solution**
A "Compact now" button in the Context hover card. It runs the harness's
own compaction: Codex `thread/compact/start`, and Claude's `/compact`
command through ACP.

**Alternatives considered**
A separate PRP `session.compact` command. Both harnesses run compaction
as a turn, so a compaction turn keeps the existing durable turn binding,
interruption, and recovery. A separate command needs a second turn
lifecycle.

## What Changed

- UI: the Context hover card shows Compact now with three states:
available, Compacting…, and unavailable with a reason (agent mid-run,
harness unsupported, task closed). The Storybook story shows each state.
- Server: `POST /api/issues/:id/context/compact` (board only,
`decideIssueAccess` `issue:mutate`, activity log
`issue.context_compaction_requested`). It queues a short run marked by
`contextSnapshot.contextCompactionRequestId`. `GET
/issues/:id/context-usage` returns the `compaction` state.
- Server: a compaction run does no task work. The native status arbiter
preserves the task status. The lifecycle skips handoff, liveness
continuation, review-path disposition and the issue comment, as it does
for goal-control runs.
- Runner TS: `startTurn({ compact: true })`. The Codex harness session
sends `thread/compact/start`. The runnerd transport maps it to PRP
`turn.start` with `compact: true` (Codex) or `text: "/compact"`
(Claude/ACPX). The native runtime option `sessionCompaction` requires a
resumed session and settles with a synthesized yielded result.
- Runner Rust: `turn.start` accepts `compact: true` for Codex.
`CodexProvider::start_compaction_turn` sends `thread/compact/start` and
binds the turn from the `turn/started` that follows the empty response.
- Fix (from #19273): Codex emitted `context.compacted` on both the start
and the end of one compaction item. It now emits it once, and the count
is distinct by `compactionId`.
- Fix (from #19273): the conversation event view dropped
`context.compacted`, so a finished run never showed the divider. The
view now keeps compactions, plus context readings for runs that
compacted. A finished run's compactions show as thread dividers.
- Fix: the Codex harness stamps before/after sizes on the compaction
event. Codex reports the compacted size before the compaction item ends,
so the old neighbour-based guess showed the wrong size.

## Verification

- `pnpm -r typecheck`, `pnpm build`, `pnpm check:token-gates`, `node
scripts/storybook-catalog.mjs`: pass.
- New tests: `server/src/__tests__/context-compaction.test.ts`,
`status-arbiter.test.ts`, `issue-context-usage.test.ts` (duplicate
compaction counts once), `ui/.../context-usage-row.test.tsx`,
`native-session-runtime.test.ts` (context compaction), Codex driver and
runnerd transport tests, Rust
`codex_compaction_turn_binds_the_turn_reported_after_its_empty_response`.
Rust `codex_provider` suite: 115 passed.
- `pnpm test:run` locally: one failure in
`chat-channels.integration.test.ts`. It reads
`.claude/skills/slack/SKILL.md` from the main checkout, which is not
present in this worktree. It is not related to this change.
- Live, local worktree instance, Codex agent on the Paperclip runner:
clicked Compact now. The run succeeded. Context went from 35k to 9.4k
tokens, and later 9.4k to 9.2k. The task stayed In Review. The thread
shows "Context compacted · 9.4k → 9.2k tokens" and the meter dropped.
- Live Claude proof: pending. The local Claude agent needs an Anthropic
API key. claude-agent-acp handles `/compact` as an echo-less turn, and
the runner already maps its "Compacting completed." notice to
`context.compacted`.

## Risks

- A compaction run resumes the agent's session. If the session cannot
resume, the run fails with `native_context_compaction_unavailable` and
the task is unchanged.
- Compact now is not available while the agent runs on the task, because
compaction interrupts the active turn.
- The conversation view now returns context readings for runs that
compacted. Runs that never compacted return the same events as before.
- An old runner binary ignores `compact` and rejects the `turn.start`
because it has no text. It does not start an ordinary turn.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 22:07:12 -06:00
DottaandClaude Opus 5.5 36643d9378 fix(native): retire finished owners whose continued turn rotated authority (#19373)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The native runner runs per-turn agent sessions in managed sandboxes.
A controller keeps a durable session root for each session.
> - When a per-turn owner finishes without a saved runner checkpoint,
the next turn retires that owner. Retirement first checks that the
session root holds only known entries.
> - The runner transport writes two entries that the check does not
know: `authority-epochs/` and `runtime-context.json`. A continued turn
writes `authority-epochs/` when it rotates authority from the turn
before it.
> - So the third turn of an ephemeral-lease task cannot retire the
second turn's owner. The run fails with
`native_continuation_requires_reconciliation` and the task waits for a
user message.
> - This pull request classifies every session-root entry in one
contract module, and adds a test that scans the writers so the list
cannot drift again.
> - The benefit is that follow-up turns continue without manual help,
and any future refusal names the unexpected entry.

## Linked Issues or Issue Description

Refs #18846, Refs #19061, Refs #19355

**What happened?**
On staging, the third turn of every task for one agent (gpt-5.6-sol,
native runner, ephemeral sandbox lease) failed. The error was
`runner_state_identity_mismatch:
prior_owner_terminal_state_indeterminate;
retirement_refused=unexpected_root_entry`. The first two turns
succeeded. The task then went to `blocked` with "Saved work recovered.
Send a message to continue."

**Expected behavior**
The third turn retires the finished second-turn owner and continues the
conversation.

**Steps to reproduce**
1. Assign a task to a native agent that uses an ephemeral sandbox lease.
2. Let the first turn finish. Reopen the task with a comment so a second
turn runs and finishes.
3. Reopen the task again. The third turn fails with the error above.

**Paperclip version or commit**
Staging near 9a79384958; reproduced locally on master 7c0922a7b6.

## What Changed

- Add
`server/src/services/native-runtime/native-session-root-entries.ts`. It
is the single contract for top-level session-root entries:
- Continuity state (moves with a continuity break): the existing
entries, plus `authority-epochs` and `launch-preparation.json`.
- Launch entries (stay in place, carry no authority):
`runtime-context.json`, `diagnostics`.
- Held entries (retirement still refuses): `plugin-runner-state.json`,
`cleanup-activation.json`, `cursor-cloud`.
- `readSettledRunnerdProjection` uses this contract. The refusal now
names the entries. A held entry reports as
`held_root_entry(cursor-cloud)`. An unclassified entry reports as
`unexpected_root_entry(unrecognized-state)`. Names are bounded to three,
limited to safe characters, and have identifiers replaced with `<id>`.
- Add `native-session-root-entries.test.ts`. It scans every local root
writer (the executor, the Cursor Cloud runtime, the provider-pack
authority, the controller migration, the runner transport, and the
launch-preparation store). It reads string and template literals,
same-file constants, `posix.join` and `path.join`, and the harness
persistence directory names. It fails when a writer adds an unclassified
entry, or reaches the root through a new unresolved variable.
- Add three cases to `native-recovered-owner-followup.test.ts`: a
finished owner whose root has `runtime-context.json`, an archived
authority epoch, `diagnostics`, and `launch-preparation.json` is
retired, and the epoch and launch preparation move into the archive; an
unknown entry still holds the owner and is named; a held entry is named
as held.

## Verification

- Red before the fix (same string as staging): `retires a finished owner
whose continued turn left runtime context and an archived authority
epoch` received `runner_state_identity_mismatch:
prior_owner_terminal_state_indeterminate;
retirement_refused=unexpected_root_entry`.
- Green after the fix:
- `cd server && npx vitest run
src/services/native-runtime/native-recovered-owner-followup.test.ts
src/services/native-runtime/native-session-root-entries.test.ts` (34
passed)
- `cd server && npx vitest run
src/__tests__/native-harness-backup-expiration.test.ts
src/services/native-runtime/native-session-resume.test.ts
src/services/native-runtime/native-session-executor.test.ts` (1313
passed)
- Live confirmation on staging is pending. After deploy, run a
three-turn task on an ephemeral-lease codex agent. The third turn must
succeed. Its log must show "finished without a saved runner checkpoint;
starting a new runner".

## Risks

- Retirement now accepts two more entries. `authority-epochs` only holds
epochs that the transport archived after it verified a suspended runner,
so it carries no live owner claim. `runtime-context.json` and
`diagnostics` are launch inputs and logs. Unknown entries still refuse
retirement.
- A continuity break now also moves `authority-epochs` and
`launch-preparation.json` into the archive. This is the intended owner:
these files belong to the retired authority lineage. If they stayed, a
later cold rotation could read an archived epoch of the retired lineage.
- I could not read the staging sandbox. The evidence for the exact entry
is from code: the second turn's transport writes `authority-epochs/`
during authority rotation, and every launch writes
`runtime-context.json`. The refusal now names the entry, so any other
entry will be visible in the next failure.

## 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-05 19:37:01 -06:00
DottaandPaperclip 7c0922a7b6 fix(native): preserve provider backups during slow workspace saves (#19359)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Remote native sessions save provider state so another web server can
continue the same conversation.
> - A provider can suspend while its accepted result is still saving a
large workspace.
> - The run has released its live lease at that point. A new
backup-owner lookup rejects this uncommitted state.
> - This pull request captures the real owner during backend creation
and retains it with the current session authority.
> - The existing database checks still reject a changed boot,
generation, result, or successor before inspection and during
publication.
> - The benefit is that a slow workspace save no longer prevents the
provider backup.

## Linked Issues or Issue Description

**What happened?**

A remote session can fail to save its provider backup with
`native_harness_storage_owner_missing`. The provider has suspended, but
its accepted result is still in `workspace_finalizing`. The close
callback tries to acquire a new owner after the run released its lease.
A later allocation cannot use a backup that was never published.

**Expected behavior**

Save the suspended provider with the authority acquired during
execution. Recheck that authority before provider inspection and before
and after upload. Keep result finalization and provider-work permissions
unchanged.

**Steps to reproduce**

1. Start a remote native session with durable provider storage.
2. Accept its result and release the live lease while its workspace save
remains pending.
3. Close the provider transport. The old owner lookup rejects the save
before provider inspection.

**Paperclip version or commit**

Reproduced on master `bf10bc695abaef6b20e85b287f456b5ce2edb266`.

**Deployment mode**

A web server with object storage and remote managed Runner sessions.

Related: #19225 retains failed-turn ownership through close. #18436
restores an external controller checkpoint before a changed warm process
is replaced. This change fixes late backup-owner acquisition while an
accepted result saves its workspace.

## What Changed

- Capture the provider backup owner when the remote backend still owns
its execution lease.
- Bind that owner to the current session authority. An older close
callback selects the current warm attachment's owner. Revalidate the
captured fence before inspecting or skipping provider state.
- Add a regression for the close callback during workspace finalization.
- Restore encrypted provider files with a captured owner while workspace
finalization remains pending. Reject boot, generation, result, and
successor changes during upload.
- Document the ownership contract.

## Verification

- Observed the callback regression fail on the old code with
`native_harness_storage_owner_missing`. The same test passes with this
fix.
- `cd server && node ../node_modules/vitest/vitest.mjs run --config
vitest.config.ts
src/services/native-runtime/native-harness-backup-storage.integration.test.ts`:
59 passed, using real PostgreSQL and encrypted object write/readback.
- `node scripts/check-module-boundaries.mjs`: passed.
- Full workspace typecheck: passed with the complete production fix.
- Native execution: 1,151 passed. Managed warm lifecycle: 9 passed.
These tests require a lost-owner error during shutdown, including before
an incomplete provider checkpoint is skipped. The final change to the
lifecycle test also requires rejection after lease renewal is lost.
- Full local `pnpm build`: passed on the final head.
- The full local `pnpm test:run` stopped in chat shard 6 on five
existing deadline assertions. Earlier chat shards passed; the PR CI chat
shards passed. This is not a full local-suite pass.
- The first CI server shard 4 attempt failed the unchanged legacy
environment-inheritance fixture at its five-second completion wait. The
same failure reproduced locally on the repair head and on an earlier
checkout without this repair. One unchanged CI retry passed. No fixture
assertion or timeout change is shipped.
- Latest-head CI has passed. Greptile reviewed the final head at 5/5
with no open threads. The separate Paperclip Review is paused. This PR
does not claim staging acceptance.

## Risks

The backup owner now lives with its session authority. A wrong authority
selection must fail rather than publish provider state. Database
publication still checks the original company, task, run, boot,
generation, and accepted result before and after upload. Tests change
the owner during upload and verify that the selected backup stays
intact. No schema, data migration, or new provider-work permission
changes.

## Model Used

OpenAI Codex, GPT-6 model family, with reasoning and code execution
tools. The host does not expose the exact runtime model ID or
context-window limit.

## 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-05 18:04:47 -06:00
DottaandPaperclip 789a71bc17 fix(runner): retry quarantined tasks in their saved workspace (#19340)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native sessions retain execution ownership and action history across
turns.
> - Task Retry must prove that old execution has stopped before it
starts another turn.
> - An attempt can inherit its predecessor's identity and fail before it
launches a provider.
> - The cleanup proof searched under that failed attempt and compared
unrelated event sequences.
> - This pull request verifies the retained owner and starts one fresh
authorized turn.
> - Saved results and uncertain action outcomes remain intact.

## Linked Issues or Issue Description

**What happened?** Task Retry accepted repeated clicks but kept the
canonical request deferred with `executionWait.reason: local_cleanup`.
It created no successor, although the retained local worker and provider
had stopped.

**Expected behavior:** After exact cleanup proof, admit one fresh turn.
Preserve the old journal and result. Keep unresolved action outcomes
unknown.

**Steps to reproduce:**
1. Finish a local native turn with retained cleanup state and a saved
result.
2. Let the next attempt inherit its identity and fail with
`native_session_cleanup_quarantined` before any launch.
3. Upgrade across a tool-contract change, then click task Retry.
4. The old code requires the failed attempt's stop receipt and journal
identity. It also requires the current tool fingerprint and equal
sequence numbers across raw and normalized events. Those requirements
cannot match the retained owner.

**Paperclip version or commit:** Reproduced on `31bf6e4482`.
**Deployment mode / installation method:** Authenticated, self-hosted
source checkout.
**Agent adapter(s) involved:** Native `paperclip_runner`, local Codex
app-server. The shared physical-stop proof also covers ACPX.
**Database mode:** Embedded PostgreSQL 18.1.
**Access context:** Board task Retry.
**Node.js version / operating system:** Incident: Node 24.20.0, Linux
ARM64. Local verification: macOS ARM64.
**Relevant logs or output:** Accepted Retry stays in
`deferred_issue_execution`; no new run or provider launch occurs.
**Relevant config:** Per-turn local native execution. The old tool
contract differs from the current contract.
**Additional context:** Related: #19311 prevents the oversized tool
result that can lead to retained cleanup. #18217 covers broader cleanup
isolation and retained-result maintenance. #19134 admits stopped Runner
sources under the current run lease. This patch fixes explicit
continuation admission on current master.

## What Changed

- Resolve the retained journal's owner under the same company, task,
agent, workspace, native session, and runner instance.
- Require terminal ownership, settled controllers, released local
leases, and exact inherited process birth. Refuse any launch evidence on
the failed attempt.
- Accept a current host's verified process absence from its exact launch
receipt when an older server lacks a stop receipt. A later launch
invalidates that observation.
- Validate normalized receipts against their own IDs, sequences, and
digests. Match ordered provider identities to the independent raw
journal.
- Keep tool compatibility at the session reuse boundary. Explicit
continuation always uses a fresh session.
- Bind the stop proof to its exact successor and canonical Retry
receipt. Reuse the proven local workspace through normal preparation.
Refuse changed bindings, closed or remote workspaces, pending
operations, and other live leases. Projectless run-ID placeholders keep
the existing fresh-workspace fallback.
- Retain journals, results, files, and uncertain action outcomes. Add
regression and opt-in live acceptance coverage.
- Make the split-handoff browser test wait for both child-completion
notices and all run settlement before teardown. Assert the final task
set stays unchanged.

## Verification

- Red: canonical Retry on the original code accepts one saved intent but
creates zero successors and records `local_cleanup`.
- Red: distinct raw and normalized event sequences reproduce that wait
after owner resolution alone. After admission was repaired, live
acceptance then exposed the empty-attempt workspace problem. A
PostgreSQL regression also reproduced that wrong workspace selection.
- Green at `6684df5e82`: **479 targeted cases**, including 26 new
PostgreSQL cases for duplicate clicks, exact process and workspace
ownership, controller/lease fences, changed state, and independent
receipt integrity. The complete 1,150-test native executor suite passed
earlier.
- `pnpm -r typecheck` and `pnpm build` pass again at final head
`432a52bea8`, including its build stamp. Token gates pass. Master now
contains the unrelated directory-fixture type correction found during
the first typecheck.
- Real authenticated HTTP Retry with Codex `gpt-6.1-sol`, high effort,
and per-turn execution passes again at final head `432a52bea8` (93.3
seconds). It preserves the first file line, appends the second once,
starts a fresh session in the existing workspace, keeps the retained
journal unchanged, commits finalization, and clears task locks.
- The strengthened split-handoff browser scenario passes **three
consecutive local runs**. Its original CI teardown and one same-head
rerun failed when cleanup cancelled an active legacy
completion-notification run and hit a PostgreSQL row-lock timeout. The
scenario now waits for the complete handoff before cleanup. [Initial
failure](https://github.com/paperclipai/paperclip/actions/runs/42106466518/job/127523693634),
[same-head
rerun](https://github.com/paperclipai/paperclip/actions/runs/42106466518/job/127531698321).
These failures remain part of the verification record.
- The local full `pnpm test:run` passed all six chat integration shards.
Its obsolete pre-rebase attempt was stopped during the general server
phase; no complete local-suite pass is claimed. Full CI, including both
required gates, passed at `6684df5e82`. At final head `432a52bea8`, all
initial CI groups passed except the unchanged status-card deadline test,
whose post-wakeup clock assertion missed its bound by 10 ms. That exact
case passed three unchanged local repetitions (16.8/27.0/18.3 seconds);
the single isolated shard rerun passed all **2,218 tests** (123 files),
with no source changes. [Initial timing
failure](https://github.com/paperclipai/paperclip/actions/runs/42114473652/job/127550722187).
- Greptile reviewed `6684df5e82` at **5/5**, with no actionable
findings. A second reviewer identified the supported projectless
placeholder case. A new real PostgreSQL regression reproduced its
workspace-binding failure before the one-line fallback correction; all
**45 focused cases** now pass, including default environment selection
and changed binding/cwd refusal. Greptile reviewed final head
`432a52bea8` at **5/5**, with no blocking findings. The second
reviewer’s thread is resolved.
- The patched Linux build verified the actual retained worker/provider
ownership and process absence in a transaction that was rolled back. No
retained state was retired or provider started during that
pre-deployment check.

## Risks

- This changes an execution admission proof. Incorrect ownership could
permit a second executor. The proof retains process, controller, lease,
scope, journal, and receipt fences, and duplicate admission is tested
through the real queue.
- Missing or conflicting evidence remains deferred. Windows, remote
execution, automatic replacement, and session reuse retain their
existing policies.
- No database migration or UI change. The original affected task still
needs deployment of the fix before recovery can be claimed.

## Model Used

- OpenAI GPT-6 through Codex, with reasoning, repository tools, code
execution, SSH diagnostics, and live provider testing. This session does
not expose the exact backend model build or context-window size. The
live test agent uses OpenAI `gpt-6.1-sol` at high effort.

## 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-05 16:04:38 -06:00
Nicky LeachandPaperclip 2506bcdaf4 Admit exact stopped Runner sources under the current run lease (#19134)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native execution keeps provider state and a durable controller
journal.
> - A completed process can stop before the next run attaches.
> - The current run must own recovery of that exact source.
> - An old controller must lose permission to change the consumed source
journal.
> - This pull request adds explicit source admission and protected task
dispatch.
> - The benefit is a narrow recovery boundary that preserves the
original successful result.

## Linked Issues or Issue Description

Native prerequisites #19132 and #19153 are merged into master. This PR
targets master and updates the TypeScript Runner driver contract
alongside Core and SDK admission. This change adds the SDK and Core
admission boundary. Executor selection and fresh source preparation are
a separate change. Related #17916 supplies the workspace export holds
preserved here.

## What Changed

- Plugins advertise `codexSourceRecoveryVersion: 1` for Codex App Server
and `acpxSourceRecoveryVersion: 1` for ACPX. Hosts can qualify either,
both, or neither. Core selects the matching field, because a
stopped-source check for one provider cannot establish the other
provider’s recovery guarantees. ACPX orchestration is tracked separately
in #19196. Use the existing exact source identity and `resumeFrom` under
the authenticated current run. No public recovery mode is added. Require
the claimed source, current account and prepared launch at protected
dispatch.
- Keep the first source binding pinned on the consumer lease, including
after uncertain dispatch. A newly acquired coordinator can refresh only
the same source.
- Claim one stopped successful source under the current run's acquired
native lease. Store the token-free consumer reference in existing lease
metadata. Preserve the source status and result.
- Check the exact six source identity fields, encrypted journal header,
company, task, agent, environment, driver, workspace, account selection,
and current coordinator generation. Keep workspace export and legacy
settlement holds.
- Serialize claims and checkpoint writers in the same coordinator lock
order. Reject stale source writes after consumption. Check the current
consumer before recovered source writes.
- Check source admission before and after protected worker dispatch.
Keep the request and lease owned by the current run while the native
identity remains the source.
- Allow the same owner to prepare B after a fresh source submission,
using the exact consumed-source proof instead of a generic warm-reuse
flag. Recheck that proof after dispatch.
- Preserve ordinary cached adoption and repeated preparation after
launch. The current authenticated owner can differ from the adopted PRP
identity without requesting a new source launch. Keep ordinary suspended
recovery and live task attachment checks. Extract the shared source
resolver to avoid a module cycle, and make ordinary selection honor the
same durable source consumer.

## Verification

- Current head `cac9cf188a` includes merged #19300 and the symmetric
capabilities introduced in `b7e25a64b4`. The task-owner and warm-rebind
fixtures now identify their physical task. All 29 SDK schema cases, 95
Core ownership and source-admission cases, 25 native Codex and ACPX
recovery cases, and the binary metadata check passed on this head. The
SDK and Runner builds and server typecheck passed. Both native providers
reject the other provider’s capability. GitHub CI is green and [standard
Greptile scored this commit
5/5](https://github.com/paperclipai/paperclip/pull/19134#issuecomment-6645574114)
with no open threads.

- Previous head `bda49271a4ae362a15baf3fe6d6d297e25fd2000` is directly
based on master after #19132 and #19153 landed. [GitHub workflow
42072192023](https://github.com/paperclipai/paperclip/actions/runs/42072192023)
passed on its first attempt. All check runs completed successfully or as
intentional workflow skips; Snyk passed. Standard Greptile scored this
exact head 5/5. That review had no unresolved findings.
- Runner and SDK TypeScript builds and server typecheck passed. The
focused SDK, task-owner, task-runtime, PostgreSQL source and authority
suites passed all 161 cases. The production transport-factory getter
regression and 14 native-lane selection checks also passed.
- The Linux native lane executed the new ordinary warm A→B process case.
It kept one Runner/provider process and completed two turns on the same
thread. The eager-getter mutation fails with
`native_runner_source_attachment_pending`; preserving the live getter
passes. The production factory test changes the current owner after
transport creation and checks that same options object.
- PostgreSQL cases cover both concurrent source-writer/claim orders,
exact source submission under the current caller, source-switch denial
after uncertain dispatch, same-source coordinator refresh, stale
same-boot generation, corrupted journal headers, competing consumers,
account changes and export holds.
- The protected dispatch sequence submits A, prepares B and retries B
preparation. It rejects undispatched sources, changed accounts and
foreign target bindings. Source status and accepted result remain
unchanged.
- Ordinary adopted A preparation under current B, repeated ordinary
preparation after launch and ordinary suspended continuation retain
their existing path. Absent or false claims preserve ordinary identity
denial; malformed claims fail closed.
- Earlier development attempts exposed ordinary preparation regressions
and the live-getter omission; the final regressions cover those fixes.
The first embedded PostgreSQL attempt needed missing library aliases
repaired in ignored dependencies. Those attempts are not passing
evidence.
- Historical, superseded CI heads had unchanged UI fake-timer and
Playwright health-response-lifetime failures. Their isolated cases and
scoped retries passed. The response-lifetime cause remains unproven. No
UI/test timeout was changed. The final rebased-head workflow above
passed without a retry.
- Whitespace and added-line secret/PII scans passed. Full workspace
checks ran on GitHub. No live vendor probe or deployment was performed.

## Risks

- Any driver that used the earlier Codex TypeScript option must use
`codexSourceRecoveryVersion`. The new plugin description rejects the
generic spelling. The Launcher’s existing Codex-specific HTTP header can
map to this property when the plugin adopts the new SDK. The change
requires no journal migration or ROM change.

- This is an inactive admission foundation. Executor orchestration is
separate in #19157. Native restore integrity and provider
stopped-source/account validation must be present before executor
activation. The native prerequisite uses ordinary recovery followed by
attachment. The composed executor proofs are in #19157.
- The source claim is durable. A later controller must recover the same
consumer or keep the hold. It cannot select another source after an
ambiguous launch.
- Admission adds bounded database checks around protected startup calls.
Busy issue locks fail closed; other lock waits have a one-second bound.
- No migration, snapshot format, proactive receipt, recurring worker, or
deployment is included. Full workspace checks and platform coverage run
in CI. These tests do not prove vendor history durability or a hosted
rollout.

## Model Used

OpenAI GPT-6.1 Sol (`gpt-6.1-sol`), high reasoning, through Codex with
repository tools and code execution. The runtime does not expose a
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 on the current head
- [x] Standard Greptile is 5/5 with no unresolved findings on the
current head
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-05 12:55:06 -08:00
DottaandClaude Opus 5.5 22281d4b3a feat(task-chat): approve harness-proposed plans (Codex plan mode, Claude ExitPlanMode) (#19291)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents run through the native runner. Codex and Claude Code each
have their own plan mode, and each produces a proposed-plan document.
> - Paperclip already has a planning work mode, an issue `plan`
document, and a `request_confirmation` approval gate for plans.
> - These parts were not connected. The Codex proposed-plan item was
transcript text only. The server path never put Codex into plan mode.
Claude was never put into plan mode, and its ExitPlanMode plan was
dropped. A planning run could close its task as done with no approval.
> - This pull request turns a harness's own proposed plan into the
issue's Plan document and the existing approval gate, for Codex and
Claude.
> - The benefit is that users approve or send back the agent's real plan
from the task chat. Approve resumes the agent. Request changes sends the
reason back to the agent.

## Linked Issues or Issue Description

Refs #19252

**Subsystem affected**
Native runner provider events (Codex and ACPX Claude), the runner Codex
provider (Rust), server native plan synchronization, and the task chat
UI.

**Problem or motivation**
Codex and Claude both produce a proposed-plan document in their native
plan mode. Paperclip did not use it. In live tests on the server path, a
Codex planning run was not in Codex plan mode, saved its own
`implementation-plan` document, and marked the task done without
approval. Claude planning runs were not in Claude plan mode, and the
ExitPlanMode plan text never reached Paperclip.

**Proposed solution**
Add a canonical `plan.proposed` event for a harness's proposed plan. Put
Codex and Claude into their native plan mode for planning runs. Save a
completed proposal as the issue `plan` document with the existing
`request_confirmation` gate. Agents can also propose a plan on a
standard task; the work mode stays standard.

**Alternatives considered**
Reuse `plan.updated`. Its schema is the within-turn checklist and is not
the durable Plan document, so a separate event keeps the two clear.
Stronger prompt text without native plan mode. Live tests showed that
the model kept its own document workflow.

**Roadmap alignment**
This reuses the issue plan document, planning work mode and approval
gate that already exist. It adds one canonical provider event.

## What Changed

- `packages/paperclip-runner/protocol/schemas/*`: add the
`plan.proposed` event (`paperclip.plan.proposed.v1`: `proposalId`,
`status`, `markdown` up to 60k characters, `source`). Regenerate the
schema bundle, validators and manifest.
- `packages/paperclip-runner/src/provider-events.ts`: map the Codex
`plan` item and a `<proposed_plan>` block in a completed Codex message
to `plan.proposed`. Add `claudePlanProposalEvent`.
-
`packages/paperclip-runner/runner/crates/runner-core/src/codex_provider.rs`:
send Codex's native `collaborationMode: plan` on `turn/start` for a
read-only (planning) Codex run. Before this change the server path never
sent it.
- `runner-core/src/acpx_event_payload.rs`: admit `plan.proposed` as ACPX
rich display activity.
- `drivers/acpx/codex-runtime-adapter.ts`: set Claude Code to native
plan mode for a read-only run. Capture Claude's ExitPlanMode plan from
its permission request, record it, and decline the native "Ready to
code?" prompt.
- `drivers/acpx/codex-acpx-driver.ts`, `cli/acpx-runtime-sidecar.ts`:
emit the captured proposal as a rich event. Complete that turn instead
of reporting it as cancelled.
- `backends/codex-native-backend.ts`: per-harness plan-mode
instructions, and a short opt-in hint for Claude on standard tasks.
- `server/src/services/native-runtime/native-session-executor.ts`:
synchronize a completed `plan.proposed` into the `plan` document and a
`request_confirmation` in planning and standard mode. Do not change the
issue status during the run (that revoked the run's lease). The run
finalizer turns the pending confirmation into the review wait.
- `server/src/services/heartbeat-run-summary.ts`: keep a
`<proposed_plan>` block out of the posted reply.
- UI: new `plan_proposal` provider family. The task chat shows a live
"Writing plan" card while a proposal streams and a one-line record when
it completes. Resolved plan reviews keep a receipt ("Approved Plan v1",
"Requested changes to Plan v1: reason"). Plan previews skip section
headings.
- Storybook: `Tasks and conversations/.../Task chat/Proposed plan
approval` shows the journey.
- `doc/SPEC-implementation.md`: describe harness-proposed plan approval.

## Verification

- `cargo test -p paperclip-runner-core --lib` (new:
`read_only_codex_turns_use_native_plan_collaboration_mode`)
- `cd packages/paperclip-runner && npx vitest run
src/provider-events.test.ts
src/drivers/acpx/codex-runtime-adapter.test.ts`
- `cd server && npx vitest run
src/__tests__/native-plan-proposal-sync.test.ts
src/__tests__/heartbeat-run-summary.test.ts`
- `cd ui && npx vitest run
src/components/transcript/native-run-events.test.ts
src/components/task-chat`
- Live test on a local worktree instance with real harnesses:
- Codex (`gpt-5.6-sol`), planning task: Codex asked one question, then
produced a native plan item. The task showed Plan v1 with Approve plan /
Request changes and moved to in review. Approve changed the work mode to
standard. Codex then implemented the plan and its tests passed.
- Codex, Request changes with a reason: Codex woke in planning mode and
produced Plan v2 that includes the change.
- Claude (`claude-opus-5-5`), planning task: Claude was in plan mode.
ExitPlanMode became Plan v1 and the turn completed. Approve resumed
Claude in standard mode, and Claude implemented the plan.
- Claude, standard task that asks for a plan first: Claude stopped
before code changes and asked for plan approval. The work mode stayed
standard.
- Storybook: open `Proposed plan approval` and check each stage.

## Risks

- Codex planning runs now really use Codex plan mode on the server path.
Codex can ask a clarifying question before it proposes. This matches
Codex plan mode.
- Claude planning runs now use Claude plan mode. A declined ExitPlanMode
ends the Claude turn; the runner reports it as completed. Claude's tool
row for that request records a rejection from Claude's side.
- Codex does not stream the proposed plan on the server path yet, so the
live card shows a placeholder until the plan completes.
- A `<proposed_plan>` block in any completed Codex message on a standard
task now asks for approval. This is the documented opt-in path.

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

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), via Claude Code with
tool use (shell, file edits, browser). Long context. No extended
thinking mode was used.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] 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-05 20:53:33 +00:00
DottaandClaude Opus 5.5 dd889e6a0b feat: show the assignee's provider usage limit in task properties (#19270)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents run on the native Runner through the Codex app-server and the
Claude ACP harness
> - Both harnesses report the provider account's rate-limit windows:
Codex sends `account/rateLimits/updated`, and Claude sends its SDK
`rate_limit_event` as `usage_update._meta["_claude/rateLimit"]`
> - Paperclip dropped both signals, so an operator could not see that an
agent was near or past its provider limit
> - This pull request normalizes both signals into one account-scoped
snapshot, keeps the latest snapshot per agent, and shows it as one
"Usage limit" line in the task properties pane
> - The benefit is that an operator sees "5h 78% · resets 15:40" (amber
above 80%, "Limited until …" when the provider refuses work, "using
credits" when a used-up window falls back to credits) next to the
assignee, and can click it for every window

## Linked Issues or Issue Description

Refs #19246
Refs #19259

**Is your feature request related to a problem?**
Codex and Claude report provider rate-limit windows during a run.
Paperclip dropped them: the Codex notification fell through the Rust
normalizer with no branch, and ACPX dropped Claude's `_meta`. Operators
could not see how close an agent was to its provider limit.

**Describe the solution you'd like**
Store the latest reported windows per agent and show one compact line in
the task properties pane, with a popover that lists every window.

**Describe alternatives you've considered**
A full gauge in the agent list and the run header. A design review chose
one property line with a details popover instead.

## What Changed

- Runner (Rust, the runnerd path): new `provider_rate_limits.rs`. Codex
`account/rateLimits/updated` becomes a `provider.notice.recorded` event
(category `provider.rate_limits`, scope `account`). `CodexProviderState`
merges sparse updates and emits a notice only when the windows change.
ACPX `usage_update` with a sanitized `rateLimit` field also emits the
notice.
- Runner (sidecar): `acpx-runtime-sidecar.ts` forwards only `status`,
`rateLimitType`, `utilization` and `resetsAt` from
`_meta["_claude/rateLimit"]`. This needs the ACPX `_meta` passthrough
from #19259. Until that merges, Claude emits nothing.
- Runner (TypeScript in-process drivers): the same mapping in
`codex-session-notifications.ts` and `codex-acpx-driver.ts` through
`drivers/provider-rate-limits.ts`.
- Shared: `provider-rate-limits.ts` holds the snapshot type, the parser,
the merge, and `summarizeProviderRateLimits` (fullest current window,
expiry by reset time, limited versus using credits).
- Server: `agent-provider-rate-limits.ts` merges each notice into
`agent_runtime_state.state_json.providerRateLimits`, scoped by company
and agent. A session reset keeps the snapshot. No migration.
- UI: `AgentRateLimitPropertyValue` adds a "Usage limit" row under the
assignee in `IssueProperties`. Transcripts treat the notice as run-log
only. A run status event refreshes the assignee's runtime state.
- Storybook: `Tasks and conversations/Task creation and lifecycle/Usage
limit property`.
- Docs: `doc/run-log-events.md` describes the notice and its storage.

## Verification

- `cd packages/paperclip-runner/runner && cargo test -p
paperclip-runner-core --lib rate_limit` and `--test
acpx_provider_events`: sparse Codex merge, dedupe, reached window,
checkpoint round trip, Claude utilization and rejection, notice shape.
- `cd packages/paperclip-runner && npx vitest run
src/drivers/provider-rate-limits.test.ts src/drivers/codex`
- `cd packages/shared && npx vitest run
src/provider-rate-limits.test.ts`: summary, expiry, limited, using
credits, parse and merge.
- `cd server && npx vitest run
src/__tests__/agent-provider-rate-limits.test.ts`: create and merge,
out-of-order snapshot, company isolation, unrelated notices, session
reset keeps the snapshot.
- `cd ui && npx vitest run src/components/transcript
src/components/issue-properties src/context/LiveUpdatesProvider`
- `pnpm -r typecheck` and `pnpm check:token-gates` pass.
- Live test on a local worktree instance: a native Codex agent on a
ChatGPT subscription login ran a real task. The runner recorded one
`provider.rate_limits` notice. The server stored it. The task properties
pane showed "7d 100% · using credits" (this account's weekly window was
used up and had credits, and the run succeeded). The transcript showed
no extra row.
- Not verified live: Claude. The local Claude CLI was not signed in, and
the Claude path also needs #19259. The Rust and sidecar paths have
tests.
- Not run locally: the full `pnpm test:run` and `pnpm build`. CI runs
them.

## Risks

- Low risk. The snapshot is display data. It does not hold or start
work. The existing provider-limit wait (#19246) still reads typed run
failures.
- Codex can send a rate-limit update often. The runner emits a notice
only when the windows change, so run-log growth stays small.
- With API-key auth, the harnesses report no windows, so no row appears.
This is the intended result.
- `CodexProviderState` gains two optional serde fields with defaults, so
older checkpoints still load.

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

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, through Claude Code
with tool use (shell, file edits, browser) 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-05 20:26:51 +00:00
Nicky LeachandPaperclip 31bf6e4482 Pin plugin Runner cleanup to its physical task (#19300)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Plugin Runner owners manage a physical task through an environment
lease.
> - The existing replacement path can assign a new physical task to that
same lease.
> - A delayed old owner can then stop the replacement or mark it
stopped.
> - Cleanup must keep the original physical task identity across RPC and
database writes.
> - This pull request pins that identity and keeps cleanup available to
the new owner.

## Linked Issues or Issue Description

Refs #19134 and #19157. This fix is based on master and does not enable
repeated stopped-source recovery. A search found no duplicate fix.

**What happened?**

The maintained `createOwnedSandbox` replacement path changes
`providerLeaseId` on one lease. An old task owner uses that mutable
lease for a later Stop. A stopped-state response already in flight can
also update the new task's flags.

**Expected behavior**

The old owner must address its original task. Its late cleanup writes
must not alter the replacement. The replacement's own owner must still
stop and complete it.

**Steps to reproduce**

1. Create a plugin task owner for a stopped task.
2. Replace that task through the existing describe/startup path on the
same lease.
3. Resume an old checkpoint or a held old status response.
4. Observe a Stop for the replacement or an incorrect stopped flag
before this fix.

## What Changed

- Require each task owner to keep the physical ID returned by its
description. Reject a rebind to a different task.
- Carry the ID through the existing internal task context after startup
ends. Reject a changed physical task before RPC dispatch.
- Match the physical task in stopped-state, completion and
credential-handoff writes.
- Hold the exact lease row through the retained-session database update
and local checkpoint save.
- Cover the maintained replacement path with real PostgreSQL tests.
Preserve positive cleanup for the replacement owner. Document this
boundary.

## Verification

- Both replacement regressions failed on unmodified master `10a7d039c8`:
delayed old-owner Stop targeted the replacement, and a held old status
response marked it stopped.
- Six focused files passed 160 tests: task authority, task runtime, warm
tasks, execution target, credential writeback and task owner.
- Four selected executor factory cases passed. They cover task-only
providers and the physical task binding after the startup context ends.
- Runner TypeScript, plugin SDK and adapter-utils builds passed. Server
TypeScript check passed. Diff and added-line secret scans passed.
- All 15 setup-probe cases passed with the actual Runner and fake
provider binaries. The first local attempt lacked cargo on PATH. A
sandboxed native attempt timed out in seven process cases; the unchanged
run with loopback permission passed all 15. A separate initial DB
integration attempt was denied a loopback listener; its
permission-correct run passed.
- Review found three missed constructors in the existing SDK/Core
lifecycle fixture. They now use the actual describe result. All four
cases in that real PostgreSQL and worker RPC file passed.
- The first GitHub attempt failed in the unchanged GitHub recovery
pagination case in `chat-channels.integration.test.ts`: its zero-request
assertion observed a configuration GET. That exact case passed locally
without code or assertion changes. The one failed-job-only retry passed;
the initial failure log is retained.
- [GitHub workflow
42082587576](https://github.com/paperclipai/paperclip/actions/runs/42082587576)
passed on its second attempt. All CI checks completed, and Snyk passed.
Standard Greptile scored exact head `fb914d98b99e` 5/5 with no
unresolved threads. The branch is mergeable and ready for review.

## Risks

The task context change is internal. It adds no plugin wire field,
schema or Runner mode. A stale owner now gets an error and cannot clean
up a replacement. A new owner can still clean up that task. The retained
checkpoint write holds one existing lease row during a bounded local
file write.

Repeated stopped-source incarnation admission, runtime pins and
deployments are outside this change.

## Model Used

OpenAI GPT-6.1 Sol (`gpt-6.1-sol`), high reasoning. Used repository
tools, TypeScript and real PostgreSQL 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 described the issue in-PR using the bug template
- [x] I have not referenced internal or instance-local Paperclip issues
or links
- [x] My branch name describes the change
- [x] I have run the focused tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation
- [x] I have considered and documented risks
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no unresolved findings
- [x] I will address reviewer comments before requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-05 12:14:09 -08:00
DottaandClaude Opus 5.5 34d308a331 fix(native): let a run retire the prior owner it reserved when its session rotates (#19263)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A native Runner agent keeps a warm provider session per task. A
follow-up run on the same task can reuse that session and its managed
instruction directory.
> - When a follow-up run cannot resume the saved session (for example,
the effective run configuration changed), it first reserves the prior
owner's instruction directory. Then it starts a new session identity and
must retire the prior owner.
> - Retirement treated the run's own directory reservation as another
executor's busy turn. The run failed at startup with
`native_session_supervisor_busy`.
> - That failure happened after the remote workspace was prepared, so
failure handling retained the sandbox and blocked the task. A board
follow-up comment left the task `blocked` with no agent progress.
> - This pull request lets a run retire the prior owner that it reserved
itself, and keeps the directory that it adopted. Any other reservation
or turn is still busy.
> - The benefit is that a follow-up on a task with a rotated session
runs once, and the task does not become blocked.

## Linked Issues or Issue Description

**What happened?**
A board follow-up comment with reopen woke an agent on a task that had
just finished a successful run. The new run reused the task's own
sandbox. The saved session could not resume because the effective run
configuration changed, so the run started a new session identity. At
startup (right after the native execution claim) the run failed with
`native_session_supervisor_busy`. The run got the error code
`native_continuation_requires_reconciliation`, the sandbox was retained
for workspace repair, and the task became `blocked`.

The agent had another live run on a different task at the same time.
That run used a different sandbox and a different workspace, so it was
not the cause. The busy owner was the task's own prior warm session. The
new run had reserved that session's instruction directory itself
(`reserveWarmNativeInstructionDirectory` sets `preparingRunId` to the
new run). `retireSupersededWarmNativeSessions` then saw `preparingRunId`
and rejected the run.

**Expected behavior**
The follow-up run retires the prior warm owner of its own task, keeps
the instruction directory it adopted, starts the new session, and runs
once. The task does not become blocked.

**Steps to reproduce**
1. Run a native Runner (Codex app-server) task to completion with a warm
session and a managed instruction directory.
2. Change the agent's effective run configuration, so the saved session
cannot resume.
3. Wake the same task again (for example, a board comment with reopen).
4. Before this change, the run fails at startup with
`native_session_supervisor_busy` and the task becomes blocked.

**Paperclip version or commit**
Observed on staging at `7fd05e13`. The code is unchanged on `master`.

## What Changed

- `retireSupersededWarmNativeSessions` now receives the current run id.
When the prior owner of the same task (same logical owner scope) is
reserved by this run (`preparingRunId === runId`), retirement continues
instead of throwing `native_session_supervisor_busy`.
- That retirement closes the prior owner with
`preserveInstructionsForRunId`, so the directory that this run adopted
is not collected. This matches the existing configuration-change branch
in warm reuse.
- After a successful close, the reservation is cleared. A failed close
keeps the reservation, so its `release()` retries retirement as before.
- A reservation by any other run, a busy owner, and an executing scope
still throw `native_session_supervisor_busy`.

## Verification

- Red before the fix, green after
(`server/src/services/native-runtime/native-session-executor.test.ts`,
"managed directory warm checkpoints"):
- "retires its own reserved directory owner when the session identity
rotates": before, the run rejected with
`native_session_supervisor_busy`. After, the prior owner closes once,
the new session runs once, the adopted directory is not collected during
retirement, and it is collected once when the new owner retires.
- "retires its own reserved directory owner after the Runner claim when
the session identity rotates" (the production `useRunnerd` path, where
retirement happens after the claim): before, admission rejected with
`native_session_supervisor_busy`. After, admission retires the prior
owner and keeps the adopted directory.
- Safety case (green before and after): "still rejects a session
identity rotation over another run's directory reservation". The
provider does not start and the other run's owner is not closed.
- Existing safety case (green): "does not retire an active turn when a
new session identity arrives".
- `npx vitest run
src/services/native-runtime/native-session-executor.test.ts
src/services/heartbeat/instruction-working-copy`: 1163 passed.
- Staging evidence was read only. This change is not deployed.

## Risks

- Low risk. The change applies only when the prior owner has the same
logical owner scope (company, agent, task, workspace, environment) and
its reservation names the current run. That reservation exists only
after this run validated reuse of the prior directory.
- The claim-path instruction copy (`claimWarmNativeInstructionCopy`,
which sets `busy`) is unchanged. That path is not used by Codex
app-server agents.
- Two related points are unchanged and are out of scope here: a startup
failure after the workspace is prepared still retains the sandbox and
blocks the task, and the "Instruction edits could not be recovered"
warning on that failure is advisory only. With this fix, this flow no
longer reaches that failure.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use (shell,
file edits, browser read of staging run records) 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
- [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

Refs #18893 (shared-sandbox ingress retirement). Refs #19143
(workspace-busy wait).

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 10:18:21 -06:00
DottaandClaude Opus 5.5 e37ab23dbc fix: hold an agent's new work while its provider usage limit is active (#19246)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Routine-driven agents run on the native Runner through ACP harnesses
such as Claude
> - When the agent's provider account runs out of allowance, every new
wake for that agent fails the same way
> - The ACP harness reported the exhausted allowance as a generic
"terminal limit failure" with an `unknown` category, so Paperclip could
not tell it from a context or turn limit
> - Paperclip recorded a reset time only for the failing run's own
startup retry; other wakes of the same agent kept dispatching, each
spent about two minutes on workspace setup, and then failed
> - This pull request classifies the limit, keeps its reset time, and
holds the agent's new work until that reset
> - The benefit is that a limited agent stops burning runs, shows a
clear "waiting for usage limit reset" state with a time, and resumes its
queued work after the reset

## Linked Issues or Issue Description

Refs #13148
Refs #13379

**What happened?**
A routine-driven Claude agent on the native Runner failed 29 times in
about 15 hours. 20 runs failed with `native_provider_terminal_failed`
and "ACP agent reported a terminal limit failure." The recorded provider
failure had category `unknown`. 6 runs failed with
`provider_session_open_rejected`. Their `session.open` result is
`command_execution_failed` with classification `provider_rate_limit`,
and the provider failure is HTTP 429 `rate_limit` ("would exceed your
account's rate limit"). The startup retry of one such run recorded a
cooldown until one hour later. A new wake for a different task of the
same agent dispatched 16 minutes later, spent two minutes on workspace
setup, and failed with the same 429. Separately, 4 runs failed with
"Project authentication conflicts with this agent's managed AI
connection". That message did not say which file or setting conflicted.
The same message was also shown when the check could not finish.

**What did you expect?**
A provider usage limit is classified distinctly. Paperclip records the
reset time when the provider reports one. New work for that agent waits
for the reset with a visible reason. Queued work resumes after the
reset, and no doomed run starts. A configuration conflict is reported
only when one is found, and the message names the file to fix.

**Steps to reproduce**
1. Use a native Runner agent with a Claude ACP harness whose account is
rate limited or out of usage.
2. Let a routine or assignment wake the agent on several tasks.
3. See each run fail with a generic terminal limit failure or a
`session.open` rejection, and see new runs keep starting.

## What Changed

- Runner: The ACPX turn now receives the typed AIR session failure
(`onTerminalSessionFailure`) through the runtime host and the ACPX port.
The sidecar adds the provider's title to the provider failure message.
Only a rate-limit or quota title becomes `rate_limit` or
`quota_exhausted`. Context, turn and configured budget limits stay
`unknown`. The failure details stay private.
- Server: A native `turn.failed` with a `rate_limit` or
`quota_exhausted` provider failure is now `native_provider_usage_limit`,
the same code as the Codex usage-limit terminal.
- Server: `agentProviderLimitCooldown` (extracted from the startup
retry) also reads a reset clock in the provider's wording, such as
"resets 2am (UTC)". The clock parser moved from the recovery service to
`provider-quota-reset.ts` without changes.
- Server: New `heartbeat/provider-limit-wait.ts`. Before any workspace
or provider work, a queued run for an agent with an active provider
limit is cancelled as `provider_limit_wait`. The run records the reset
time, whether the provider reported it, the category and the run that
reported the limit. Paperclip schedules one retry of the same wake at
the reset through the existing AI connection wait lane. That lane does
not consume the failure retry budget. Manual runs, recovering runs,
conversation turns and pool-routed connections are not held.
- Server: The execution projection shows "Waiting for usage limit reset"
with the reset time and the next step.
- Server: Only a found project authentication conflict is a
configuration failure. Its message names the project file and the
settings to remove. A check that cannot finish (timeout or another exit
code) is now an ordinary failure, not a configuration conflict.
- Docs: `doc/execution-semantics.md` describes the agent-level limit
wait.

## Verification

Red before the fix, green after:
- `server/src/__tests__/heartbeat-provider-limit-wait.test.ts`: An
automation wake after a 429 `session.open` rejection dispatched the
adapter before the fix. Now the run is cancelled as
`provider_limit_wait` and a scheduled retry exists at the reset. After
the reset passes, the retry is promoted and the work succeeds. A manual
run, a later success, expired evidence and an unclassified terminal
still dispatch.
- `native-startup-retry.integration.test.ts`: "reads the reset clock
from an ACP terminal limit failure's provider wording" (it fell back to
one hour before).
- `native-session-executor.test.ts`: An ACP `turn.failed` with a typed
`quota_exhausted` or `rate_limit` failure is a usage-limit terminal;
`unknown` is not.
- `packages/paperclip-runner/src/drivers/provider-failure.test.ts`: A
typed AIR limit title is kept and classified; context, turn and budget
limits are not; details do not leak.
- `execution-projection.test.ts`, `ai-connections.test.ts`: The wait
label, and conflict versus unfinished check.
- Safety cases: `runtime-host.test.ts` and
`codex-runtime-adapter.test.ts` forward the callback. The existing
startup retry, provider capacity, recovery classification, legacy
reconciliation, retry scheduling and execution configuration suites
pass.

Commands:
- `cd server && npx vitest run
src/__tests__/heartbeat-provider-limit-wait.test.ts
src/__tests__/ai-connections.test.ts src/services/native-runtime/
src/services/recovery/ src/services/execution-projection.test.ts
src/services/legacy-execution-recovery.test.ts
src/__tests__/heartbeat-retry-scheduling.test.ts
src/__tests__/heartbeat-execution-configuration.test.ts`
- `cd packages/paperclip-runner && npx vitest run
src/drivers/provider-failure.test.ts
src/drivers/acpx/runtime-host.test.ts
src/drivers/acpx/codex-runtime-adapter.test.ts
src/cli/acpx-runtime-sidecar.test.ts`
- `tsc --noEmit` for server and paperclip-runner; the ACPX sidecar and
provider-failure contract checks pass.

I did not run a live provider limit on staging. The live evidence is the
staging run records for the affected agent, which I read only.

## Risks

- The limit evidence is per agent, not per account. If the board changes
the agent's AI connection, automatic wakes can still wait until the old
reset (at most one hour when no reset was reported, and at most one
day). A manual run is not held, and one successful run clears the
evidence.
- A new wake that arrives during a limit is now cancelled and retried at
the reset, instead of failing. The task stays in its status, and the run
shows the reset time.
- The Runner change takes effect when the provider pack is rebuilt and
deployed. Until then, the server still holds new work using the existing
`rate_limit` evidence from `session.open` rejections.
- A turn terminal with a typed rate limit or quota is now
`native_provider_usage_limit` instead of
`native_provider_terminal_failed`. Both are terminal and board-owned.
The recovery text now asks the board to restore provider usage capacity.

> 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, browser reads of staging run
records) 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-05 08:57:55 -06:00
DottaandPaperclip 3a090b815b fix: retain native checkpoint ownership through warm close (#19225)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A native warm session must save its checkpoint under its exact
coordinator owner.
> - A failed turn can return an accepted result before that checkpoint
is saved.
> - The executor stopped renewal and released the owner before the
managed session closed.
> - The backup owner guard then refused the uncommitted, released owner.
A failed close could remain in the warm session map.
> - This pull request keeps the claim and its existing renewal through
checkpoint, close and instruction collection.
> - The benefit is a valid checkpoint owner and one fenced release after
the work joins.

## Linked Issues or Issue Description

Refs #19208, #19206 and #14695. Those fixes cover a destroyed backup
source, Stop before a provider session opens, and retained agent-file
ownership. They do not change this checkpoint ownership order.

**What happened?**

A failed warm turn accepted its result. Its coordinator reached
`workspace_finalizing`, with a result ID and no lease owner. Managed
close then called the durable checkpoint writer. The writer correctly
refused this state with `native_harness_storage_owner_missing`. The
failed close could remain for the next fresh session to retire.

**Expected behavior**

The current claim and renewal remain valid until the required
checkpoint, close and instruction collection join. Cleanup stops and
joins renewal, then releases only that exact claim. A changed owner is
not cleared. An earlier error keeps its identity.

**Steps to reproduce**

1. Run the new real-Postgres regression against the base commit.
2. Complete a structured failed warm turn through the normal
control-plane result producer.
3. Let the actual managed-session close call its production checkpoint
callback.
4. Observe the missing-owner refusal after coordinator release.

**Paperclip version or commit**

Base: `56517dda6c`. The native ownership order also exists on the
earlier failing revision.

**Deployment mode**

Native warm execution with durable harness storage and a managed remote
workspace.

## What Changed

- Join warm checkpoint, managed close and instruction collection before
coordinator release.
- Keep the existing lease renewal active through that work. Stop and
join it before release.
- Run the same fenced release after a checkpoint or collection error.
Preserve the initiating error unless restart detachment has won. Keep
the typed restart outcome and changed-owner fences. Attach only closed
cleanup-failure flags.
- Add real-Postgres ownership, delayed-renewal, error, retry and denial
cases. Route the managed-session regression through the existing
artifact-equipped native test lane. Update the continuity test for the
retained claim. Document the active renewal lifetime.

## Verification

- Red: the producing coordinator had an accepted result,
`phase=workspace_finalizing` and `leaseOwner=null` at managed close. The
production checkpoint callback refused it with
`native_harness_storage_owner_missing`.
- Green: all 9 real-Postgres regression cases pass. They cover a real
20-second renewal tick, a failed checkpoint, one force-fresh successor,
and wrong-boot and changed-owner denial. Actual restart detachment wins
even when checkpoint and joined renewal-stop both reject. The
replacement claim remains unchanged. No new provider turn or
cancellation receipt is created. A private canary does not enter the
closed diagnostic flags or an error cause.
- The executor, restart-recovery and heartbeat compatibility suites
pass: 1,259 tests, with 20 gated skips. The container-enabled continuity
case was not run locally. Its 21-second timer check remains. It now
requires the exact claim during `hasChanges()` and checks release after
the joined work.
- All 39 shard-routing and combined-coverage tests pass. The new
regression runs in the existing native lane with the required Runner
artifact.
- Final local head `0edcc37f35` passes `pnpm -r typecheck`, `pnpm
build`, `node scripts/check-maps.mjs` and the 9-case regression. The
earlier broader compatibility run used identical source bytes. A
separate earlier repo-wide local suite on `6cdd68e915` ended with exit
1. Its general-server stage had one GitHub enrollment acknowledgement
failure, 31,469 passing tests and 215 skipped tests. That
acknowledgement failure reproduced in one narrow run on the same old
tree. It is not an established flake or proven caused by this fix. Later
repository stages did not run. No duplicate broad suite or full local
pass is claimed.
- The first published head failed review for detach-error precedence and
the old continuity assertion. Its server shard also failed because the
new test lacked a Runner daemon in the general lane. Those failures
remain recorded. This candidate fixes their causes. Both exact-head
reviews are now 5/5. The hosted native-server lane ran all 9 new
ownership tests successfully. One unchanged tool-gateway rate-rule test
failed in the first current-head CI run. It passed previously and passed
locally, both narrowly and in its full 183-test file. The single
same-head failed-job rerun passed. All current-head hosted gates are
clean. The initial failure remains recorded; no test assertion was
weakened.
- The provider transport is an in-memory fixture. The managed wrapper,
production checkpoint callback, control-plane result and Postgres
ownership are real. No paid provider or live remote process is
exercised. This does not prove a deployed recovery or a remote stop
receipt.

## Risks

- A slow required checkpoint extends the lifetime of the existing active
renewal. Its cadence stays at 20 seconds for remote claims and 5 minutes
for local claims. It runs only while the admitted checkpoint, close and
collection remain active, then stops and joins on completion or
unwinding. The tests observe a real remote renewal tick. No idle timer,
new retry budget or new worker is added.
- The release still checks company, task, run, owner, attempt, boot,
PID, process start and expiry. Restart detachment retains its recovery
owner.
- The storage owner guard is unchanged. No migration, permission,
provider setting or deployment change is included.

## Model Used

OpenAI Codex, `gpt-6.1-sol`, with reasoning, source inspection, local
code execution and read-only staging metadata. Context window and
reasoning effort are host-managed.

## 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-05 05:23:19 -06:00
DottaandClaude Opus 5.5 beb73381ca fix(native): let a Stop before the provider session opens continue once (#19206)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A remote task restores its saved workspace into a sandbox before the
agent starts.
> - When that restore fails, the run records only
`remote_workspace_operation_failed: operation=restore exit=1`.
> - The run then fails as `setup_failed`. No retry starts, and a
`blocked` task gets no recovery action.
> - An operator cannot tell which step failed, and nobody owns the next
step.
> - This pull request makes the sandbox scripts name their failed step
in a closed grammar, and it gives a restore failure a bounded retry or a
named board recovery action.
> - The benefit is a visible cause and a clear owner. Saved work stays
protected.

## Linked Issues or Issue Description

Refs #19152 and #19187. Those changes name copy-back (save) failures.
Refs #18852 and #19195. Those changes added the bounded bootstrap retry
that this change reuses.

**What happened?**

A board reply woke an agent on a `blocked` parent task. The run failed
during setup 13 seconds later. The only run event was
`remote_workspace_operation_failed: operation=restore exit=1`. The run
had no workspace operation row. The task stayed `blocked`, and its
recovery actions showed `active: null`. The same opaque family also
appeared for workspace preparation (`exit=1` only).

**Expected behavior**

The run names which restore step failed, without command output or
paths. A failure before provider work uses a bounded retry when a retry
can help. Otherwise, and when the retry budget is spent, the board gets
a named recovery action. The saved version is never changed or
overwritten.

**Steps to reproduce**

1. Save a remote workspace, then start a new run on a different
allocation.
2. Put a folder at the published workspace path in the sandbox before
restore.
3. Observe that the base code records only `operation=restore exit=1`,
then `setup_failed`, with no recovery action for a `blocked` task.

**Paperclip version or commit**

Base: `87dfba6818`.

**Deployment mode**

Legacy runtime with sandbox (Daytona) remote workspace storage.

## What Changed

- The prepare, repository, and restore sandbox scripts track their
current stage. On failure they print `remote_workspace_<op>_failed:
stage=<stage>` plus closed fields: `reason=` (the script's own
snake_case refusal), `archive=` (the archive helper's fixed reason),
`code=` (errno), `disk=full`, `git=` (a fixed label chosen from Git
output), and the existing `exit`/`signal`/`program` fields.
- The server accepts that text only when it matches a strict grammar for
the same operation. Other output is dropped, as before.
- Provider refusals during the restore upload and restore command are
recorded by closed labels, like copy-back.
- A failed saved-workspace restore raises `RemoteWorkspaceRestoreError`
with the server step (`snapshot_lookup`, `snapshot_download`,
`sandbox_restore`). It is retryable only when the owner re-reads the
same writer, epoch, and saved version with no unsaved work, and the
sandbox did not name a refusal reason.
- Setup failure handling classifies a pre-dispatch legacy restore
failure as `remote_workspace_restore_failed`. A retryable failure uses
the existing bounded bootstrap retry. A non-retryable or exhausted
failure records a board-owned recovery action with the redacted cause,
when stranded-work recovery does not scan the task status (for example
`blocked`).
- Stranded-work recovery gets a named notice for this code. Generic
continuation recovery does not grant it a second budget, and legacy
reconciliation does not invent provider actions for it.
- `doc/execution-semantics.md` documents the contract.

## Verification

- Red: the new transport tests failed on the base code with
`remote_workspace_operation_failed: operation=restore exit=1` (and the
same for prepare and repository). The new completion tests failed
because the run became `setup_failed` with no retry and no recovery
action.
- Green: `npx vitest run src/services/remote-workspace-transport.test.ts
src/services/heartbeat/run-completion.test.ts
src/services/remote-workspaces.integration.test.ts
src/__tests__/heartbeat-retry-scheduling.test.ts
src/services/legacy-execution-recovery.test.ts src/services/recovery
src/services/remote-workspace-save-concurrency.test.ts
src/services/native-runtime/native-workspace-finalizer-failure.test.ts
src/__tests__/native-workspace-finalization-work.test.ts` passes (563
tests).
- The transport tests use real sandbox commands. They prove an occupied
folder reports `stage=inspect_root reason=occupied_root` and keeps its
files. They prove a wrong manifest reports `stage=extract
archive=manifest_mismatch`. They prove a missing branch reports
`stage=checkout_branch`, and a changed origin reports
`stage=verify_origin`. Forged output with a path is dropped.
- The real-PostgreSQL cold-restore test proves an occupied root gives a
non-retryable `sandbox_restore` failure and keeps the unknown files.
- The completion tests cover retried, exhausted, refused, post-dispatch,
and `in_progress` cases.
- `tsc --noEmit` for the server reports no errors in the changed files.

## Risks

- A retry keeps the same restore path. If the cause is deterministic but
unnamed, the run uses its two retries before the board action. Named
refusals skip the retries.
- The board action is recorded only for statuses that stranded-work
recovery does not scan, and only when no other action is active. This
prevents a second recovery identity for one failure.
- Script stderr grammar is new. Old sandboxes run the script that the
server sends, so no runner update is necessary.
- No migration, deployment, or credential 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 (`claude-opus-5-5`), Anthropic, through Claude Code with
tool use, code execution, and browser tools for read-only staging
evidence. Context window and reasoning mode are host-managed.

## 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-05 03:13:44 -06:00
DottaandClaude Opus 5.5 f5891564ec fix(recovery): continue from a retired sandbox's backup and hand spent export repairs to the board (#19208)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native tasks keep a warm sandbox and a verified per-turn backup of
the provider session.
> - Recovery holds stop a task when continuation is not safe. They must
have a next action and an owner.
> - In a staging soak, two kinds of held tasks could not move forward.
Users followed the product's own instruction, and nothing changed.
> - First, a reply to a task whose warm sandbox Paperclip had already
destroyed failed every time. The label then said "Send a message to
continue" again.
> - Second, an exhausted system workspace repair stayed active and
system-owned for hours. The board saw no owner, and a reply started
nothing.
> - This pull request lets a reply continue from the backup when the
source sandbox is provably gone, and hands a spent system repair to the
board with a named Retry.
> - The benefit is that productive work always has a next action that
can succeed, and nothing replays or runs twice.

## Linked Issues or Issue Description

**What happened?**
(a) A native warm task ran one successful turn. A later legacy follow-up
retired the warm sandbox lease. The next user reply got a new sandbox
(`created` lease). The executor found a verified backup but no reusable
lease, so it threw `runner_harness_state_mismatch:
backup_without_reusable_lease`. The run retried on the restart-recovery
path, failed `native_remote_recovery_lease_mismatch`, and ended as
`native_continuation_requires_reconciliation`. Workspace recovery then
said "Saved work recovered. Send a message to continue." Each new reply
repeated the same failure.
(b) A run failed `native_workspace_sync_out_retry_exhausted`. System
workspace recovery used its 6 bounded attempts and became `exhausted`.
The recovery action stayed `active` with `ownerType: system`. The UI
hides system-owned recovery actions, so the board saw no action. Nothing
ran for more than two hours.

**Expected behavior**
(a) When Paperclip destroyed the source sandbox and no provider work
followed the backup, the reply continues once from the backup. Ambiguous
states still fail closed.
(b) When the bounded budget is spent, the action names a board owner and
a Retry action that can progress it.

**Steps to reproduce**
(a) Finish a warm native turn. Retire its reusable lease through
obsolete-lease cleanup. Send a reply. The new run gets a `created` lease
and fails `backup_without_reusable_lease`.
(b) Let system workspace recovery reach its deadline or retry limit.
Read the recovery action. It is still `ownerType: system`.

Related merged work checked first: #19145, #19184, #19187, #19189,
#19190, #18943 (added the guard for migrated sessions). Open recovery
PRs (`gh pr list --search recovery --state open`) do not touch these
paths.

## What Changed

- `native-retired-backup-source.ts` (new):
`retiredSourceAuthorizesNativeBackupRestore` decides whether a created
lease may restore the backup. All must be true: the backup's source
lease is `expired` or `failed`; it has a provider termination receipt
with state `destroyed`; its `nativeHarnessBackup` stamp names this exact
manifest SHA-256, session and runner instance; exactly one host lease
maps to the provider allocation; and no provider durable event for the
session was recorded after the backup completed. The provider event type
set moved here and the executor imports it.
- `native-session-executor.ts`:
`shouldRestoreNativeHarnessBackupIntoSandbox` accepts
`retiredSourceContinuation`. A created lease with a configured reusable
lease evaluates it, beside the existing migrated-session path. The
restore uses the existing verified restore and stamps the new lease.
- `system-recovery.ts`: `updateSystemRecovery` moves an `exhausted`
action to `ownerType: board` and writes a next action and workspace
recovery message that name the attempt count, the last failure code, and
**Retry recovery**. Retry (`requestWorkspaceRecovery`) already starts a
new bounded generation and returns ownership to the system.
- `doc/execution-semantics.md`: documents both rules.

## Verification

- Red before the fix, green after:
- `npx vitest run
src/services/native-runtime/native-session-executor.test.ts -t "restores
a warm backup into a created lease only when"`. Before: the
destroyed-source case rejected with `runner_harness_state_mismatch:
backup_without_reusable_lease`. After: it restores the runner state and
provider home into the new sandbox and stamps the new lease.
- `npx vitest run src/services/workspace-recovery.test.ts -t "hands an
exhausted system export repair"`. Before: `exhausted` with `ownerType:
"system"`. After: `ownerType: "board"`, the blocker says Retry recovery
and never says "send a message", no provider run or lease resume
happens, and Retry starts one new system generation.
- Safety cases (fail closed): a stopped (not destroyed) source, a stamp
for another checkpoint, provider work after the backup, a still-reusable
`released` lease, a missing receipt, and two host leases for one
allocation. These run in the executor test and in
`native-retired-backup-source.integration.test.ts` against embedded
Postgres.
- Related suites pass: `workspace-recovery.test.ts`,
`system-recovery-state.test.ts`, `native-session-executor.test.ts`,
`task-retry.test.ts`, `explicit-native-continuation.test.ts`,
`workspace-recovery-source.test.ts`,
`native-workspace-export-retry.test.ts`,
`native-remote-retry-admission.test.ts`,
`native-workspace-warm-reuse.test.ts`,
`native-workspace-sync-retention.test.ts`,
`heartbeat-observed-workspace-preservation.test.ts`,
`execution-control-work.test.ts` (2,224 tests).
- `npx tsc --noEmit -p server/tsconfig.json` passes.
- Not done: a live run against staging. Staging access was read-only.
The staging evidence is the run event timeline described above.

## Risks

- (a) A backup restore after a lost sandbox could rewind a session if
newer provider state existed. The predicate requires a provider
`destroyed` receipt and no provider event after the checkpoint. A
stopped sandbox, a pending cleanup, or a still-reusable lease still
fails closed with the old error.
- (a) The event-time check can fail closed if a late event from the
backup turn is written after the checkpoint time. That keeps the old
behavior; it never authorizes a restore.
- (b) The action's next action text stays as the exhausted message while
a later Retry generation runs. The task blocker shows the live workspace
recovery message first, so users see "Recovering saved work…" during
that time.
- Not changed: when the predicate fails closed, the run still follows
the existing reconciliation path. That path can still end with the "Send
a message to continue" receipt. That residual is narrower now and needs
a separate change to the workspace recovery receipt.

> 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. Agentic coding with
tool use (shell, file edits, read-only browser inspection of staging).
Default context window.

## Checklist

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

> - Paperclip is the open source app people use to manage AI agents for
work
> - Operators stop a running agent run with `POST
/api/heartbeat-runs/:runId/cancel`
> - For a native run, Stop waits up to 30 seconds for the native session
to open before it can deliver the cancel
> - A remote native session can take longer than 30 seconds to open.
Then the wait fails with `native_cancellation_pending_recovery`, and the
route answers HTTP 500
> - The Stop intent was already saved. The startup owner fenced the late
session in memory only, so the run ended as a provider failure, not as a
cancellation
> - This pull request maps that outcome to a clear 409 and makes the
late session acknowledge the saved Stop intent
> - The benefit is a consistent cancel result and a run that ends as
`cancelled` when an operator stops it during a slow startup

## Linked Issues or Issue Description

**What happened?**

An operator stopped a native run about 40 seconds after it started. The
run was still opening its remote provider session (session startup took
about 105 seconds). The cancel request waited 30 seconds for the startup
handle, then returned HTTP 500 `{"error":"Internal server error"}`. The
run record showed `executionCancellation.state = "requested"` and
`nativeCancellation.dispatchState = "pending"`. When the session opened,
the executor fenced it and threw `native_finalization_missing: session
returned no semantic result`. The run ended `failed` with
`native_session_interrupted`, and the pending Stop intent was never
acknowledged.

**Expected behavior**

The cancel route returns a clear result and never an internal error. If
the Stop is saved but the session is still starting, the route returns
409 with code `native_stop_pending`. When the late session opens, it
acknowledges the saved Stop, and the run settles as an operator
cancellation.

**Steps to reproduce**

1. Start a native run whose provider session takes more than 30 seconds
to open.
2. Stop the run from the board while the session is still opening.
3. See HTTP 500 from the cancel route, and see the run end `failed`
instead of `cancelled`.

## What Changed

- `server/src/services/heartbeat/run-control.ts`: the cancel path
catches `native_cancellation_pending_recovery` from native dispatch. It
throws `409 native_stop_pending` instead. The run stays `running` and
keeps its saved Stop intent. No terminal transition or release happens.
- `server/src/services/native-runtime/native-session-executor.ts`: when
a late startup handle opens after a Stop caller timed out, the executor
reads the run. If a run-scope Stop intent is still `pending`, it calls
the durable `cancelNativeSession(..., { db, scope: "run" })` against the
published handle. This records the audited acknowledgement (`dispatched:
true`). The existing in-memory fence still runs. If the acknowledgement
fails, the executor logs a warning and keeps the previous behavior.
- New regression
`server/src/services/heartbeat/native-stop-pending.test.ts` (real
Postgres): startup-wait timeout and remote-ack timeout both give 409
`native_stop_pending`. The run stays `running` with its Stop intent.
Other native Stop errors stay unchanged.
- New regression in `native-session-executor.test.ts`: a Stop that times
out on startup gets its saved intent acknowledged when the late handle
opens. The provider session is cancelled once, and no turn is submitted.

## Verification

- Red before the fix: `native-stop-pending.test.ts` failed (raw
`NativeCancellationPendingRecoveryError` / `Error`, not `HttpError`).
The executor regression failed (no acknowledgement write).
- Green after the fix: `cd server && npx vitest run
src/services/heartbeat/native-stop-pending.test.ts`
- `cd server && npx vitest run
src/services/native-runtime/native-session-executor.test.ts -t "late
startup"`
- Related suites:
`src/services/native-runtime/native-session-executor.test.ts`,
`src/services/heartbeat/`,
`src/services/native-runtime/native-stop-accounting.integration.test.ts`,
`src/__tests__/agent-live-run-routes.test.ts`.
- Evidence: the incident run had cancellation recorded at 01:12:01Z and
the 500 at 01:12:32Z. This matches the 30-second
`waitForNativeSessionStartup` deadline. Its final error matched the
startup-stop branch in the executor.

## Risks

- Low to medium. The 409 only replaces an error that was already a
failure. The caller still sees "not stopped yet", and the run is not
marked cancelled.
- The late acknowledgement runs only when a run-scope intent is
`pending` and the handle is published. It uses the same durable cancel
path that the reattach branch already uses. It can commit the normal
Stop status decision for the issue. That is the same effect a timely
Stop has.
- If the acknowledgement fails, the code falls back to the previous
in-memory fence. A Stop never gives a second executor: no provider turn
is submitted after the handle opens.

## 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
- [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-04 23:30:45 -06:00
DottaandClaude Opus 5.5 f1516e8009 fix(native): stop a session.open model rejection instead of a doomed bootstrap retry (#19190)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native Runner runs start a provider session with the `session.open`
command before any prompt
> - A definite `session.open` rejection is treated as a pre-session
failure and gets a bounded same-run bootstrap retry
> - The runner also reports a closed classification. Two classifications
say that the provider cannot run the selected model:
`requested_model_unsupported` and `grok_model_mismatch`
> - Paperclip ignored that classification. It scheduled a retry that
must fail the same way. On a remote sandbox with a retained runner PID,
the due retry entered restart recovery. Restart recovery has no
checkpoint for a bootstrap retry, so it failed closed as
`native_restart_recovery_blocked` / `ambiguous_remote_recovery_evidence`
> - The task then stayed `blocked`. The recovery actions settled as
no-replay holds, so no active action told anyone to change the model,
and the parent task stalled too
> - This pull request classifies those two definite rejections as the
existing `native_provider_model_rejected` failure
> - The benefit is a terminal failure on the first attempt, an accurate
error, and an active board recovery action that names the next step:
choose a supported model

## Linked Issues or Issue Description

**What happened?**
On a hosted staging instance, a Grok agent run failed at `session.open`.
The runner log said `ACPX sidecar command session.open was rejected
(retryable=false, classification=grok_model_mismatch)`. Paperclip
recorded `recoveryMode: "bootstrap_retry"` and scheduled a same-run
retry 30 seconds later. The run had a remote sandbox and a recorded
sandbox PID, so the due retry went to restart recovery. Restart recovery
found no checkpoint and no provider events and failed closed with
`ambiguous_remote_recovery_evidence`. The run ended as
`native_restart_recovery_blocked`. The task stayed `blocked` with no
active recovery action for more than one hour. Its parent task stayed
blocked on it.

**What did you expect to happen?**
The selected model cannot start, so a retry cannot succeed. The run
should fail once with the model-rejection error. The task should show
the existing "choose a model" action, owned by the board, until someone
changes the model and retries.

**Steps to reproduce**
1. Start a native run whose `session.open` command fails with
`classification=grok_model_mismatch` (or `requested_model_unsupported`).
2. Read the `native_run_finalizations` row. Before this change it is
`retryable_failure` with `recoveryMode: "bootstrap_retry"` and a
`nextAttemptAt`.
3. On a remote sandbox run with a recorded PID, the due retry enters
restart recovery and fails as `native_restart_recovery_blocked`.

**Paperclip version or commit**
`master` at `f5f5184f23`.

Related: #18910 routes due remote retries with a retained PID to restart
recovery. #18908 continues verified failed native preparation. Open PRs
#19177, #19180, #19182 and #19184 change no-replay hold settlement and
workspace hold release. They do not change this classification. This PR
does not duplicate them.

## What Changed

- `nativeSessionFailureSourceCode`
(`server/src/services/native-runtime/native-session-executor.ts`)
returns `native_provider_model_rejected` for a definite `session.open`
failure or rejection whose runner classification is
`requested_model_unsupported` or `grok_model_mismatch`. Other definite
`session.open` rejections still return `provider_session_open_rejected`
and keep the bounded startup retry. An indeterminate `session.open`
outcome stays `native_session_interrupted`.
- `native_provider_model_rejected` is already a permanent failure. The
run stops on the first attempt with no `nextAttemptAt`, so it never
enters the restart-recovery retry path.
- When no provider session exists, the coordinator `nextAction` and the
board recovery action `nextAction` say: choose a supported model for
this agent, then retry the task.
- The existing product paths for `native_provider_model_rejected` apply:
the task presentation shows `choose_model`, and the recovery service
treats it as `configuration_incomplete`.
- `doc/execution-semantics.md` describes the rule under "Native provider
model capacity".

## Verification

Red before the fix (new test, unchanged production code):

```sh
cd server && npx vitest run src/services/native-runtime/native-startup-retry.integration.test.ts
# Tests 2 failed | 21 passed
# failureCode: native_session_interrupted, phase: retryable_failure, recoveryMode: bootstrap_retry,
# nextAction "Retry provider bootstrap on this same run ..." - the same record seen on staging
```

Green after the fix:

```sh
cd server && npx vitest run src/services/native-runtime/native-startup-retry.integration.test.ts
# Tests 23 passed

cd server && npx vitest run src/services/native-runtime/native-session-executor.test.ts \
  src/services/native-runtime/native-startup-retry.integration.test.ts src/services/task-interruption-presentation.test.ts
# Test Files 3 passed, Tests 1186 passed

pnpm --filter @paperclipai/server typecheck   # exit 0
```

The new integration cases use embedded PostgreSQL and the real executor
failure path. For both classifications they check:

- coordinator `terminal_failure`, `failureCode:
native_provider_model_rejected`, `nextAttemptAt: null`, attempt 1
- run `failed` with `errorCode: native_provider_model_rejected`
- task `blocked`, execution lock released
- an `active` board recovery action with cause
`native_provider_model_rejected`, return owner set to the agent, and the
choose-model next action
- `settleUnrecoverableExecutions` leaves the action `active`, so it does
not become a silent no-replay hold
- `claimNativeSessionResumptions` claims nothing, so there is no second
executor

The unit test also checks that `session_ensure_failed` keeps its bounded
retry and that an indeterminate model rejection stays unclassified.

A live Grok run with a mismatched model was not run. The failure record
in the test matches the staging record field by field.

## Risks

- Low risk. The change adds two closed runner classifications to an
existing permanent failure code. Other `session.open` rejections keep
their current retry.
- A model mismatch that a retry could fix would now need an explicit
retry. The runner already marks these rejections `retryable=false`.
- This PR does not change restart recovery. A remote bootstrap retry
with a retained sandbox PID for other `session.open` causes can still
fail closed as `ambiguous_remote_recovery_evidence`. That needs a
separate change with remote stop evidence.

> 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, browser reads of the
staging API). It did the investigation, code change, tests and PR text.

## 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-04 23:24:34 -06:00
DottaandClaude Opus 5.5 a994a648f1 fix(workspaces): name copy-back failures on the run and end failed save operations (#19187)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A native run in a Daytona sandbox saves its workspace after the
provider turn. This save is "copy-back".
> - On staging, about 20 runs kept a saved result, but copy-back failed
three times. The issues became blocked with
`native_workspace_sync_out_retry_exhausted`.
> - #19152 added a redacted cause for a sandbox command that exits with
an error. Three gaps stayed open: a provider that refuses the command
still gives a bare code, the run does not show the cause, and the failed
save operation stays "running".
> - The first failure also had the wrong name. The runner kept the
result but did not prove a durable suspension. The executor reports this
as `native_session_lease_lost`, and the workspace operation shows only
that text.
> - This pull request names each of these failures with closed, redacted
labels. It also ends a failed save operation as "failed".
> - The benefit is that operators can see why copy-back failed and which
save is still active. Retries and saved results do not change.

## Linked Issues or Issue Description

Refs #19152, #19145. These are the earlier copy-back diagnostics and
retry-scheduling repairs. This PR does not duplicate them.

**What happened?**
A native task ran in a Daytona sandbox and its result was accepted. The
run then failed with `native_workspace_sync_out_retry_exhausted`. The
run had no error text. The automatic copy-back retries each failed in
under one second, with only `workspace_sync_out_failed`. The final
`remote_workspace_snapshot` operation stayed "running" after the retries
stopped. The first `workspace_finalize` operation showed only
`native_session_lease_lost`. The run log showed that the runner shutdown
timed out before durable suspension. The lease was not lost.

**Steps to reproduce**
1. Run a native task in a Daytona sandbox until its result is accepted.
2. Make the sandbox provider refuse the copy-back command (for example,
a lease that is no longer active), so that no exit status comes back.
3. Let the automatic retries stop. Then read the run and its workspace
operations.

**Expected behavior**
The run, the coordinator, and the failed operations name a redacted
cause. The failed save operation is "failed" with a finish time.

**Actual behavior**
The run has no cause. The save operation stays "running". A provider
refusal keeps only the bare `workspace_sync_out_failed` code.

## What Changed

- `remote-workspace-transport.ts`: the provider can throw instead of
returning an exit status for the copy-back helper, the capture, or the
archive download. `remoteWorkspaceFailureCause` then returns
`remote_workspace_operation_failed: operation=<op> transport=<label>[
code=<ERRNO>]`. The label is from a closed list (for example
`sandbox_lease_inactive`, `permission_denied`, `sandbox_not_running`,
`transport_timeout`, `transport_closed`, `daytona_sandbox_not_found`,
`provider_error`). Provider text is never copied into the cause. The
thrown error does not change, so preparation errors,
`classifyNativeWorkspaceFailure`, and other callers keep their
contracts. Archive-safety and Git-metadata refusals get no label,
because they have their own diagnostics.
- `remote-workspaces.ts`: when the last active invocation of a save
fails, the save operation becomes "failed" with `exitCode`,
`finishedAt`, a redacted `stderrExcerpt`, and `metadata.lastFailure`. A
retry of the same boundary sets it to "running" again. A success sets it
to "succeeded". After the final save commits, a failed save is retired
for export cleanup in the same way as a running save.
- `remote-workspace-snapshot-retention.ts`: a failed save that is not
retired keeps its snapshot pinned, as it did while it was "running". Its
retry can still use an uploaded snapshot.
- `native-run-finalizer.ts` and `native-workspace-finalizer.ts`: the
redacted cause of each copy-back attempt goes to the coordinator
`failureDetail.failureCause` and the run `resultJson.failureCause`. When
retries stop, the run error adds `Last cause: <cause>`. The server
checks the cause against a fixed grammar again before it writes it. A
cause with a path or other text is dropped.
- `native-post-result-failure.ts` (new), `native-session-executor.ts`,
`runtime-execution.ts`: a failure after an accepted result gets the
label `runner_settlement_incomplete` when the runner did not prove a
durable suspension. The failed `workspace_finalize` operation records
this closed diagnostic as `failureDiagnostic`. The coordinator records
it as `native_post_result_failure: category=<label>`. The control code
`native_session_lease_lost` and its fencing do not change.

## Verification

- Red, before the fix (the new tests run against `master` source):
- 4 provider-refusal cases and the archive download failure: no redacted
cause was available, so the copy-back recorded only the bare code.
- Failed save, with only the transport fix applied: `- "status":
"failed"` / `+ "status": "running"`.
- Run cause after retries stop: `expected null to be
'remote_workspace_operation_failed: …'`.
- Green, after the fix:
`cd server && npx vitest run
src/services/remote-workspace-transport.test.ts
src/services/remote-workspace-save-concurrency.test.ts
src/__tests__/native-workspace-finalization-work.test.ts
src/services/native-runtime/native-post-result-failure.test.ts
src/services/remote-workspaces.integration.test.ts
src/services/remote-workspace-snapshot-retention.test.ts` passes.
- Safety cases:
- The failed save is retried on the same boundary operation, records
"succeeded", and keeps the saved file bytes.
- `daytona_sandbox_not_found` is still classified as
`workspace_sync_out_unrecoverable`.
  - An archive-safety refusal keeps its exact text.
  - A forged cause with a path never reaches the run or the coordinator.
- A run-level timeout cause (`remote_workspace_operation_timed_out:
capture (configured timeout 300000ms)`) reaches the run and the
coordinator.
- `heartbeat-native-remote-failure-retention.test.ts` passes: a
preparation failure keeps its exact run error.
- The existing integration tests for retired saves, poisoned
reservations, duplicate invocations, and snapshot retention pass. They
prove export cleanup and identity protection.
- `tsc --noEmit` for `server` reports no errors.
- Not done: a live Daytona copy-back. This PR adds diagnostics and
corrects the operation state. It does not change when a retry runs.

## Risks

- Thrown copy-back errors do not change. Only the recorded cause is new.
The `daytona_sandbox_not_found` and archive-safety classifications are
tested.
- A failed save operation is now "failed" before the final save commits.
Retirement, export cleanup, and snapshot pinning now also select these
failed rows. They select only rows with `lastFailure` and no retirement
receipt.
- Privacy: each new cause is a closed label or a server-built code. The
server checks it again with an anchored grammar before it writes the
run. No sandbox output, path, or message text is stored.
- The retry count, the retry delay, and the classification of each
failure do not change. No provider turn is added.

> 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

- Provider: Anthropic. Model: Claude Opus 5.5 (`claude-opus-5-5`), in
the Claude Code agent harness.
- Capabilities used: tool use, shell and code execution, repository
search, read-only browser reads of staging, 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-04 23:07:42 -06:00
Nicky Leach 8c062fa481 fix(native): deliver plugin VM Runner usage to Core (#19162) 2026-11-04 21:02:22 -08:00
Nicky Leach 737db73275 fix(native): measure resumed run usage from the prior run's total (#19161) 2026-11-04 18:55:14 -08:00
DottaandClaude Opus 5.5 11f2a5293d fix(native): name the proof that holds a recovered per-turn owner (#19061)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native Runner per-turn sessions in a remote sandbox keep one
conversation across task turns.
> - A follow-up must prove that the previous Runner owner is finished
before it starts a new Runner. #18846 added a retirement path for an
owner that finished without a saved Runner checkpoint.
> - On a test deployment, follow-ups still fail with
`runner_state_identity_mismatch:
prior_owner_terminal_state_indeterminate`. The retirement path has about
twenty separate fences, and each refusal gives the same error.
> - The read-only run data cannot tell which fence held these runs. A
code fix without that evidence would be a guess.
> - This pull request names the first failed retirement proof in the
error. Admission behavior does not change.
> - The benefit is that the next failure on a deployment identifies the
exact fence, so the follow-up fix can target it.

## Linked Issues or Issue Description

Refs #18846 (retirement of recovered per-turn owners). Refs #18992
(settlement from live state).

**What happened?**

On a test deployment, 16 runs in one day failed before provider work
with `runner_state_identity_mismatch:
prior_owner_terminal_state_indeterminate`. All were per-turn
`acpx_runtime` runs in a Daytona sandbox, and each got a new sandbox. In
the runs that I inspected, the previous run committed its result. In one
of them, `harness_state.backup.persist` and `session.checkpoint.persist`
ran for about 346 seconds and failed, so the Runner left no suspension
receipt or failover backup. That is the case that #18846 addresses, but
the follow-ups still failed. The run log does not show the retirement
message, and the error is the same for each fence. Thus the cause cannot
be found from the run data.

**Expected behavior**

When admission holds a follow-up, the error names the proof that failed.

**Steps to reproduce**

1. Seed a per-turn sandbox owner that committed its result without a
Runner suspension receipt.
2. Make one retirement proof fail. For example, leave the owner phase
`observed`, or leave a `runner.suspend` command pending.
3. Admit a follow-up in a new sandbox.
4. Before this change, every case fails with the same error text.

**Paperclip version or commit**

Observed on a deployment before `e107a75755`. The same error text is on
`master`.

## What Changed

- `retireFinishedPerTurnRunnerdOwner` returns the refusal reason instead
of `false`. `readSettledRunnerdProjection` returns the journal refusal
reason instead of `null`.
- When admission tries the retirement and it fails, the error is
`prior_owner_terminal_state_indeterminate; retirement_refused=<reason>`.
A warm session gets no suffix, because retirement does not apply to it.
- Reasons use only fixed lists of known names: owner phase, recovery
state, controller fence reason, unsettled command type and status,
sandbox outcome, and quarantine copy result. Any other stored value
appears as `other`. Reasons do not include payloads, file names, or
paths.
- The same proofs apply in the same order. Admission accepts and refuses
the same states as before.
- `doc/execution-semantics.md` describes the suffix.

## Verification

- **Failure before the change:** I ran the updated
`native-recovered-owner-followup.test.ts` against the unchanged
executor. 18 tests failed. Example: expected `...indeterminate;
retirement_refused=owner_observed`, received `...indeterminate`.
- **After the change:** `cd server && pnpm exec vitest run
src/services/native-runtime/native-recovered-owner-followup.test.ts`: 22
passed. Each held case now asserts its reason:
`owner_retryable_failure`, `controller_remote_controller_lease_live`,
`owner_recovery_resuming_session`,
`journal_commands_unsettled(runner.suspend.pending)`,
`journal_tool_input_unanswered`, `quarantine_journal_identity_mismatch`,
`runner_lifecycle_not_suspended`, and
`quarantine_runner_lifecycle_not_suspended`.
- The success cases still retire the owner and admit the follow-up.
- `pnpm exec vitest run
src/services/native-runtime/native-session-executor.test.ts
src/services/native-runtime/native-runner-restart-recovery.integration.test.ts
src/services/native-runtime/remote-warm-session-recovery.test.ts`: 1227
passed, 20 skipped.
- `pnpm exec vitest run
src/services/native-runtime/native-controller-recovery.integration.test.ts
src/__tests__/task-workspace-binding.test.ts`: passed.
- `tsc --noEmit` reports no errors in the changed lines. This worktree
did not build every workspace package, so the full-package typecheck
shows unrelated missing-module errors. CI runs the full typecheck.
- **Not verified live:** no deployment has run this change.

## Risks

- Low risk. Admission logic is unchanged. Only the error text gets a
suffix.
- Consumers that match the error by prefix still match.
`failed-run-retry.ts` uses `startsWith("runner_state_identity_mismatch:
")`. No code compares the full string outside tests.
- This change does not fix the held runs. It finds the fence. A second
change is needed after a deployment reports the reason.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with tool use,
shell execution, and read-only browser access to a test deployment. The
harness does not expose the 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
- [ ] 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-04 18:50:01 -06:00
DottaandClaude Opus 5.5 3a72efebe5 fix(native): confirm no runner process when remote staging fails before launch (#19059)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native sandbox runs supervise a remote `paperclip-runnerd` process.
When that process exits during a turn, the transport restarts it inside
a reconnect grace window.
> - The server launcher stages the runtime assets first. Then it sends
one launch RPC that starts runnerd.
> - When staging failed, the launcher reported the exit as unconfirmed.
This happened although no launch RPC was sent and no process could
exist.
> - Reconnect recovery treats an unconfirmed exit as a possible second
owner. So it stopped at once with
`runner_process_termination_unconfirmed` and did not retry.
> - This pull request confirms the exit for failures before the launch
RPC. Failures after dispatch stay unconfirmed.
> - The benefit is that a transient staging failure, such as a full
sandbox disk, uses the normal bounded retry. A real failure ends with
`transport_reconnect_grace_exceeded` and keeps the staging error as the
detail.

## Linked Issues or Issue Description

**What happened?**
On a hosted staging deployment, 7 native ACPX runs failed in 24 hours
with `Provider notification transport failed closed.`
(`native_provider_terminal_failed`). In every run, the runner process
exited during the turn. The replacement launch then failed while it
uploaded the runtime context into the sandbox:
`runner_process_termination_unconfirmed: replacement runner health could
not be verified: ... .paperclip-upload-<uuid>.tar.gz: create: ... no
space left on device`.
The sandbox disk was full. No replacement process was started. But the
launcher said that it could not confirm the exit. So recovery did not
retry inside its grace window.

**Expected behavior**
A staging failure before the launch RPC proves that no replacement
process exists. Recovery must retry within the configured reconnect
grace. If the failure continues, the run must end with the
reconnect-grace failure and the staging error.

**Steps to reproduce**
1. Create a remote runner launcher whose `ensureArtifact` step throws,
for example with an ENOSPC upload error.
2. Wait for the handle completion to reject.
3. Before this change, `isExitConfirmed()` returns `false`, and no
launch RPC was sent. The transport then fails closed with
`runner_process_termination_unconfirmed`.

**Paperclip version or commit**
Observed on staging revision `89703c3`. Reproduced on `master` at
`be20d4ad97`.

Related: #19035 and #19014 change other runner paths (replayed terminal
turns and redacted external tool results). They do not change remote
launcher exit evidence. #18999 adds a free-space reserve for host-volume
seed copies. It does not change sandbox disk use or this classification.

## What Changed

- `createRemoteRunnerProcessLauncher` records when it dispatches the
launch RPC. If the completion fails before that point, the handle
reports `isExitConfirmed() === true`. The original error is kept
unchanged.
- A failed or timed-out launch RPC still reports an unconfirmed exit,
because the RPC may have started runnerd.
- Added a launcher regression for a staging failure before dispatch.
- Added safety cases for a failed and a timed-out launch RPC.
- Added a transport regression with the real `paperclip-runnerd`. A
replacement whose staging fails is retried, and the next launch restores
the durable session without a notification failure.

## Verification

- Red before the fix: `cd server && npx vitest run
src/services/native-runtime/native-session-executor.test.ts -t "remote
runner process supervision"` failed `confirms no replacement process
exists when staging fails before the launch RPC` with `expected false to
be true`. The two launch-RPC safety cases passed.
- Green after the fix: `npx vitest run
src/services/native-runtime/native-session-executor.test.ts` passes 1131
of 1131 tests.
- `npx vitest run src/services/native-runtime/setup-readiness.test.ts`
passes 98 of 98 tests.
- `cd packages/paperclip-runner && npx vitest run
src/live/runnerd-codex-transport.test.ts` passes 246 of 246 tests with
the real debug `paperclip-runnerd`. This includes the existing case
where a replacement with a dispatched but unverifiable process still
fails closed.
- `npx tsc --noEmit -p server` and `pnpm --filter
@paperclipai/paperclip-runner typecheck` pass.
- Not run: a live hosted sandbox with a full disk. The staging data
showed the exact error, but this change was not deployed there.

## Risks

- Low risk. The change affects only failures that occur before the
launch RPC. In that window, the launcher has not run any sandbox command
that can start runnerd.
- Retries now run for the reconnect grace window, which is bounded and
uses the existing backoff. Each failed staging attempt cleans its own
upload scratch.
- This change does not free sandbox disk space. If the disk stays full,
the run still fails, now with `transport_reconnect_grace_exceeded` and
the ENOSPC detail.

## Model Used

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

## Checklist

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

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A native run claims its finalization coordinator before task
preparation. Then it starts the provider turn.
> - Task preparation can fail after the claim and before any provider
turn.
> - In that case the executor kept its claim. Failure settlement read
the claim as a live owner and stopped. Nothing failed the run.
> - The 60-second execution-control watchdog then failed the run as
`execution_finalization_deadline_exceeded`. The original error was lost.
> - This pull request releases only that exact fresh, unstarted claim
after renewal stops.
> - The benefit is that ordinary settlement fails the run at once with
its real error and keeps the workspace files. The watchdog stays a
backstop.

## Linked Issues or Issue Description

Refs #19028. That PR keeps the earlier failure code when the watchdog
fails a run. In this case no earlier code is saved, so it cannot help.
This PR removes the wait for the watchdog. I searched open PRs for
duplicates. #18820, #19034 and #19038 touch near code, but none release
a preparation claim.

**What happened?**

A native task with a prepared sandbox workspace failed during
preparation, after the controller claim committed. The coordinator
stayed `observed` and kept this controller's lease owner. The heartbeat
failure path calls workspace preservation. Preservation refuses any
lease owner without a matching restart claim, so it raised
`NativeWorkspaceFinalizationBusyError`. The heartbeat then treated
ownership as held and returned without a terminal write. About 60
seconds later the watchdog failed the run with a generic "verify the
provider stopped" action. The real error (for example
`native_harness_storage_owner_missing`) stayed only in the workspace
operation record.

**Expected behavior**

When no provider turn started under a fresh claim, settle the failure
like a failure before the claim: retain the source, fail the run with
its own error, and do not wait for the watchdog.

**Steps to reproduce**

1. Start a native run with a prepared remote workspace.
2. Make task preparation fail after `native.coordinator.claim` commits
(for example, a preparation callback throws).
3. See that the run stays `running` until the control deadline fails it
as `execution_finalization_deadline_exceeded`.

## What Changed

- `native-execution-lease.ts`: add
`releaseUnstartedNativeExecutionClaim`. It clears the lease owner and
expiry only for the exact run, company, issue, owner and attempt, while
the phase is `observed` and no result exists.
- `native-session-executor.ts`: when task preparation fails after a
fresh claim, mark that claim. The startup owner releases it after
renewal stops. A restart-recovery claim, a startup-cleanup context,
Stop, restart detach, or a different owner keeps the old behavior.
- Tests: an executor regression for the fresh and restart cases, and a
real-database test for release and workspace retention against the exact
owner, a foreign owner, a different attempt and an accepted result.
- `doc/execution-semantics.md`: state that the deadline is a backstop
and describe the release.

## Verification

- Red before the fix: `cd server && npx vitest run
src/services/native-runtime/native-session-executor.test.ts -t "releases
only a fresh claim"` fails with "releaseUnstartedNativeExecutionClaim
... Number of calls: 0".
- Green after the fix, with the same command.
- `npx vitest run
src/services/native-runtime/native-session-executor.test.ts
src/services/native-runtime/native-failed-workspace-recovery.test.ts
src/services/native-runtime/native-execution-lease.test.ts
src/services/execution-control-reconciliation.test.ts` passes (embedded
Postgres for the retention test).
- `pnpm --filter @paperclipai/server typecheck` passes.
- Not done: a live run against a real sandbox at this revision. The
staging evidence is read-only.

## Risks

- The release is fenced by the exact owner, attempt, `observed` phase
and no result. Renewal stops before the release, so a renewal cannot see
a missing owner and detach or cancel the run.
- The fix covers failures inside task preparation only. Failures later
in startup, or after a restart-recovery claim expires, still reach the
watchdog. Those keep fail-closed behavior and need separate evidence.
- If the release write fails, the run keeps the old watchdog path.

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

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, 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
- [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-04 18:25:36 -06:00
DottaandPaperclip 818d2bb661 fix(native): preserve closed post-result failure diagnostics (#19126)
## Thinking Path

> - Paperclip manages AI agents and their work.
> - Native execution persists a result before it completes cleanup and
workspace export.
> - A later execution error enters the failure-write path.
> - That write must reject an already selected result. The existing code
reports lease loss for every rejected write.
> - This can hide the category of the first error even when the same
controller still owns an unexpired lease.
> - This change adds closed diagnostic labels after exact ownership and
accepted-result checks. It keeps the existing control error and recovery
policy.

## Linked Issues or Issue Description

**What happened?**

A local real-PostgreSQL regression accepts a result through the
production control plane, then injects a harmless filesystem failure.
The production executor reports `native_session_lease_lost`. It loses
the original failure category because the failure transition requires a
null result ID.

**Expected behavior**

Keep the result guard and the existing lifecycle classification. Record
a closed diagnostic category when the selected result is accepted and
the controller still has exact, unexpired authority. Keep ordinary
lease-loss fencing when that proof is absent or its diagnostic lock is
busy.

**Steps to reproduce**

Run the new post-result cases in
`server/src/services/heartbeat/runtime-execution.test.ts`. They use the
real executor, control-plane result commit, heartbeat recovery path and
PostgreSQL. The provider boundary is controlled. No live provider is
used.

**Paperclip version or commit**

The regression fails on base `7d1e9b3477`. Related open PR #16917
changes legacy lease recovery. Open #19063 adds terminal finalizer
reasons, and #15310 preserves database finalization errors. They do not
change this executor diagnostic branch. This change only adds native
diagnostic evidence and does not reclaim a lease.

## What Changed

- Add a typed diagnostic with a closed stage, category and
accepted-result flag. Do not retain the source message, path, arbitrary
code or lease token.
- After a rejected failure write, check the selected accepted result and
the exact company, task, run, owner, attempt, generation, process
identity and expiry. Use a coordinator-only `NOWAIT` lock. Fall back to
the original control error if the diagnostic read fails.
- Report the closed event after rollback. Keep
`native_session_lease_lost` as the error message and leave every
failure-write predicate unchanged.
- Add real-PostgreSQL safety cases for stale and expired authority,
owner replacement, Stop state, result selection, pending accounting, one
provider invocation, closed labels and diagnostic sink failure.
- Document the closed event, unchanged authority, and possible late
diagnostic sink completion.
- Add a competing-row-lock barrier. It verifies actual PostgreSQL lock
refusal and control settlement before the competing lock is released.

## Verification

- Permanent regression on unchanged base: RED because the closed
diagnostic is absent.
- NOWAIT mutation: RED when only NOWAIT is removed. PostgreSQL reports
lock timeout instead of immediate lock refusal. The test uses barriers
and the actual database error, with no elapsed-time assertion.
- Final related suites: 56 tests passed across runtime execution and
provider failure integration tests.
- Full server source and test typecheck: passed.
- Full `pnpm -r typecheck`: passed in an independent locked-dependency
checkout.
- Full `pnpm build`: passed.
- Full `pnpm test:run`: running in the independent immutable checkout.
All six chat shards passed; the remaining suites are still pending.
- The first current-head CI attempt failed in two cases: workspace
runtime initial fixture HTTP readiness and instructions-bundle
error-message rendering. Both exact cases passed in isolation on
unchanged PR head and base. The original causes remain unknown after
bounded test-only observation. One controlled same-head rerun of each
failed job passed. Current required CI and all other completed checks
passed. Original failures remain recorded; no infrastructure or
flaky-test claim is made.
- Module boundary check and `git diff --check`: passed.
- Exact-head Greptile: 5/5 with no actionable findings or open review
threads. Automatic native review remains queued. That advisory check is
not required by current master rules; no current-head native assessment
is claimed.
- Earlier fixture setup failures remain recorded. These included missing
fixture fields and observing the Drizzle wrapper instead of its
PostgreSQL cause. They are not the regression RED. An earlier broad
typecheck was interrupted for the required NOWAIT revision; the final
full typecheck passed.
- No hosted export recovery or original hosted failure cause is claimed.

## Risks

The extra read is diagnostic only. It uses NOWAIT and cannot wait for a
competing coordinator lock. Its failure preserves the existing control
classification. The event sink uses the existing 60-second
execution-control bound. A slow sink can finish later and emit a late
event. Result selection, Stop, accounting, retry eligibility and
provider work do not depend on event success. No schema or lifecycle
contract changes are required.

## Model Used

OpenAI GPT-6.1, model ID `gpt-6.1-sol`, with reasoning, tool use and
code execution. The host did not expose the 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
- [ ] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 17:45:53 -06:00
DottaandClaude Opus 5.5 86afc2048b fix(native): admit follow-ups after an owner finished in this process (#19071)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native Runner per-turn sessions in a remote sandbox keep one
conversation across task turns.
> - Before a follow-up starts a new Runner, it must prove that the
previous owner's controller stopped. #18846 added a retirement path for
this proof.
> - The native controller runs inside the webserver process. Thus the
owner's recorded controller PID is the PID of the webserver.
> - When the same server process serves the follow-up, that PID is alive
and has the same start time. The proof reads this as "controller still
alive" and refuses. The follow-up then fails with
`runner_state_identity_mismatch:
prior_owner_terminal_state_indeterminate`. This continues until a deploy
replaces the process.
> - This pull request uses the controller's per-process boot identity to
recognize that this process recorded the owner. The process then uses
its own session registry to prove that the owner's controller stopped.
> - The benefit is that follow-ups on a long-lived server no longer fail
before provider work. All other ownership fences stay as they are.

## Linked Issues or Issue Description

Refs #18846 (retirement of recovered per-turn owners). Refs #19061
(names the refused retirement proof). The two changes are compatible.
Whichever merges second needs a small rebase in
`retireFinishedPerTurnRunnerdOwner`.

**What happened?**

On a test deployment, 17 runs over 14 tasks failed before provider work
with `runner_state_identity_mismatch:
prior_owner_terminal_state_indeterminate`. Each run had a per-turn
lifecycle in a Daytona sandbox and got a new sandbox. In each inspected
case, the previous run on the task succeeded and committed its result.
Its run profile had no `nativeControllerRecovery` pointer. Without that
pointer, `evaluateNativeControllerTakeover` is not lease-fenced, so it
uses process liveness. The recorded `controllerPid` is `process.pid` of
the webserver. When the follow-up is served in the same process,
`process.kill(pid, 0)` succeeds and the start time matches. The result
is `controller_still_alive`, and retirement refuses.

**Expected behavior**

A committed owner that this server process finished, released, and no
longer runs is proven stopped. The follow-up then starts a new Runner,
as #18846 intended.

**Steps to reproduce**

1. Seed a per-turn sandbox owner that committed its result and released
its lease. Record this process's controller identity
(`currentNativeControllerIdentity()`) as the owner.
2. Keep the owner's file-backed journal in the local session root, and
remove the database recovery pointer.
3. Admit a follow-up in a new sandbox.
4. Before this change, admission fails with
`prior_owner_terminal_state_indeterminate`.

**Paperclip version or commit**

Observed on `e107a75755`. The same code is on `master`.

## What Changed

- `native-restart-recovery.ts`: add
`isCurrentNativeControllerProcess(owner)`. It is true only when the
owner's `controllerBootId` is this process's per-start boot UUID and
`controllerPid === process.pid`. A recycled PID or another container
never matches.
- `retireFinishedPerTurnRunnerdOwner`: when the owner was finished in
this process and no `activeNativeSessions` or `nativeSessionStartups`
entry exists for its run, treat its controller as stopped. Otherwise,
use `evaluateNativeControllerTakeover` as before. The same in-process
check runs again just before the archive.
- All other retirement proofs are unchanged: committed result, released
lease, no recovery state, settled journal, answered tool inputs,
quarantine copies, and new or replacement sandbox.
- `native-recovered-owner-followup.test.ts`: the seed helper accepts the
owner's controller identity. New cases cover same-process admission
(with and without a recovery pointer), an exited legacy controller,
another live controller, and the same PID with a different boot
identity.
- `doc/execution-semantics.md`: describe the same-process
stopped-controller proof.

## Verification

- **Failure before the change:** with only the new tests applied, `cd
server && npx vitest run
src/services/native-runtime/native-recovered-owner-followup.test.ts`
fails 1 case: "admits a follow-up in the process that finished the
owner: local journal only". It expected admission and received
`runner_state_identity_mismatch:
prior_owner_terminal_state_indeterminate`, the exact error from the
deployment. The safety cases already passed on the old code.
- **After the change:** the same file passes 27 of 27.
- Adjacent suites: `native-recovered-owner-followup`,
`remote-warm-session-recovery`,
`native-runner-restart-recovery.integration`, `native-restart-recovery`,
`native-session-resume`: 210 passed, 20 skipped.
- `native-session-executor.test.ts`: 1128 passed.
- `tsc --noEmit -p server/tsconfig.json`: no errors.
- **Not verified live:** this change has not run on a deployment. Run
data came from read-only API calls only. No recovery was done on the
deployment.

## Risks

- **Second executor.** The new proof applies only when this exact
process recorded the owner, and this process has no session or startup
for that run. The database proofs still require a committed result and a
released lease. Another live controller, or the same PID with a
different boot identity, still holds the owner. Tests cover both cases.
- **Scope.** The change applies only to the per-turn retirement path.
The warm remote recovery path (`inspectRemoteWarmSessionRecovery`) is
not changed. Warm handoff in the same process uses the in-memory idle
owner instead.
- **Hypothesis limit.** The deployment API does not expose finalization
rows. Thus I inferred that the same process served each follow-up from
the evidence: no recovery pointer, a local journal present, and no other
failing proof. The local reproduction gives the exact error. #19061 can
confirm the refused fence on the deployment.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic. Agentic coding in
Claude Code with tool use (shell, browser for read-only staging API
reads, code execution).

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 16:29:25 -06:00
Nicky LeachandClaude Opus 5.5 e107a75755 fix(native): decide session settlement from live state (#18992)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native Runner sessions keep one provider conversation across task
turns, so a later run attaches to a session that an earlier run settled.
> - Three gates decide if a session is settled: Core's journal check,
the Runner's attach check, and the Runner's drain of provider messages
after a turn.
> - Each gate kept a list of harmless events and refused anything else.
Every new harmless event became an outage.
> - An example: Codex replays a settled turn's usage on `thread/resume`,
so `run.attach` refused every session that restarted after its idle
timeout.
> - Events after a terminal describe the past. Refusing the session
cannot undo them, and it hides them.
> - This pull request asks a different question: is anything live or
waiting right now? Past events are recorded and no longer block.
> - The benefit is that harmless events stop breaking session
continuity, with no event list for Core and the Runner to keep in step.

## Linked Issues or Issue Description

No issue exists. I searched open and closed pull requests and issues for
`run.attach requires a settled Codex provider session` and
`prior_owner_terminal_state_indeterminate`. Related work: #18890 added
one more accepted event to Core's list. This change replaces that list.
#18991 handles late usage and is independent of this change.

**What happened?**

On a test deployment, 11 runs failed in 21 days when `run.attach`
refused with `requires a settled Codex provider session with no pending
events`. In all 11, the four pending events were two startup audit
events, `session.resumed`, and a `codex_resume_usage_snapshot`
diagnostic. The diagnostic is what the attach check refused. The retry
then failed with `prior_owner_terminal_state_indeterminate` in 6 of
them, which hid the first failure.

**Expected behavior**

A session with nothing live or waiting accepts the next run. If
attachment fails, the run reports the real cause.

**Steps to reproduce**

1. Finish a Codex turn on a native Runner session.
2. Wait for the idle timeout so that the provider process stops.
3. Start the next run. Codex replays the usage for the settled turn on
`thread/resume`.
4. The attach fails.
`durable_backend_attaches_after_a_settled_resume_usage_snapshot`
reproduces this on the unfixed code.

**Paperclip version or commit**

Observed on `8fd917aedf`. The same check is on `master` before this
change.

**Deployment mode**

Cloud deployment, with a native Runner in a plugin sandbox environment.

## What Changed

- **Runner attach:** `attach_run` refuses only for live or waiting
state: no thread, a closed session, a session that needs reconciliation,
an active or ambiguous turn, an active goal, a pending tool call, or an
unanswered provider request. The error now names the reason.
- **Pending events:** they no longer block attachment. They stay in the
retained queue, and the Runner commits them under the prior authority
before it rotates. `session.resumed` is still discarded because it is
superseded.
- **Runner drain:** `drain_completed_turn_tail_for_warm_attachment` no
longer needs a list of known messages. It still fails on a message that
names another turn, on `turn/started`, on a provider request, and on a
process exit.
- **Core:** `isSettledRemoteWarmControlState` accepts any authenticated
event after the terminal unless it starts work (`turn.started`,
`turn.accepted`, `run.attached`, `tool.execution.started`,
`semantic_tool.input`, `runtime_request.created`), or a goal snapshot or
update shows an active goal or work in progress. All identity, source
and protocol checks are unchanged.
- **Visibility:** the next run's log names the event types that were
recorded after the previous run finished.
- **Real cause:** a same-run retry after a failed attachment now fails
with `prior_attach_failed: <original failure>`, instead of
`prior_owner_terminal_state_indeterminate`.
- **Docs:** added "Session settlement after a run" to
`doc/execution-semantics.md`.

## Verification

- `cd packages/paperclip-runner/runner && cargo test --locked -p
paperclip-runner-core --test codex_provider`: 102 passed, 2 ignored.
`--lib`: 478 passed, 2 ignored. `--test durable_recovery`: 3 passed.
- **Failure before the fix:**
`durable_backend_attaches_after_a_settled_resume_usage_snapshot` failed
on the unfixed code with the same error as production.
- `cd server && pnpm exec vitest run
src/services/native-runtime/remote-warm-session-recovery.test.ts
src/services/native-runtime/native-session-executor.test.ts -t "settled
remote warm control evidence|recovers a blocked|automatically recovers
only a proven settled local session|retained|warm"`: 476 passed, 680
skipped by the filter.
- `pnpm exec vitest run
src/services/native-runtime/native-session-resume.test.ts`: 57 passed.
- `npx tsc --noEmit -p .` in `server` reports no errors.
- **Real data, read-only:** I decrypted the 11 failed runs' stored
events on a test deployment and printed only event types, field names,
and thread and turn IDs. All 11 show the same four events.
- **Audit of live state:** I read the Runner's provider state and
request tracking to build the attach check. It is a code audit. I have
not proved that every kind of live work sets a flag. The Runner holds
active turns, ambiguous starts, tool calls and provider requests in
state today.
- **Not covered:** the wiring that throws `prior_attach_failed` has unit
coverage for its message only, not an end-to-end test.
- **Review:** Greptile Apex scored 4/5, then 5/5 at the current head.
- **Not run locally:** the full test suite. CI provides that coverage.
- **Not verified live:** no live run has used this change.

## Risks

- **Fail-open for unknown live work:** an event type that starts work,
but is not in the short list and sets no state flag, would now pass.
This is the main tradeoff. The previous list failed closed, but it
caused real outages.
- **More events reach the journal after a terminal.** The usage replay
diagnostic is now delivered rather than refused. These events are
informational, and they are named in the next run's log.
- **A failed attachment is not retried on the same run.** Some refusals
could clear on their own, such as a turn that finishes, and some attach
errors are not about live work, such as a failed provider restore. The
run now reports the cause, and it needs another run to continue. On a
test deployment, the next successful run after each failure used a new
provider thread and a new execution workspace. A later change could add
same-run retry.
- **Rollout order:** deploy Core before the Runner. An older Core would
refuse the new events that a new Runner commits after a terminal.
- **Migrations:** this change has no migration.

> 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 reworked.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), through Claude Code,
with tool use and code execution. A human directed the design and
approved the scope.

## 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-04 12:04:29 -08:00
DottaandPaperclip 3a2249dd57 fix(runner): retry proven ingress bootstrap failures (#18981)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native execution must retain its admitted model and execution
authority during recovery.
> - A sandbox provider can reject Runner endpoint acquisition before the
Runner gets a credential.
> - The old error path loses the structured HTTP status and leaves that
startup failure ambiguous.
> - A zero counter in an old checkpoint cannot prove that no credential
was issued.
> - This pull request adds a versioned producer proof and carries closed
provider status through the existing RPC.
> - A proved transient startup failure uses the existing retry budget. A
terminal proved-unused run can settle its accounting.

## Linked Issues or Issue Description

Refs #18018 and #18934. Those changes restore existing checkpoints and
ready retained environments. This change covers endpoint acquisition
before Runner dispatch. A search of related public PRs found no
duplicate implementation.

**What happened?**

A provider HTTP 502 during Runner endpoint acquisition becomes an
untyped worker error. Native recovery treats it as ambiguous and stops
without retrying. The run's native usage receipt also stays pending
after startup ends without model work.

**Expected behavior**

Retry a structured transient endpoint failure only when the complete
current controller checkpoint proves that this exact run never issued a
Runner credential. Preserve authentication, configuration, Stop,
ownership and provider-progress gates. Settle zero usage only under the
existing terminal accounting owner.

**Steps to reproduce**

1. Use the controlled endpoint in the Daytona ingress integration test
to return HTTP 502.
2. Register the actual native Runner driver and persist its bootstrap
journal through the encrypted checkpoint writer.
3. Observe the old terminal ambiguous disposition instead of bounded
startup recovery.

**Paperclip version or commit**

Reproduced on the pre-change master source. The original regression used
the production native driver, a real HTTP fault, a real durable control
plane and local Postgres. The new contract additionally tests the pinned
Daytona SDK and real worker RPC.

## What Changed

- The controller owns a v2 persisted state with retained origin and
credential history. Current readers still accept v1. Older writers
reject v2. Restore and checkpoint decoding do not add missing history.
- The proof validates the complete bootstrap journal, all six identity
fields and every delivery, credential and progress control. The server
rechecks its fingerprint under the current controller claim before retry
or accounting changes.
- Daytona and the SDK carry a closed acquisition phase and HTTP status
through the existing ingress RPC. Both public error wrappers omit
response bodies and causes, including Pino cause serialization.
- Only structured HTTP 429 or 5xx ingress failures can use the existing
three-attempt startup budget. HTTP 401/403, configuration and unknown
errors do not gain retry authority.
- A terminal proved-unused run wakes existing accounting and records
zero provider work. It does not create a semantic result, prove physical
termination or release an unknown process/workspace hold.
- The normal automatic second attempt rechecks its exact claim, frozen
execution and matching complete local/encrypted journal before admitting
absent remote Runner state. It keeps the original pending bootstrap
command IDs. A proved unused Codex home can rematerialize sealed skills.
Full raw local state is checked before streaming-event or policy
projection.
- Normal saved-state readers and the existing E2E journal evidence
matcher accept the closed v1/v2 set. The separate legacy no-launch
witness is unchanged. Technical docs describe the contracts.

## Verification

Current head: `cd38f70409cdb8664e14bfbf4efa208f468a0419` (final delta
adds only fixture artifact preparation). Production files are unchanged
from the independently reviewed `93c8a33520` source. Full repository
typecheck and build passed at `96417acb58`; the affected server
typecheck also passes after the final two-line fixture delta. Focused
protocol, raw-proof, real retry, SDK and RPC acceptance pass on this
source. Earlier broad suite counts are listed with their limits below:

- Durable Runner protocol: 187 tests.
- Native executor, driver, startup/accounting and redaction: 1,211
tests, including real Postgres and encrypted checkpoint persistence.
- Normal saved-state restore/projection compatibility: 21 focused cases.
- SDK: 209 tests.
- Standalone Daytona: 544 tests; 8 opt-in live tests skipped.
- Explicit standalone `pnpm -C
packages/plugins/sandbox-providers/daytona exec vitest run --config
vitest.sdk-ingress.config.ts`: 6 cases with real pinned SDK 0.203.0 and
worker RPC ingress acceptance. These include lookup 502/401/403/400,
private-preview 502 after successful lookup/refresh, missing credentials
and private-response exclusion.
- E2E journal evidence: 4 cases across both supported formats, with
wrong ownership, missing stimulus and unknown-schema controls.
- The exact historical controller source rejects a new v2 checkpoint at
decode and restore. A v1 control is accepted. This verifies the
downgrade fence with the actual older parser.
- Startup controls cover issued and pruned credentials, old/truncated
journals, provider progress, exhausted retry accounting, and
checkpoint/Stop/controller-claim races. Race cases retain the current
execution and pending usage.
- New exact admission controls: 19 cases through encrypted persistence
and cold restore. They cover current/stale/expired claims, Stop, pause,
model/trust/account changes, prior progress, issued/pruned credentials,
adopted/old/truncated state, local/canonical fingerprint mismatch,
hidden delivery, unsupported work and company scope. Two additional
red/green cases prove that raw local stream progress and missing event
policy are rejected before lossy checkpoint projection. No case mutates
run, claim, result or accounting state.
- Actual native executor, scheduler and Rust Runner retry: 2 cases.
Codex uses its fake app server. OpenCode uses the real proxy and fake
OpenCode HTTP server with the frozen DeepSeek model and allow permission
configuration. Each path launches no Runner on attempt 1, starts one
Runner and provider on attempt 2, reuses both original command IDs,
commits one accepted result, validates the persisted local workspace,
records nonzero synthetic native usage and settles accounting once. No
zero-work accounting flag survives.

The controlled SDK and protocol tests use local source, isolated
PostgreSQL, a private HTTPS preview and fake provider servers on macOS
arm64 with Node 24.22.2. They create no hosted sandbox and make no model
call. The root workspace intentionally omits standalone provider
dependencies; the explicit SDK target fails if its pinned dependency is
absent and has no conditional skip. They do not qualify hosted rollout
or repair an old unmarked checkpoint. The first full local typecheck
found missing checkout SDK links from an install with scripts disabled;
the repository link helper repaired that setup.

Current-head Linux CI typecheck and build pass. Independent final review
checked 12 source scopes and six raw-proof controls with no remaining
finding. Fresh Greptile review is 5/5 with no unresolved thread.

The full Runner CI suite found two older control-peer test doubles
missing the new proof method. Both now return no proof, with all 27
permission, ordering and cancellation assertions retained and passing.
The new Cargo-dependent native retry suite uses the existing Rust-cached
CI lane. All 39 lane coverage controls pass and prove complete,
duplicate-free suite coverage. The first full test run stopped at an
admitted chat Retry polling failure. CI also found a stale
retained-allocation fixture, now repaired on master by #18984, and a
canceled runner job. A real SDK suite initially relied on a dependency
absent from the root workspace; the explicit standalone target now owns
that acceptance.

The separate Product E2E support run had 12 failures in four untouched
files. All 12 reproduce on the original clean base with independent
dependencies, compiled base source and verified workspace resolution.
These concern workflow expectations, cleanup timeout arguments, a
profile snapshot and operating receipts. They are not passing
acceptance.

The later full local repository test command stopped at a 15-second
timeout in the unchanged GitHub pre-allocation retry `controller` case.
That exact case passed in isolation with unchanged assertions. The broad
Runner run initially omitted `test:typescript:prep` and lacked debug
fake-provider binaries. Required preparation then passed 370 + 44
package contract cases (11 skipped); the prepared full package run
reported 4,932 passed, 9 failed and 13 skipped. All failed families then
passed with unchanged assertions in isolation: 8 ACPX controller tests
and 2 credential controls. The paired original-base ACPX control also
passed. The full-run failures remain recorded; they are not passing
full-suite evidence.

The first Rust-cached native CI run found a missing generated OpenCode
proxy in a fresh checkout. The fixture now runs the existing verified
provider-entrypoint builder. Removing the generated proxy reproduces the
exact ENOENT before the fix; both real retry cases pass after the fix
from the same absent-artifact state.

All current-head CI gates pass, including the aggregate verification
check. Linux native CI passes all 10 suites: 177 tests pass and 14
opt-in cases skip. Both real provider retry cases pass. The full Runner
package passes 297 suites and 4,950 tests, with 1 suite and 10 tests
skipped. The CLI workspace groups pass 4,762 and 286 tests. The first CI
attempt had Runner shutdowns in the Runner package, Canary and three
general server jobs. One unchanged CLI shard also hit child-server
startup and dense ZIP deadlines. Both CLI files passed unchanged in
isolation: 18 tests. One bounded rerun of unsuccessful jobs and their
dependents passed. The original failures remain recorded.

## Risks

- Older controllers reject newly written v2 state on downgrade. Current
controllers retain v1 restore support, but old or unmarked state cannot
gain this new proof.
- An older plugin that omits structured ingress evidence remains held.
The server and provider must use the new producer contract for this
recovery path. The Docker `cloud` target builds the SDK, server/Runner
and standalone Daytona producer from one source tree. The default
`production` target does not build the standalone producer. Hosted
rollout must confirm the new image and active image-owned plugin worker;
a registry version alone does not prove its source. An npm-installed
producer needs an explicit matching artifact upgrade.
- This prevents future proved startup failures. It does not release
historical accounting based on an old checkpoint, change retry budgets,
replace a model or weaken sandbox isolation.
- Zero model usage is separate from process termination, saved files and
resource release. Those owners keep their existing gates.

## Model Used

OpenAI Codex with tool use and code execution. This runtime does not
expose the exact backend model ID or context window to the agent.
Read-only independent agents reviewed checkpoint compatibility and
SDK/RPC error boundaries.

## 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 (affected suites; full
local run limits are recorded 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-04 12:16:51 -06:00
DottaandPaperclip 396b7fc62a fix(runner): scope session diagnostics to the active run recorder (#18944)
## Thinking Path

> - Paperclip manages agents and their work.
> - The native executor can retain a transport between runs.
> - Each run recorder has a shorter lifetime than that transport.
> - The transport kept its first recorder after warm reuse and
completion.
> - A later close could write to the wrong run or a sealed object log.
> - This change binds diagnostics to the active run epoch and retires
that binding at execution exit.
> - The existing recorder still owns accepted-byte draining and honest
incomplete finalization.

## Linked Issues or Issue Description

**What happened?**

A warm native transport retained the first run's diagnostic callback.
Reusing the session for another run did not change that callback. A
later idle close could append to a retired writer and emit a persistence
warning.

**Expected behavior**

Active diagnostics belong to the current company, session and run. A
retired transport callback must not use a completed recorder. Diagnostic
metadata must not delay essential execution cleanup.

**Steps to reproduce**

Run the native warm-session regression with two sequential run owners.
Close the retained transport after both runs finish. The original code
calls the first recorder. The real completion regression also observes
the resulting closed-writer warning after normal lease cleanup.

This is a local source defect with behavioral red/green proof. Repeated
hosted generic persistence warnings motivated the investigation, but
their original exceptions were not retained. This PR does not claim to
identify every such warning or repair an export failure.

Related #18379 queues sanitized performance spans in heartbeat. It does
not own transport diagnostic callback lifetime.

## What Changed

- Bind transport diagnostics to an exact active company/session/run
epoch, including warm reuse.
- Activate a retained channel in the existing session-admission hook
after identity validation and before attachment starts, including an
attachment failure before `onSession`.
- Forward active diagnostics synchronously. Detach their recorder at
execution exit. Keep the original asynchronous metadata semantics.
- Keep idle transport evidence inspectable. Log only a fixed category
and trusted company/run identifiers to the process logger.
- Preserve recorder-owned byte draining, redaction, source coverage and
genuine persistence-failure warnings.
- Cover actual native executor, completion and lease-close boundaries,
including stalled local appends and exact object-log source/hash
preservation.
- Place the retained-workspace test fixture under the existing
published-path contract after merging newer recovery tests from master.
Production validation and native/catalog assertions remain unchanged.

## Verification

- Current head: `c500e4933a85a4c453f66c872fa335108c661b15`. Normal
master merges preserve upstream behavior. The one textual conflict was
in the executor imports.
- The first GitHub run tested merge tree
`a4cb59448a3b5cc2047ac41c691720e6acec6e8d`, which included newer
recovery tests from master. Five cases failed with
`remote_workspace_published_path_invalid`. The same five failures
reproduced on an identical local tree. The fixture now puts retained
files under `.paperclip-workspaces/<workspaceId>` and keeps the target,
lease and authority bound to that path. Production validation and the
native-success/catalog-denial assertions are unchanged.
- On unchanged `c500e493`, the full recovery file passed **244/244**
tests. Existing cold-restore and occupied-root controls passed **4/4**.
Server typecheck, server production build, module boundaries and
whitespace checks passed. An intermediate missing-import setup failure
was corrected and is separate from the five-case behavioral red.
- Before that fixture correction, local `f2fdaddb` passed **1,221
tests** across the five diagnostic, executor, output, completion and
object-store files. Recursive typecheck and full repository build also
passed. Those results are tied to that earlier revision; [fresh
current-head
CI](https://github.com/paperclipai/paperclip/actions/runs/41842043248)
must qualify the final merge.
- Behavioral regressions cover the original closed-writer warning, warm
A-to-B ownership, and an actual Runner attachment exception before
`onSession`. The admission hook gives the attachment diagnostic to B and
leaves A untouched. The attachment fixture uses synthetic event
persistence and outer failure projection; it does not certify a real
database terminal projection.
- The tests preserve synchronous diagnostic admission and asynchronous
metadata recording. A stalled append reaches the existing bounded
finalization result with unknown size/hash. The normal object-log
control preserves canonical bytes, SHA256 and complete source coverage
after lease cleanup.
- An initial local run had **37 PostgreSQL startup failures** because
the Darwin package lacked aliases to its included versioned libraries.
Only isolated `node_modules` aliases were restored. No source or
lockfile changed for this setup issue. A passing intermediate run
crossed a source merge and is excluded from immutable-head proof.
- An earlier combined test run passed 1,216 assertions but exited with a
completion `afterAll` timeout. Its cause remains unproved. Later
isolated 60/60 passes used the original timeout and do not erase that
failure. An earlier repository-wide `pnpm test:run` was interrupted
after its source became obsolete; it is not full-suite or current-head
proof.

Greptile reviewed the exact `c500e493` head at **5/5** with no open
findings. Fresh CI must pass before merge. The native review is queued
behind its separate saved-file recovery; no current-head native
assessment is claimed.

The provider execution loop is a fixture. These tests do not start a
model or prove hosted recovery. Missing accounting receipts remain
pending, not zero cost. Checkout-release invocation is observed; this is
not a separate task-arbiter journey. The original native review
published a valid finding before a separate workspace-export failure.
The finding is fixed; this PR does not claim to fix that export failure
or every hosted persistence warning.

## Risks

Idle process logs intentionally omit raw provider text. The existing
bounded transport evidence retains its own diagnostics. Accepted fs
appends that do not settle retain the existing unknown-size/hash warning
and cannot be certified complete. No new timeout, storage policy, schema
or provider execution is added.

## Model Used

OpenAI `gpt-6.1-sol` through Codex assisted with diagnosis,
implementation, tests and review. The exact context-window size is not
exposed. Tool use and code execution were enabled.

## 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 local tests and recorded passing checks, pending checks
and unresolved failures 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] I will require all Paperclip CI gates to be green before merge
- [x] I will require Greptile 5/5 with no unresolved findings before
merge
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 08:04:52 -06:00
DottaandPaperclip b891e69bd2 fix(native): correct unread wake comments before completion (#18937)
## Thinking Path

> - Paperclip manages AI agents and their work.
> - Native agents report completion through a server-owned tool
boundary.
> - A truncated external-chat request requires a complete run-bound
comment read.
> - The terminal guard checks that receipt after the provider ends its
turn.
> - Early completion feedback omitted the same check and could accept an
unread report.
> - The provider then lost its chance to correct the report in the same
turn.
> - This pull request returns the existing read requirement through the
completion tool before accepting the report.
> - The final guard still protects the exact source and snapshot after
provider settlement.

## Linked Issues or Issue Description

Related: #16995 keeps run-scoped readers available to retained provider
threads. #18913 preserves queued comment input. This change fixes a
separate completion-feedback boundary.

**What happened?**

A native external-chat run could submit an accepted completion without
reading all server-bound comments. The terminal guard then failed the
run with `native_current_wake_comments_unread`. Early feedback also
accepted partial, changed and foreign read receipts. The agent did not
receive the required correction while it could still act in the same
turn.

**Expected behavior**

The completion tool must reject the report with a
`read_current_wake_comments` instruction and the cursor requirements. A
complete current read must permit the corrected report in the same turn.
The final snapshot check must remain effective.

**Steps to reproduce**

1. Create a running native task with a truncated external-chat wake and
exact accepted comment IDs.
2. Call completion feedback before reading the comments, or after only a
partial or stale read.
3. Observe that the old code returns an accepted report instead of the
read correction.

## What Changed

- Check the existing run, company, task and agent binding before every
completion-feedback acceptance return.
- Reuse the authoritative current-wake reader and terminal receipt
validator.
- Share the two unchanged read-error codes from their owner across the
validator, feedback and executor classification.
- Convert only the two owned unread or changed-snapshot errors into a
bounded tool correction. Other errors still propagate.
- Keep the final validator, receipt parser, result persistence and
admission policy unchanged.
- Add real PostgreSQL regressions and an actual OpenCode MCP tool
sequence with a controlled local provider process.
- Document the early correction and final snapshot fence.

## Verification

The source checkout uses Node 24.22.2 on macOS arm64 with a fresh owned
embedded PostgreSQL database.

Final published tip: `37bfb269432f86f98cab1a58c09b5519ed9477fd`. Its
production code is commit `8c7297318c3a841fc1c72c846acfaabf58adabe9`;
the last commit adapts three existing module mocks. The expanded
affected group passes **1,284 cases** with **35 existing opt-in skips**.
The skips require captured private-runner transition fixtures and a
configured fresh database, which were not supplied. All nine real
PostgreSQL/MCP cases execute and pass. Recursive typecheck and the full
build pass for production commit `8c7297318c`; the final changed test
layer also passes.

- Untouched master `1a39dea9aa2bb9bea416439b0a574c788e88b9a0`: four
causal failures. Unread, partial, changed and foreign complete receipts
incorrectly receive accepted completion feedback. The ordinary
inline-complete control passes.
- Current source: all nine new cases pass. The actual authorized reader
returns the full accepted comment. The same running task and run remain
intact, with no replacement or accepted-result replay.
- The OpenCode native backend sends a real MCP finish rejection to the
controlled provider process. The process follows the returned reader
instruction, reads every page, and resubmits finish. One provider turn
produces one semantic result proposal. The terminal snapshot fence still
rejects a later comment edit.
- All 98 affected OpenCode driver and MCP bridge cases pass with the
fixture-mode addition.
- The first tool-sequence attempt failed on a fixture-only relative path
before starting the provider. The test now uses its module URL. This
setup failure is separate from the four causal failures.
- The first recursive typecheck found missing local workspace links in
the clone's excluded sandbox-provider directories. Existing dependency
links were copied into the isolated clone. No lockfile or source change
was made for this setup repair.
- At `c57e532a87`, all 1255 cases in the six-file affected server
family, recursive Node 24 typecheck and the full build pass. At
production commit `1ae9f54107e1762bc8222572b365a013d4572d1a`, all 1,257
affected cases, recursive Node 24 typecheck and the full build pass.
- Greptile found that the initial correction could restart a partial
read after page two. On `c57e532a87`, following that correction fails
with the actual reader cursor-order error. The follow-up continues the
caller's most recent cursor and retains the changed-snapshot restart
path. All nine new cases pass, including this regression, a
stale-partial control, the foreign receipt refusal and the actual
same-turn MCP sequence.
- [Linux server3/12
job](https://github.com/paperclipai/paperclip/actions/runs/41788671370/job/126437378334)
found five failures in the older format-feedback SQL fixture. Its flat
heartbeat row did not match the real joined projection. The same five
failures reproduced locally. The test-only follow-up returns a coherent
run/agent/task projection and keeps the real resolver and formatter
assertions. All 14 format and real-PG/MCP cases pass, and the generated
server test layer typechecks. Production code and the earlier
1,257-case/typecheck/build proof are unchanged.
- Qualification of the shared-code change found 49 failures because
whole-module mocks omitted the new exports. The three mocks now pass
through actual module exports and keep their controlled reader methods.
Literal error assertions remain independent. The final 1,284-case group
passes with the exact 35 opt-in skips described above. No production
fallback or substitute code was added.
- [One Linux Runner package
attempt](https://github.com/paperclipai/paperclip/actions/runs/41792315273/job/126448500037)
passed 4,913 cases and skipped 10, but its synthetic post-rename
credential durability case failed with ENOENT. The complete Runner
package tree and lockfile are identical to the [earlier Linux
control](https://github.com/paperclipai/paperclip/actions/runs/41790199252/job/126441739681),
where all 42 credential cases passed on Node 24.22.2. The exact case
also passes once on both base and head on macOS with Node 24.22.2. The
original failure cause remains unverified; OS/load and shared-test
timing uncertainty remain. Credential code and assertions are unchanged.
The single affected-job rerun passes, including all 42 credential tests.
The original failure remains unexplained.
- An ordinary full local test attempt was stopped before the review
repair. It has no completion summary and is INCOMPLETE. Current-head
full CI completed; no local full-suite pass is claimed. Greptile
reviewed the final published tip at 5/5 with zero open threads. All
current-tip checks pass: 54 successful check runs, one successful legacy
commit status, 3 skipped and 2 neutral, with no pending or failing
checks. Paperclip also reviewed final tip `37bfb26943` at 5/5 with no
remaining findings. Its prior 4/5 code-ownership recommendation is fixed
and resolved. No review nudge or hosted task retry was issued.

The controlled OpenCode process uses the supported v1 factory envelope
to exercise the common native feedback and MCP boundary. It does not
call a live model or qualify a provider pack. PostgreSQL, authorization,
pagination and receipt checks are real. No hosted task, model,
permission or saved result was changed. Recovery of an already accepted
historical run remains unverified.

## Risks

- Each finish call adds an exact binding query. A mandatory read also
validates the current accepted-comment snapshot through the existing
transaction. Cost grows with those exact comment IDs, bodies and
attachment metadata; it does not scan unrelated task history or add idle
polling.
- A valid read can become stale before the provider settles. The
unchanged terminal guard still rejects that race.
- Missing ownership or unexpected validation errors remain refusals.
This change does not grant replay authority or turn an accepted
historical result into fresh work.

## Model Used

OpenAI Codex, GPT-6 family, with repository inspection, code execution
and GitHub tools. The runtime does not expose the exact serving model ID
or context window. The coordinator performed independent read-only
source review.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` /
`Closes: #` / `Refs: #` OR (b) described the issue in-PR following the
relevant issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change and contains no internal
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-04 07:19:12 -06:00
DottaandPaperclip d8c1b9d4b5 fix(runtime): reclaim failed remote checkpoint retries (#18877)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native recovery must preserve the original execution and provider
state.
> - A failed remote run can retain its provider PID after heartbeat
releases the task execution lock.
> - Normal resumption excludes that PID, while restart recovery rejects
the released lock.
> - Recovery must reclaim that exact execution only after current
admission and durable ownership checks pass.
> - Failed retries and admission holds need separate finite budgets.
Neither can prove a provider stopped.

## Linked Issues or Issue Description

**What happened?**

A failed remote native run remains in retryable failure without a task
execution owner. Normal resumption and restart recovery both reject it
before inspecting the preserved remote provider. The current remote
workspace reference format also misses the due-retry routing path.

**Expected behavior**

A due exact-checkpoint retry reclaims its original task execution only
when current ownership and safety gates permit it. Newer task input
prevents an old claim. Lost or ambiguous provider state still prevents
replay or replacement. Ineligible recovery eventually becomes a quiet
manual reconciliation obligation.

**Steps to reproduce**

1. Start a remote native run and persist its provider checkpoint and
encrypted Runner authority.
2. Fail with exact-checkpoint recovery and a retained remote PID.
3. Release the task execution lock at heartbeat completion.
4. Run ordinary resumption and restart recovery after the retry
deadline.
5. Observe that both reject the same eligible run before remote
inspection.

**Paperclip version or commit**

Reproduced from source at `d59791828482`; integrated with upstream
`db9be4c5a6`.

**Deployment mode**

Remote workspaces with PostgreSQL. Local regressions use disposable
PostgreSQL and encrypted checkpoint persistence.

Related public repairs #18846, #18854, #18860 and #18841 cover finished
per-turn owners, pre-provider failures, unstarted follow-ups and storage
migration. They do not reclaim this failed run's released execution
lock. Merged #18879 also adds scheduled-resume lock retention and
reclaim. This PR keeps that compatibility and requires encrypted Runner
authority, canonical source ordering and a charged failed retry before
reclaim. Its scheduled-resume regression now uses a real encrypted
authority and retains a snapshot-only refusal control.

## What Changed

- Share normal admission checks and one guarded execution-binding
restore/event helper between normal and remote resumption.
- Keep agent-name normalization in a module with no imports. Preserve
heartbeat's existing export. Native admission no longer loads heartbeat
orchestration for a string key.
- Recheck company, assignment, agent eligibility, Stop, pause, approval,
dependencies, execution and checkout ownership before reclaim.
- Reject newer task input using PostgreSQL creation-time and ID
ordering, preserving timestamp precision.
- Require the recorded remote target, encrypted Runner authority, exact
checkpoint identity and existing controller fence.
- Commit task binding, controller lease, run status and recovery event
atomically. Recognize both remote workspace reference formats.
- Charge failed reattachments against the existing
three-provider-attempt budget, including retained-lock crash windows.
Active controller adoption preserves its charged attempt.
- Use the existing five-retry/30-minute system-recovery policy for
admission holds. Ended tasks and newer sources exhaust it immediately.
Preserve PID, checkpoint and task bindings, suppress resource
settlement, and assign manual reconciliation with executor stop/state
explicitly unverified.
- Register recovery intent before deadline writes; preserve live/future
deadlines quietly and roll back zero-row writes. Record changed hold
reasons once and terminal dispositions separately.
- Keep historical provider-budget exhaustion behind current
admission/source/controller/authority checks. Make an existing
orphan-cleanup fixture explicitly due.
- Include exact plugin task leases that have no workspace reference.
Reuse every reclaim and retry-budget guard, with a bounded
company/task/run lease projection.
- Share strict manual ownership proof across stale-lock cleanup,
adoption, queue release and wake admission. Require termination receipts
for every exact remote lease before normal cleanup can release either
binding.
- Preserve governing Stop through the existing audited cancellation
path. Its acknowledgement does not release an unverified provider.
Ordinary Retry can wait for exact Stop and file preservation, then queue
one continuation.

## Verification

- Current head is `5d7baf74aa89d00d7ac6e1493d38a6ce2ae7b7b5`. Its
[current CI
matrix](https://github.com/paperclipai/paperclip/actions/runs/41782198858)
is terminal and clean: 54 successful, three skipped and two neutral
contexts. Required `ci / verify` and `ci / e2e` pass. Greptile is 5/5 on
this exact head with no open findings. No fresh Paperclip 5/5 assessment
is claimed.
- The [prior complete CI
matrix](https://github.com/paperclipai/paperclip/actions/runs/41754365388)
passed on `3e641466d3edcaa6dd5021d979627082d388eb05`, with Greptile and
Paperclip both 5/5 and no open threads.
- All 233 tests across the six affected heartbeat, restart-isolation,
remote-admission, scheduled-resume and session-binding families pass on
production-identical `a5104fa89f9cb493091e000bf21ed3858871ae46` under
Node 24.22.2 and pnpm 9.15.4. They cover both workspace formats and
plugin task leases, competing controllers, exact rollback, Stop,
assignment/company boundaries, attempts, work-intent ordering, finite
backoff and quiet held state.
- The merge preserves #18931's per-run audit-failure isolation and this
PR's unchanged-transition hold. Two initial composition cases failed
because their setup or expected result described the older contract. The
rollback test now expects the exact per-run deferral while retaining
full coordinator equality and no work intent. The routine fixture writes
a real encrypted checkpoint through the normal owner before reaching the
actual routine unique constraint. No production admission guard or
rollback assertion was weakened. A separate agent reviewed this exact
merge and fixture delta without finding a blocker.
- Full recursive `pnpm -r typecheck` and `pnpm build` pass on production
merge `89c616fe45ceff75db450f67fd34224b4df7441d`. Subsequent commits
change only three test files. Both the two-file `a5104fa` and final
`5d7baf` server test-layer typechecks pass.
- Four production-identical `a5104fa` probes pass with the rebuilt
actual Rust Runner, a scripted ACPX subprocess and encrypted PostgreSQL
checkpoints under Node 24.22.2. Workspace-reference and plugin-lease
admission each cover surviving and lost providers. Replacement preserves
one provider launch, one original side effect, the charged attempt and
restored task execution binding. Lost providers reject snapshot recovery
without replacement. This is protocol-boundary evidence, not a live
Claude or hosted provisioning claim.
- Initial `a5104fa` CI found one additional routine integration fixture
missing the persisted `exact_checkpoint_resume` mode. Its encrypted
checkpoint was already present. The exact case fails locally before
repair and passes after recording that mode and expecting the charged
provider attempt. Every exact routine conflict, rollback, checkpoint,
task binding, successor-count and sibling-startup assertion remains.
Final head changes only that fixture. The correct native-server group
passes all nine files: 175 executed cases and 14 container-only skips,
matching the now-successful native-server CI gate.
- The earlier canonical HTTP Retry acceptance passed on the prior
reviewed head with real PostgreSQL, the recovery worker and real files.
Pending Stop protects both bindings and starts no successor. Exact
termination and copyback queue one preserved-workspace continuation. No
browser or live-model outcome is inferred from that test.
- The predecessor's full local `pnpm test:run` failed in an unchanged
Discord Gateway fixture with a 200 ms lease and a 422 initial ownership
error. Focused predecessor and untouched-base controls pass; that
full-run cause remains unproved. The required command was also started
on production-identical `a5104fa`. All six local chat shards passed,
covering 1,850 cases. The duplicate local invocation was interrupted
with SIGINT while the general-server group was still running after this
head's complete CI and the combined repair qualification had passed. The
wrapper exited 1 on cancellation, no process retained its test log, and
no full local pass is claimed. An earlier attempt to select the native
lane through the CI wrapper instead selected local package sharding:
2,631 cases passed and 13 failed. Those separate failures are retained
without causal attribution or unrelated source changes; that run is not
counted as native-server qualification. The direct native group and
current complete CI matrix pass.
- Live Claude Opus 5.5 continuation, hosted plugin provisioning,
deployment and recovery of historical tasks remain unverified. No
original task was retried or mutated for this verification.

## Risks

- Restoring a task binding grants remote inspection; it does not prove
that the provider survived.
- Unknown effects, lost providers and missing semantic results retain
their existing reconciliation requirements.
- Exhausted admission holds preserve execution bindings and resource
evidence for manual reconciliation. They do not authorize a fresh
executor or provider/resource release.
- The source guard is conservative: newer task input prevents recovery
of an older failed source.
- No database migration or provider profile change is required.

## Model Used

OpenAI Codex, based on GPT-6, with reasoning, tool use, code editing and
shell execution. The runtime does not expose the exact model snapshot 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-04 07:18:07 -06:00
DottaandPaperclip 84aa989bf9 Preserve native sessions through legacy workspace enrollment (#18943)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A native session retains its workspace and provider session across
turns.
> - An ephemeral web server must restore those files without its
predecessor's disk.
> - Legacy enrollment changes the workspace authority and can change
both workspace addresses.
> - The retained provider backup checks those addresses as part of its
identity.
> - This pull request preserves the exact addresses in a verified
migration receipt.
> - The benefit is that the next turn can use the original session after
enrollment.

## Linked Issues or Issue Description

Refs #18932, merged as `612d9931a6`. This pull request builds on that
all-Runner durability change. Related: #18918 fixes concurrent workspace
preservation locks. This change covers identity preservation during
offline enrollment.

## What Changed

- The native workspace migration owner records the selected session
bindings in the successful workspace migration operation.
- Runtime selection retains the original logical address only for the
exact agent, task and session.
- Remote workspace preparation restores the original provider address.
It refuses conflicting targets, forged receipts and occupied unowned
roots.
- The environment owner includes the migration operation in the reusable
lease fingerprint. Warm policy stays warm and receives a fresh
allocation through normal lease cleanup.
- Exact migration evidence permits the existing verified provider backup
to restore into that fresh warm allocation.
- Preserve verified provider allocation files through a normalized,
lease-bound digest receipt. Unknown files and altered markers remain
refused; interrupted publication is retryable.
- Retain legacy managed-instruction session fingerprints using the exact
imported content hash and recorded paths, preserving all other
configuration and credential checks.
- Migrate existing local skill archive/manifest objects through upload
claims, readback verification and receipt compare-and-swap; retain
original objects and IDs.
- Add explicit two-turn browser qualification for every released native
Daytona profile, with warm and per-turn variants. The production
maintenance entry runs offline. The old home is deleted only after a
verified export and successor readiness. The oracle checks provider
identity, native session, addresses, reference bytes, question and final
document.

## Verification

- Full repository typecheck passed at `b12ecd823e`. The production
session fix passed 198 session/credential tests. Full repository build
passed at `94bb917947`. The final Rust-only provider fix passed 57 ACPX
provider tests and the exact Linux release build.
- Workspace and transport checks passed: 117 transport/workspace tests,
six final real PostgreSQL migration boundary cases, 178 environment
tests, 539 Daytona tests (eight skips), and the native warm-backup
admission control.
- Local skill migration passed eight PostgreSQL/S3 tests (one existing
opt-in skip). Failed readback retains the source. Successful readback
still works after deletion of the old home.
- Latest Product E2E typecheck and 38 focused harness tests pass.
Earlier catalog/admission/report checks passed. Twelve unrelated
support-test failures reproduce on unchanged base `1a39dea9aa`; these
are not reported as passing.
- Live Codex per-turn at `2d5bb9a6d1` retained the native and provider
sessions, both workspace paths and exact reference bytes after deletion
of the old home. It completed the task and delivered its saved document
and original reference. Its original failure was an output-count grading
defect. The corrected grader passes all 21 checks against retained,
hashed evidence; the original result remains unchanged.
- Live OpenCode per-turn and Codex warm at harness `94bb917947` pass all
21 product checks. Their launcher cleanup failed because a read-only
diagnostic observer kept its disposable database open. The observer now
closes before ordinary database cleanup; a no-provider reproduction
confirms the fix. All owned sandboxes were independently verified
absent, child keys revoked and buckets removed. Original failed campaign
dispositions remain unchanged.
- Claude at `94bb917947` kept its sessions and reference bytes but
omitted the final clickable document link. That user-delivery failure
remains recorded. The fixture now explicitly asks for the link and
records migration evidence before checking navigation. The fresh Claude
attempt at `b12ecd823e` passed both strict migration checks: exact
provider/native session, both paths, original file bytes and
independently downloaded attachment. Its full browser workflow failed
again: it requested unnecessary confirmation, used the answer sentence
as the reference, and omitted the clickable document link. This is
retained as a failed user-workflow result, not a full pass.
- Earlier live failures exposed and reproduced the skill-object,
fresh-allocation marker and managed-instruction fingerprint bugs. The
fixes retain original identity checks. No provider session reset or
accounting bypass was used.
- The real Grok continuation at `b12ecd823e` failed before provider
execution because its immutable-instruction attach policy rejected
relocated registered file copies. A no-provider regression reproduced
the exact rejection. `b05ba3728a` selects the existing strict
authenticated asset-path policy for Grok; content digests, unknown
context fields, session/workspace identity and same-run mutations remain
enforced. The regression and all 57 ACPX provider controls pass. The
Grok browser migration now passes all 21 checks with the b05 Linux
Runner, including provider identity, exact reference bytes and final
user output; the launcher also reports successful cleanup.
- `24fabf1d81` applies the same policy to released ACPX Codex after a
second no-provider reproduction. All 58 ACPX provider controls and the
final Linux release build pass. Its live browser migration passes all 21
checks on the final binary, including exact original session/file
preservation and completed user output.
- The full native-session executor suite passes all 1,121 tests after
updating its existing lease database fixture for the migration-receipt
join. The prior CI failure was this unsupported test-double method; the
production guard still rejects ordinary unproven recovery. CI and
Greptile 5/5 passed at `ca1e0d5fe6`. The first CI attempt lost several
workers and had one rotated-recovery transport failure; its seven
descriptor cases passed locally, and the single unchanged retry passed.
The original failure remains recorded. Staging enrollment has not
started. Initial offline enrollment and subsequent rolling availability
are separate checks.

- The final review follow-up adds real runtime-selection PostgreSQL
cases for exact, different-session and absent receipts, plus
executor-level warm restoration that copies actual backup files and
verifies provider identity and the new lease stamp. Both complete
affected suites pass (1,172 tests), and full repository typecheck
passes. Removing either production migration connection independently
makes its new positive case fail; both source files were restored
byte-for-byte after these negative controls.

- Final commit `0971d373a3` passes all 63 checks (57 successes, five
expected skips, one neutral); Greptile is 5/5 and every review thread is
resolved.

## Risks

- An identity conflict blocks enrollment and keeps the original files.
It does not silently reset the session.
- A new migration generation retires a reusable lease through its
existing owner. The original sandbox must finish normal cleanup before
reuse proceeds.
- Initial enrollment has an offline step. The separate rolling suites
prove uninterrupted deploy behavior after enrollment.
- This adds no schema migration and changes no provider credentials or
accounting gate.

## Model Used

OpenAI Codex, GPT-6 family. The exact serving model build and context
window are not exposed in this session. The agent used reasoning,
terminal tools, code edits, PostgreSQL/filesystem tests and isolated
live browser qualification.

## 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-04 04:43:56 -06:00
DottaandPaperclip 612d9931a6 Support per-turn Runner sessions on ephemeral web servers (#18932)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The web server controls native Runner sessions in remote sandboxes.
> - An ephemeral web server must recover without its previous local
filesystem.
> - The existing admission rule accepts warm sessions. Managed Claude,
Grok, and OpenCode sessions resolve to per-turn execution.
> - Per-turn execution also needs durable provider-session bytes after
the sandbox is retired.
> - This pull request adds encrypted backup publication and removes the
warm-only admission rule.
> - Both lifecycle modes can use the existing native ownership and
checkpoint contracts during a web deployment.

## Linked Issues or Issue Description

Refs: #18518, #18529, #18826, #18835, #18841, #18864, #18887.

**What existing behavior does this improve?**

Native Runner execution on web servers with
`PAPERCLIP_EPHEMERAL_SERVER=true`.

**Current behavior**

The admission rule requires a warm Runner lifecycle. Managed provider
credentials can resolve an agent to per-turn execution, so enabling the
ephemeral deployment profile blocks those turns. A database controller
checkpoint does not preserve provider files after the old web home and
per-turn sandbox are removed.

**Proposed behavior**

Admit native execution in either resolved lifecycle. Keep provider and
credential choices unchanged. Publish encrypted provider state to shared
storage before sandbox cleanup. Restore it from a fresh web home for the
next turn. Use the existing controller lease and generation checks for
active takeover.

**Reason and benefit**

Operators can remove the web server volume without excluding per-turn
providers or forcing managed accounts into warm sessions. New native
Daytona profiles automatically enter both rolling-deployment
qualification lanes.

**Breaking changes**

The additive migration creates a backup publication ledger. Existing
volume-backed local-disk installations retain their local backup path.
S3 installations publish encrypted backups and require a stable secrets
master key. Existing-volume enrollment must import surviving session
data before detaching the volume.

## What Changed

- Admit native Runner execution independently of its warm or per-turn
lifecycle. Keep host-owned legacy execution excluded from ephemeral
deployments.
- Store private provider state as encrypted objects. Journal uploads
before I/O, read them back, and fence publication against the current
native owner. Keep the previous published backup when storage or
ownership checks fail.
- Restore exact provider bytes into a private cache. Require an exact
durable backup before sandbox cleanup. Extend the existing bounded
retention pass to retire stored objects without adding idle polling.
- Import selected legacy controller and provider state in the existing
offline maintenance process. Preserve queued work, task pointers,
pending recovery, and original files. Prefer canonical database
checkpoints.
- Preserve already-failed legacy controllers with missing original
artifact proof as encrypted archives. These archives do not become
execution authority. Successful retained sessions still require proof.
Report archival and executable imports separately.
- Verify unmanaged Grok API keys through the existing xAI account
validator. The live test exposed a readiness gap that kept remote Grok
agents paused after platform checks. Preserve failed credentials and the
separate runtime warning.
- Ingest Grok whole-prompt usage at the owning ACP boundary. Bind the
receipt to the exact outbound request/session; reject replay, conflicts,
and incomplete authority. Separate turn cost from cumulative session
cost in the sidecar schema and Rust normalization. Preserve managed
billing identity and identify direct xAI API credentials.
- Preserve published workspace directory names across cold allocations,
including destroyed-allocation cleanup. Keep unpublished epoch/lease
directories isolated and refuse to overwrite an occupied preparation.
This fixes the cold-session identity regression exposed by #18922.
- Increase bounded checkpoint-header inspection to 1 MiB. Retain the
aggregate inspection limit.
- Register all nine native Daytona profiles in per-turn and warm
rolling-deployment suites. Delete the entire departing server home,
exercise ordinary reads and writes, require active ownership transfer,
then check a later turn on another fresh server. Permit only exact
notification replay after reconnect.

Ownership stays with the existing modules: Runner validates its protocol
state; Core validates run scope and controller ownership; storage owns
object I/O; the maintenance process owns offline conversion; the E2E
wrapper owns process replacement and probes. No provider protocol is
added to the shared admission rule. Grok receipt parsing stays in its
provider module. The shared sidecar gains an explicit cost basis because
its existing price field means session-cumulative spend, while Grok
reports prompt totals. Existing consumers retain cumulative behavior
when the new field is absent.

## Verification

- Focused native admission, archive validation, publication fencing,
cleanup, readiness, legacy import, and E2E observer tests pass. Real
PostgreSQL cases verify that queued intent and historical recovery rows
stay unchanged.
- Full typecheck and build pass on the rebased branch. The review
follow-up passes server typecheck, 29 focused
PostgreSQL/archive/readiness tests, and module-boundary checks. Revision
fb8d39a7a1 reached 55 successful CI checks and Greptile 5/5 with no
unresolved threads. The Grok follow-up passes Runner/server typechecks,
238 ACP runtime/projection/usage tests, 16 sidecar-schema tests, the
Rust cost regression, and Core billing regression. The e9aae9cf revision
passed all 57 successful checks, with 5 skips and 2 neutral checks, and
exact-head Greptile 5/5. The latest 9cbec30a4f follow-up changes only
E2E catalog/admission controls; its 291 affected tests pass, all 57
check contexts succeeded (5 skips, 2 neutral), and exact-head Greptile
is 5/5 with no open review threads.
- Migration numbering and safety checks pass.
- A local real PostgreSQL/S3 roundtrip preserved 2,870,101,591 payload
bytes across 89,100 archive entries after deleting the source directory.
Restored SHA-256 matched; upload/readback took 56.5 seconds, restore
74.8 seconds, process peak RSS was 1.40 GB, and all fixture resources
were removed.
- Archives preserve contained relative links, reject link
chains/traversal, and enforce independent byte/entry/object limits.
Offline verification streams hashes. Orphaned deleted-run backups stay
on the original volume. Obsolete object retention preserves the selected
backup, live lease references, and pending uploads with owners.
- Isolated API-key Codex deployment: original active runner/session
transferred to the replacement and committed. All 567 reads, 57 writes,
and 57 readbacks succeeded. A second replacement completed a
same-session continuation with all 287 reads and 29 writes succeeding.
The strict cell failed two agent-authored shell commands; this result is
retained as failed.
- Isolated API-key Claude deployment: original active runner/session
transferred and committed. All 584 reads, 59 writes, and 59 readbacks
succeeded. A second replacement completed a same-provider-session
continuation, with 211 reads and 22 writes succeeding. The strict cell
failed an initial completion call that omitted a registered attachment
citation; the agent corrected it. This result is retained as failed.
- Isolated API-key OpenCode deployment: the same active runner/session
transferred and committed. All 481 reads, 49 writes, and 49 readbacks
succeeded. A second replacement retained the provider session and
completed useful work, with 156 reads and 16 writes succeeding. The
strict cell failed agent tool calls: a denied external-directory write,
malformed artifact hashes, and a second completion after the first
result was committed. There was one submitted/completed turn and one
committed result; reconnect notification replay was identical. This
result is retained as failed.
- Grok's real first turn now completes with settled usage and provider
cost ($0.41397); the next turn is admitted normally. That attempt then
exposed a shared workspace-path identity regression before deployment.
Three cold-restore cases reproduced it before the fix; 106
workspace/transport tests and four focused cold-restore cases pass
afterward, including destroyed allocation and occupied-directory
preservation. Server typecheck and build pass. At b5b0dbb03f both
controller switches passed the read/write probe without failure, and the
active runner retained its process while the controller generation
advanced. The third turn exposed an invalid reasoning-token subset
check; ad07e681ba fixes it with the actual receipt regression. Two later
live attempts failed with an upstream Grok rate limit before deployment.
A tiny preflight prompt succeeded before the second attempt, but the
full workload was still rate-limited. The final live gate remains
incomplete. No accounting or identity guard was bypassed. Earlier failed
attempts and their exact resource-cleanup receipts remain recorded.
- Earlier Codex/Claude/OpenCode receipts predate #18922; they establish
their tested revisions. Current-head live qualification remains pending;
#18943 separately preserves pre-enrollment logical/provider workspace
addresses and adds actual legacy migration proof. Its first Codex
migration attempt stopped safely at skill-version verification.
- Earlier attempts remain recorded: missing local provider-pack setup
blocked one Claude launch, and the observer rejected identical reconnect
replay in another. Setup and the observer were corrected before the
latest run.
- Named staging enrollment has not started. Its serving revision and
post-deployment journeys remain separate delivery gates.

- Broader E2E harness validation exposed deployment-catalog and
installed-lane coverage drift; the affected 291 controls now pass.
Twelve other failures reproduce on unchanged base 1a39dea9aa. They
concern cleanup call expectations, dated operating-overhead fixtures, a
Pi profile-version expectation and workflow-security expectations. No
baseline failure is reported as passing.

## Risks

- Provider archives can contain credentials. Objects use authenticated
encryption with scoped associated data and a wrapped data key. The
original stable master key remains required.
- Storage failure prevents publication and sandbox cleanup. Operators
must keep shared storage available and preserve the previous volume
during enrollment.
- Legacy state without verified identity cannot become executable
recovery authority. Archival preserves evidence and existing failures;
it does not recover already-lost provider files or clear blocked tasks.
- Local overlap probes establish the tested application behavior. They
do not establish Railway routing behavior or live managed-subscription
qualification.

## Model Used

OpenAI Codex, based on GPT-6, with reasoning, tool use, code execution,
and live integration tests. The exact runtime variant 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 `#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
- [ ] 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-04 02:12:51 -06:00
DottaandPaperclip e2dc2cc0be fix(native): retire idle shared-sandbox ingress owners before dispatch (#18893)
## Thinking Path

> - Paperclip manages agents and their tasks.
> - Native tasks can share one reusable sandbox workspace.
> - A completed child can keep its provider process warm.
> - A parent has its own logical session, but the sandbox has one
ingress listener.
> - Retirement by task identity leaves the child's listener in place.
> - A new parent Runner then fails to bind that listener.
> - This pull request retires a proven idle physical owner before the
parent's distinct session starts.

## Linked Issues or Issue Description

Refs #15353 and #17097 for listener configuration and warm transport
rotation. Refs #18682 for owned process retirement. #18860 repairs a
different resume-source failure. Searches found no existing repair for
retained warm sessions on different tasks that share the same physical
ingress allocation.

**What happened?**

A completed child and a later parent can use the same reusable sandbox.
Warm-session retirement compares logical task ownership. It does not
find the child's distinct task scope. The child keeps the fixed ingress
listener, and the parent's new process fails with
`runner_ingress_bind_conflict` / address already in use.

**Expected behavior**

A parent must start in its own logical session after the proven idle
prior listener closes. Active, foreign or unverified owners must remain
protected.

**Steps to reproduce**

Run the `retires the completed child listener` cases in
`native-session-executor.test.ts`. The actual executor registers a warm
child. PostgreSQL holds its committed, accounted result and clean
expired lease. A distinct parent receives the same allocation and
current workspace writer. The provider boundary uses real TCP listeners.
The original executor reaches the second bind with the child still
listening and fails with EADDRINUSE. The changed executor waits for the
owned close, then starts the distinct parent session.

**Paperclip version or commit**

Reproduced on ab2d2098a2. The branch
includes current master 44a4ca1c61 before this PR was opened.

**Deployment mode**

Native Runner with provider ingress on a reusable sandbox workspace.
Local regression uses real PostgreSQL and local TCP with a controlled
provider-process boundary. No hosted provider call was made.

## What Changed

- Keep physical ingress identity separate from logical task/session
ownership. Never attach the parent to the child's session.
- Reserve a physical allocation during local admission. Retire a
distinct warm owner only after canonical company, agent, workspace,
provider, allocation, lease, task and coordinator checks pass.
- Require the current remote workspace writer and location to match the
admitted run. Require the prior run to be committed and accounted, with
its original controller generation and a clean released or expired
lease.
- Use the existing close, checkpoint and stopped-file collection path.
Wait for close before parent dispatch, then recheck canonical authority
before staging files or calling the parent backend. Retain the entry
when close is uncertain.
- Add actual executor/PostgreSQL/TCP safety cases and document the
contract. Keep maps unchanged for this bug fix.

## Verification

- Untouched production at ab2d2098: actual executor + PostgreSQL + TCP
fails EADDRINUSE. Same positive case passes after the change.
- Final focused suite: 20 passed. It covers distinct parent session
identity; foreign company, allocation and workspace; active or
unaccounted owner; changed controller; reassignment; competing
lease/writer; stale allocation metadata; Stop before and after claim;
workspace-lock race; failed close that leaves the listener and accepted
result intact; and controller transfer, Stop or writer change during
awaited close. These last cases reject the stale parent before backend
invocation after the old listener closes.
- Untouched head3425acbe: transferring the incoming coordinator during
close still reaches the parent backend. The same actual
executor/PostgreSQL/TCP case passes after the post-close authority
check.
- Broader `native warm session supervision` suite: 160 passed. This run
preceded the final two-line logical-session collision guard; the final
20-case suite includes that guard.
- `pnpm --filter @paperclipai/server typecheck`: all source and test
layers passed, including `src/__tests__`.
- `pnpm --filter @paperclipai/server build`: production server and
Runner build passed at current head572acfef79. The exact-head build
stamp is recorded.
- The provider/close boundary is controlled. The fixture seeds canonical
committed/accounted finalization data in PostgreSQL. These tests do not
claim a live model result, operator ACK, hosted PID-to-listener
identity, or recovery of a historical task.
- Full repository pnpm checks were not run locally for this server-only
slice. Required current-head CI and reviews are pending.

## Risks

The new retirement path can close a process from another task. It
requires the same canonical company, agent, allocation and workspace, a
current exclusive writer, and a completed idle owner. Missing or changed
evidence leaves admission blocked. Active owners, changed generations,
reassignment and cancellation remain fenced. A failed close retains the
owner and saved result.

This covers current-controller warm entries with recorded physical
identity. Historical processes outside the registry keep the existing
exact-owner recovery requirements. No port shuffle, port kill, task
capability expansion, new polling loop or invented receipt is added.

## Model Used

OpenAI GPT-6.1 Sol (`gpt-6.1-sol`) in Codex, with source editing,
terminal execution and tool use. The host did not report its
context-window size.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have 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-03 19:44:12 -06:00
Nicky LeachandClaude 8fd917aedf fix(native): accept late provider notices in recovery (#18890)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native Runner sessions keep a provider conversation across task
turns.
> - When a later run gets a new task lease, recovery must prove that the
prior run settled.
> - Recovery required `run.terminal` to be the last committed event.
> - A provider can send a late notification for a settled turn. The
Runner then records a `provider.notice.recorded` warning after
`run.terminal`.
> - This false denial stranded a clean session. The notice stays in the
journal, so each later run on that session meets the same denial.
> - This pull request lets recovery accept a recoverable notice after
the terminal event. All other new events still block recovery.
> - The benefit is that a late warning no longer ends session
continuity. A denial now also names the check that failed.

## Linked Issues or Issue Description

No issue exists. I searched open and closed pull requests and issues for
the error code, the event name and the function name. I found no
duplicate.

**What happened?**

A run failed in about nine seconds with `runner_state_identity_mismatch:
plugin_checkpoint_recovery_unproven`. No provider work started. The
prior run had finished and was committed as succeeded. Its journal had
one `provider.notice.recorded` warning, "ignored a notification for a
settled Codex turn", 1 ms after `run.terminal`. A second session had the
same warning about 61 seconds after a normally completed turn.

**Expected behavior**

Recovery accepts the session and continues the provider conversation. A
recoverable notice does not start work, so it must not block recovery.

**Steps to reproduce**

1. Start a warm native session in a plugin sandbox environment.
2. Finish a run so that a provider notice follows `run.terminal` in the
journal.
3. Wait until the idle timeout stops the Runner task and releases the
lease.
4. Start the next run on that session. It gets a new task lease.
5. The run fails with the error above. The notice stays in the journal,
so a retry on the same session meets the same denial.

The `local-receipt-late-notice` test mode in
`native-session-executor.test.ts` reproduces this failure on the unfixed
code.

**Paperclip version or commit**

Observed on `d8a9b3eb0c`. The same check is on `master` before this
change.

**Deployment mode**

Cloud deployment, with a native Runner in a plugin sandbox environment.

## What Changed

- `isSettledRemoteWarmControlState` accepts a `provider.notice.recorded`
event after the terminal event. The notice must be recoverable and have
`info` or `warning` severity. It must come from the same runner, run,
session and lease.
- Error notices, unrecoverable notices, notices from another source and
new work still block recovery.
- Checkpoint recovery and local quiescent recovery now use this
function. They no longer have separate checks that require
`run.terminal` to be the last event.
- Each checkpoint recovery denial adds a specific reason after the
existing code, for example `plugin_checkpoint_recovery_unproven:
journal_not_settled`. The `runner_state_identity_mismatch: ` prefix does
not change.
- I reworded the existing comment on `isSettledRemoteWarmControlState`
so that it stays correct. I added no new code comments.

## Verification

- **Failure before the fix:** I added the tests first and ran them on
the unfixed code. 38 tests failed. In the 4 `local-receipt-late-notice`
cases, checkpoint recovery rejected with
`runner_state_identity_mismatch`. In the `late provider notice` case,
quiescent recovery rejected with `quarantine_owner_unverified`. The new
unit test for notices also failed. The other 32 failures were assertions
for the new denial reasons.
- **Targeted tests after the fix:** 134 tests passed, with 987 skipped
by the filter.
- `cd server && pnpm exec vitest run
src/services/native-runtime/remote-warm-session-recovery.test.ts
src/services/native-runtime/native-session-executor.test.ts -t "settled
remote warm control evidence|recovers a blocked|automatically recovers
only a proven settled local session"`
- **Type error found in review:** Vitest does not check types, and my
first version passed these tests. Review of the diff showed an
incompatible `identity` parameter type. I changed that type to
`Record<string, unknown>`.
- **After the type change:** `remote-warm-session-recovery.test.ts`
passed (23 tests) and `pnpm run typecheck` in `server` passed. I did not
run the 134 tests again, because this change affected only types.
- **Real data, read-only:** I ran the new function on 285 session
journals from a test deployment. The result changed for 2 journals.
These are the 2 stranded sessions, and each has only provider notices
after `run.terminal`.
- **Not run locally:** I did not run the full test suite. I stopped a
run of the wider native-runtime test files before it reported a result.
CI provides the full regression coverage.
- **Not verified:** no live run has reached `read_state` on a stranded
session after this fix. The tests use a fixture for that call.

## Risks

- **Stricter identity check:** checkpoint and quiescent recovery now
also require the journal's saved identity to match. Recovery reads this
identity from the same file, so only damaged state should fail this
check.
- **Wider goal snapshot rule:** these two paths now accept idle goal
snapshots after the terminal event. Warm handoff already used this rule.
- **Wider notice rule:** warm handoff and the settled projection check
also accept recoverable notices now. A notice does not start work, and
the checks for pending commands and unanswered tool input stay in place.
- **Longer error text:** code that compares the full
`plugin_checkpoint_recovery_unproven` message for equality would stop
matching. I found no other reference to this code in the repository.
- **Stranded sessions:** these sessions need no data repair. They can
still fail with `checkpoint_not_settled` if the sandbox checkpoint is
not clean.
- **Migrations:** this change has no migration.

> 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`), through Claude Code,
with tool use and code execution. A human directed the diagnosis and
approved the change.

## Checklist

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

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-11-03 16:37:39 -08:00
Devin FoleyandPaperclip 4e29f00139 fix(native): keep settled ACPX sessions across Runner upgrades and run scheduled resumes (#18879)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native Runner keeps one ACPX provider conversation across the
per-turn runs of a task. Each run cold-attaches the session that the
previous run settled.
> - The saved ACPX state binds the digest of the launch profile that
wrote it. Every deploy ships a new provider pack, so that digest
changes.
> - After a deploy, the next run of such a task fails `run.attach` with
`ACPX durable launch profile digest does not match runner startup`.
Every same-run retry fails the same way.
> - On one instance this happened seven times. Six tasks used their
retry budget and ended blocked, or in progress with no runner. In the
seventh, the scheduled same-run resume never ran at all: the task stayed
`in_progress` with nothing running, and its monitor did not fire.
> - This pull request re-binds a settled session to the upgraded launch
profile, types the remaining mismatches so the server stops retrying
them, and makes a scheduled remote resume survive the stale-lock sweep.
> - The benefit is that a task continues its conversation after a
deploy. When it cannot, it stops with a clear next action instead of
stalling.

## Linked Issues or Issue Description

Refs #18846, #18847, #18854 and #18860 (nearby native recovery changes).
Refs #15795, an open change to the same stale-lock sweep function. It
changes run teardown, not lock cleanup, so a second merge needs at most
a small rebase.

**What happened?**

A per-turn native task used provider `acpx` with Claude, driver
`acpx_runtime`, on a remote sandbox.

1. Run A succeeded and settled its provider session. The next run with
the same build cold-attached it.
2. The instance deployed a new build.
3. A board approval woke run B in a fresh sandbox. Run B failed after
about five minutes: `native_session_recovery_failed: Error: PRP command
run.attach failed: {...
"result":{"code":"command_execution_failed","message":"ACPX durable
launch profile digest does not match runner startup"}}`.
4. The coordinator recorded `retryable_failure`, attempt 1,
`native_session_interrupted`, recovery mode `exact_checkpoint_resume`,
recovery state `resuming_session`, and a retry 30 seconds later.
5. Two seconds after the failure, the stale-lock sweep cleared the
task's execution and checkout locks, because run B was `failed`.
6. Forty minutes later the attempt was still 1. The scheduled monitor
had not fired. The task stayed `in_progress` with nothing running.

**Expected behavior**

A session that the previous run settled continues after a deploy. If it
cannot continue, the run stops at once with a clear next action. A
scheduled resume either runs or fails visibly.

**Steps to reproduce**

1. Save a suspended ACPX session under one launch profile.
2. Change the sidecar bytes and the provider-pack authority at the same
paths.
3. Cold-attach the next run in a new runner. Restore fails with the
digest mismatch.
4. For the stall: leave a remote native run `failed` with a due
`retryable_failure` coordinator, run the stale-lock sweep, then run
restart recovery for that run. It returns `blocked:
issue_execution_lock_changed` and writes nothing.

The new Rust and PostgreSQL tests run these steps.

**Paperclip version or commit**

`master` at `b0c4e345267`.

**Deployment mode**

Native Runner, per-turn lifecycle, remote sandbox. The re-bind applies
to every ACPX agent with an ordinary startup.

## What Changed

### Root causes

- **Upgrade failure.** `AcpxLaunchProfile::canonical_digest` hashes the
provider-pack authority, the sidecar command and arguments, each
artifact path and SHA-256, and any candidate admission. For a remote
sandbox the authority is the provider-pack manifest digest, which covers
the Runner source revision and dist digest. So every deploy changes it.
`AcpxDurableState` stores only the digest, and `validate()` rejects any
difference. Only a Kimi candidate run binding had a carve-out.
- **Silent stall.** The failure write keeps the task locks for
`retryable_failure` and clears them only for `terminal_failure`. The
stale-lock sweep treats any `failed` run as cleanable, so it cleared
them. A remote retry goes through `claimNativeRestartRecoveries`. That
claim requires `issues.execution_run_id` to name the run, so it returned
`blocked: issue_execution_lock_changed` with no coordinator write and no
deadline. The live recovery queue then dropped the entry. The local
claim (`claimNativeSessionResumptions`) already reclaims a free lock, so
only remote runs stalled.
- **Monitor.** `noActiveNativeMonitorRun` holds a task's monitor while
any native run for it has a coordinator that is not `committed` or
`terminal_failure`. Run B's `retryable_failure` held it. That is by
design; it ends when the resume runs.

### Runner (Rust)

- `rebinds_settled_session` in `acpx_provider_backend.rs`. Restore
re-binds a saved session to the current launch profile only when all of
these are true:
- The lifecycle is `suspended` and an identity exists. There is no
active turn, pending runtime request, undelivered provider event, or
unconfirmed provider exit.
  - The startup has no candidate admission.
- The saved descriptor's sidecar command and arguments equal this
startup's.
- Any other digest mismatch keeps failing with the same message, plus
`classification: "provider_state_incompatible"` in the durable command
result. `DurableRunnerError` carries an optional closed classification
for this.
- A saved descriptor that names an agent release this build does not
qualify (ACPX version, agent server package or version, or command
digest) also gets the class. `validate_session()` and its message are
unchanged; restore only adds the class.

### Runner (TypeScript)

- `runnerRecoveryFailureClass` reads the classification from the
authenticated `runnerCommandResult`. The Codex app-server driver (which
also drives ACPX through runnerd) and `HarnessDriverBackend` pass it on
as `failureClass`.
- If replacement after a failed resume is not allowed,
`executeNativeSession` throws `NativeProviderStateIncompatibleError`
(message unchanged: `native_session_recovery_failed: ...`). If
replacement is allowed, the existing path opens a fresh session in the
same run, as before.

### Server

- `nativeSessionFailureSourceCode` maps that error class to
`native_provider_state_incompatible` with `instanceof`, not text. The
disposition is permanent: `terminal_failure` at once, the task blocks,
and the board owns the recovery action. Its next action says that a
Retry continues in a fresh provider session with the same workspace.
- `findNativeSessionResumeRun` also returns the barrier's `errorCode`.
`rebindNativeSessionCheckpoint` does not resume a source with
`native_provider_state_incompatible`. The existing fallback in
`buildNativeExecutionWithCheckpoint` then starts a fresh normalized
session with the full task bootstrap.
- `sweepStaleIssueLocks` keeps both locks while a referenced run's
coordinator owns a result-less `retryable_failure`, unless the task is
done or cancelled. Its update also requires that no referenced run has a
live status at the write, because a resume claim can return the run to
running after the status read.
- `claimNativeRestartRecoveries` reclaims a free execution lock for a
`retryable_failure` resume, like the local claim does, when the checkout
lock is free or held by this run and no other run for the task is
queued, running or scheduled. It records
`native.recovery.execution_reclaimed`. A lock held by another run still
blocks.
- Docs: `packages/paperclip-runner/docs/durable-recovery.md` and
`doc/execution-semantics.md`.

### Safety argument for the re-bind

What the binding protects, and what this change keeps:

- **Candidate admission** carries company, model, platform and expiry
authority. A candidate startup never re-binds. The Kimi run-binding
carve-out and all candidate rejections are unchanged.
- **The agent that loads the conversation** stays exact. `validate()`
runs on the saved descriptor before the digest check and requires this
build's qualified agent profile: ACPX version, agent server package and
version, and `commandDigest`. The saved identity must still match that
profile and the provider mode.
- **In-flight provider work** is never re-bound. A turn, runtime
request, undelivered event or unconfirmed exit keeps the strict binding,
so no old provider process or request continues under a new build. The
next launch only loads a settled conversation.
- **Launch shape.** A changed sidecar command path or arguments still
rejects. Only the content at the same paths may change.
- **Artifacts** are not trusted from the saved state. The current
profile comes from the trusted controller at startup, and session start
verifies every artifact's bytes and the restored provider identity, as
before.

What changes: a new provider-pack authority, or new sidecar or Node
bytes, at the same paths no longer invalidates a settled conversation.
The saved digest cannot show which part changed, so the rule does not
depend on it. I did not add state fields to tell authority changes from
artifact changes, because the rule would treat both the same.

One existing assertion changes.
`released_kimi_cold_restore_rejects_candidate_injection_and_changed_full_profile`
expected a settled ordinary Kimi session to reject a changed authority,
artifact digest, or saved digest. Those three cases moved to a new
positive test. Command, arguments, every candidate grant field, a
missing profile and run binding still reject.

### How a task in the reported state recovers after this deploys

The task has old-digest saved state, a `retryable_failure` coordinator
at attempt 1, and cleared locks.

1. Startup recovery, and the live recovery queue, run restart recovery
for the due run. It reclaims the free execution lock and claims the run.
The resume runs now.
2. Run B's failed `run.attach` is a durable outcome of B's exact run
authority. If the resume replays that outcome instead of executing it
again, it fails with the old, untyped text. The remaining attempts then
fail the same way, and the run ends `terminal_failure` with
`native_session_retry_exhausted`. The task blocks with a board recovery
action. This takes about a minute.
3. If the task blocks, press **Retry** once. Retry selects run B as the
source (it has process identity and controller evidence, so the #18860
skip does not apply) and rebinds B's checkpoint, which holds A's
provider session. The new run cold-attaches under the new Runner, and
restore re-binds the old digest because the session is suspended and
quiescent. The conversation continues. Run B's failed attach never wrote
the state file, so the state is still A's settled state.
4. Tasks that already exhausted their retries (blocked, or in progress
after `native_session_retry_exhausted`) need the same single Retry.
Their earlier attempts ran under the old Runner.

Wait for the task to show blocked before pressing Retry. A Retry pressed
while the resume is running joins that run.

## Verification

- Rust, failing first:
`settled_session_rebinds_to_the_upgraded_runner_launch_profile` failed
on `master` with the production message.
`unsettled_or_relocated_session_keeps_the_strict_launch_binding` failed
on the missing classification. Both pass now.
- `cargo test -p paperclip-runner-core --no-fail-fast`: all 41 test
binaries pass. The library has 471 passed and 1 ignored.
- `cargo fmt --check`, and `pnpm --filter @paperclipai/paperclip-runner
typecheck` (TypeScript, generated contracts, `cargo check`): pass.
- Server, failing first: `native-scheduled-resume-lock.test.ts`
(PostgreSQL). On `master` the sweep cleared the locks and restart
recovery returned `issue_execution_lock_changed`. Both negative controls
(another run's lock, a queued run) already passed. All four tests pass
now.
- `pnpm --filter @paperclipai/server typecheck`: pass.
- Runner vitest (`src/native-session-runtime.test.ts`, `src/backends`,
`src/drivers/codex`, `src/drivers/provider-failure.test.ts`,
`src/live/runnerd-codex-transport*.test.ts`,
`src/control-plane/acpx-controller-recovery.test.ts`): 31 files, 1,011
tests pass. New: the typed error when same-session recovery is required,
and the recovery class reader and pass-through.
- Server vitest: the native executor, status corpus and provider-pack
admission suites (1,268 tests) pass. New: the classifier makes the typed
error permanent, and the same text stays retryable. Checkpoint selection
returns `errorCode`, and an incompatible source starts a fresh session
with the same workspace (unit and PostgreSQL).
- Server vitest over the recovery area: stale-lock sweep, native
resumption, process recovery, failed-workspace recovery, restart
recovery (integration, diskless, preparation), controller recovery,
startup retry, startup cleanup, recovered-owner follow-up, session
resume, runtime selection and recovery deadlines. 926 passed. Four
`heartbeat-process-recovery` tests failed in that loaded run. Three pass
when I run them alone. `redacts opaque environment-bound credentials
from Sentry diagnostics: result` fails the same way on unmodified
`master` on this host (a local environment value is redacted).
- Review round 1: a new sweep test returns the run to running before the
clear. It cleared the locks without the write recheck and keeps them
with it. The strict Rust restore test covers an older agent server
release and an older ACPX release. The ACPX backend and durable runner
unit tests, the lock, sweep, resume projection and controller recovery
suites, `cargo fmt --check` and the server typecheck pass. In the second
full cargo run, the two `stdio_mcp` relay tests timed out waiting 5 s
for a child exit on this loaded host. They do not use the changed code
and passed in the first full run.
- After the rebase onto the current `master`: the new lock test,
failed-workspace recovery, stale-lock sweep and session resume suites
pass (189 tests). Server typecheck, `pnpm check:module-boundaries` and
`git diff --check` pass.
- CI on `929ef4508d2`: in the first run, `isolated-repository-rehome`
hit its 15 s test timeout, `idle-completed-history` could not reach its
fixture runtime service, and e2e shard 3 was cancelled by a runner
shutdown. A rerun of the failed jobs passed. All 55 checks pass.
- Not verified: a live remote sandbox deploy, and whether run B's
same-run resume replays its recorded `run.attach` failure or executes it
again. The Retry path is tested at source selection and checkpoint
rebind (server) and at cold attach (Runner). It is not tested end to end
through a sandbox.

## Risks

- A settled session now continues under new sidecar bytes. If a sidecar
change made an old ACPX record unreadable, session start fails as any
failed resume does. It does not corrupt state, and the run follows its
normal recovery policy.
- The sweep now keeps locks for a failed native run with a scheduled
resume. If that resume can never be claimed (for example the agent stays
paused), the task keeps the lock until the coordinator settles or the
task closes. Before, the sweep cleared it, and the resume still never
ran.
- If another run holds the task's lock, a scheduled remote resume still
stays blocked and the coordinator stays `retryable_failure`. This change
does not settle that case. It needs a separate decision about
superseding the resume.
- `native_provider_state_incompatible` is a new failure code. No UI or
migration change is needed. Older servers never see it.
- No schema migration.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`) through Claude Code, with tool use
(shell, file edits, test runs). The runtime does not expose the context
window or reasoning configuration.

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-03 16:30:58 -08:00
Devin FoleyandPaperclip 909d86b59a fix(native): retry runs that fail before provider work (#18854)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native runs start the provider through Runner. `session.open` starts
the provider session before any prompt.
> - When `session.open` fails, the server records the generic
`native_session_interrupted`. Recovery evidence then reports
`ambiguous_state`, so the first failure blocks the task with no
automatic retry.
> - The failure is often temporary. On one instance an agent's provider
account hit its usage limit. For about 14 hours every later run failed
at `session.open`, and 47 tasks stayed blocked. Outside limit windows,
the same error also occurs once and a later retry succeeds.
> - The server already has a bounded same-run bootstrap retry for
failures that are proven to come before provider work. It did not
recognize this failure, and it did not consider a known provider limit.
> - This pull request classifies a definite `session.open` failure as a
pre-session failure. Bootstrap retries back off and wait out a known
provider limit. The task is blocked only after the shared budget is
used, with a clear reason.
> - The benefit is that a temporary startup failure or a provider limit
no longer strands tasks on the first failure, and every existing replay
guard stays in force.

## Linked Issues or Issue Description

Refs #18847. That pull request keeps the provider cause on
`session.open` rejections and names typed categories (for example
`classification=provider_rate_limit`). This change does not depend on
it. With #18847, a typed limit at `session.open` uses the same cooldown
path, because the server reads the cause with
`providerFailureFromError`. Refs #18622, which added the
`ProviderFailure` cause. Refs #13993, an open change about legacy
adapter startup backoff; it does not touch the native recovery path.

**What happened?**

A native run (`paperclip_runner`) failed at `session.open` before any
prompt: `ACPX sidecar command session.open was rejected
(retryable=false, classification=session_ensure_failed)`. The server
recorded `native_session_interrupted`. Recovery evidence had no
checkpoint and no provider event, but the cause was not in the
pre-session list, so the mode was `ambiguous_state`. The coordinator
went to `terminal_failure`, a board-owned `active_run_watchdog` action
was created, and the task was set to `blocked` on the first failure.
During a provider usage limit, every later run of the agent failed the
same way, so each task was blocked.

**Expected behavior**

A run that provably failed before provider work retries automatically,
with backoff and a bounded budget. If the agent has a known provider
limit, the retry waits for the cooldown. The task is blocked only when
the budget is used, and the recovery action explains why. A run where
provider work may have started is never replayed.

**Steps to reproduce**

1. Make Runner reject `session.open` for a native run (for example a
provider at its usage limit).
2. Observe the coordinator: `terminal_failure`, `recoveryMode:
"ambiguous_state"`, `providerSessionEstablished: false`. The task is
`blocked`.
3. The new `native-startup-retry.integration.test.ts` reproduces this
with a real database and the executor.

**Paperclip version or commit**

`master` at `5877d751c0e`.

**Deployment mode**

Native Runner with an ACPX Claude provider. The fix is in the server and
is not specific to one deployment mode.

## What Changed

- `nativeSessionFailureSourceCode` returns a new cause,
`provider_session_open_rejected`, when Runner reports that
`session.open` failed or was rejected. Those outcomes are definite, and
the command runs before any prompt. An `indeterminate` outcome stays
`native_session_interrupted`. Earlier, more specific causes (for example
a missing runtime package) keep their classification.
- `nativeProviderRecoveryEvidence` treats the new cause as pre-session.
The durable evidence still decides. A bootstrap retry happens only with
no provider session, no provider event and no checkpoint. Otherwise the
mode stays `ambiguous_state` and the task is blocked as before.
- New module `native-runtime/native-startup-retry.ts` plans each
bootstrap retry. It is called only for `bootstrap_retry` mode.
- Backoff: the existing `infrastructureRetryDelayMs` (about 30 s, then
about 60 s, plus or minus 20 %). The jitter derives from the run and
attempt, and it is persisted with `nextAttemptAt`.
- Limit cooldown: evidence is a `rate_limit` or `quota_exhausted`
provider failure on the current failure (the #18847 shape), or on an
agent run that started in the last 24 hours. A
`native_provider_usage_limit` or `provider_quota` error code also
counts. The cooldown is the provider's `retryAt` (or `retryNotBefore`),
or one hour after the failure when no reset time is known. A later
successful run of the agent clears older evidence. The cooldown is
capped at 24 hours. The retry runs after the cooldown plus the backoff
delay.
- A failed evidence read is logged and falls back to plain backoff, so
it cannot stop the failure from settling.
- The budget does not change: three provider attempts, shared with
checkpoint resumes and replacements. When it is used, the coordinator
records `native_session_retry_exhausted`. The task is blocked, and the
board-owned action says: "Provider startup failed on 3 attempts before
any provider work began. Automatic retries are stopped." When a limit is
known, the message includes the expected reset time.
- While a retry waits, the coordinator detail, the recovery action
evidence and the scheduled wake record `providerCooldownAt`. The next
action names the reset time, and the task projection shows "Retry
scheduled".
- `PROVIDER_LIMIT_DEFAULT_COOLDOWN_MS` (one hour) moves to
`system-recovery-state.ts`. The legacy provider-quota recovery default
now refers to it. The value does not change.
- A chat retry of an exhausted `provider_session_open_rejected` run is
accepted like the other pre-provider causes. The existing proofs still
apply.
- `doc/execution-semantics.md` describes the backoff, the cooldown and
the exhausted state.

## Verification

- Red: before the fix, the first new integration case failed. The
coordinator was `terminal_failure` with `recoveryMode:
"ambiguous_state"`, `originalFailureCode: "native_session_interrupted"`
and `providerSessionEstablished: false`, and the task was `blocked`.
Four more cases failed the same way. The two never-retry cases passed
before and after the fix.
- Green: `npx vitest run
src/services/native-runtime/native-startup-retry.integration.test.ts` in
`server` (embedded PostgreSQL, the real executor, and the real resume
claim). 8 cases pass:
- a rejected `session.open` is retried with backoff (attempt 1 in 24 to
36 s, attempt 2 in 48 to 72 s), and the task stays `in_progress` with an
agent-owned action and a `resume_native_run` wake;
- after three attempts the task is blocked, the action is board-owned
with the clear reason, and the resume claim refuses another attempt;
- a recent limit failure holds the retry until one hour after that
failure, and the claim refuses it before the cooldown;
- a provider reset time from an earlier run, or from the startup failure
itself, sets the cooldown;
  - a distant reset time holds the retry for at most one day;
- a later success, another agent's limit, and an expired limit do not
delay the retry;
- a durable provider event, or an indeterminate `session.open` outcome,
still blocks the task with `ambiguous_state` and no retry.
- `native-session-executor.test.ts`: all 1,087 tests pass, including a
new classification case for `failed`, `rejected`, `indeterminate` and a
different command.
- `chat-channels.integration.test.ts -t "requires terminal-safe native
ownership for an exact chat retry"`: 23 pass, including the new
`session_open` case. That case fails without the list change.
- Related suites pass: native session resumption, native provider
failure, capacity and overload integration tests, provider failure
classification, native status arbiter corpus, native restart recovery.
- `npx tsc --noEmit -p server` passes. `git diff --check` passes.
- [GitHub
CI](https://github.com/paperclipai/paperclip/actions/runs/41713988964)
is green on head `32561aa335d`. This includes `ci / verify`, `ci / e2e`,
typecheck, build, all server, chat and workspace groups, all serialized
server suites, and all Runner checks, which run the native-runtime
server tests. In the first attempt, eleven jobs stopped because their
runners received a shutdown signal; no test reported a failure. A rerun
of those jobs passed.
- A local run of the whole `server/src/services/native-runtime`
directory was stopped before completion because it runs serially for a
long time. CI ran those files.
- Runtime dependency: the new work is one indexed query (`company_id`,
`agent_id`, `started_at`) with `LIMIT 20`. It runs only when a bootstrap
retry is planned or exhausted, not on the normal path. No new timer or
polling: the wait uses the existing persisted `nextAttemptAt` deadline
and the existing resume dispatch.
- Not verified: a live provider account at its usage limit, and a live
remote sandbox that waits through a full cooldown. The full repository
suite was not run locally; CI ran it.

## Risks

- A task that waits for a cooldown stays `in_progress` with its
execution lock, for up to one day. This is the existing same-run retry
state. Stop, reassignment, pause and an explicit Retry keep their
current behavior through the existing claim checks.
- During the wait the failed run keeps its environment lease in the same
way as any same-run retry. A remote lease with an unexported prepared
workspace is retained by the orphaned-lease sweep until the retry runs.
Other active leases of the failed run are released after the usual five
minutes, and the retry acquires a new one.
- A `rate_limit` failure without a reset time (for example a short HTTP
429) delays only startup-failure retries of that agent, by up to one
hour. Normal runs are not delayed, and a later success clears the
evidence.
- A startup failure that needs user action (for example a sign-in
failure) now uses two automatic retries, about 90 seconds in total,
before the task is blocked. Before, it was blocked at once with an
ambiguous reason.
- The agent's other queued runs are not held during a cooldown. Each one
fails at `session.open` without provider work and then waits for the
same cooldown. An agent-wide admission hold has no clean extension point
in the current queue, so it is not part of this change.
- Re-arming consumed scheduled monitors is out of scope. This change
makes it matter less: a run that consumed a monitor wake now retries
itself on the same run. A monitor that a run consumed before its retry
budget was used stays consumed.
- No schema or API change. The new error code appears on runs and in run
error codes.

> 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`) through Claude Code, with
extended reasoning and tool use (shell, file editing, test execution).
The host does not expose the 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-03 13:24:59 -08:00
Devin FoleyandPaperclip 80c308f872 fix(runner): admit follow-ups after recovered per-turn owners (#18846)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native Runner sessions keep one conversation across task turns.
> - After a per-turn sandbox turn, the controller saves a Runner
suspension receipt or a failover backup.
> - A full sandbox disk can stop that save after the turn is complete.
> - Recovery then commits the result, but the controller has no proof
that the Runner suspended.
> - Each follow-up rebuilds the old controller journal from the database
and rejects it again.
> - This pull request lets admission retire that old authority when the
prior owner is provably finished.
> - The benefit is that the task continues with its saved workspace and
does not stay blocked.

## Linked Issues or Issue Description

Refs #18705 (checkpoint save on a full sandbox disk). Refs #18829,
#18718 and #18343 (related admission and controller recovery). Refs
#18811 (a related warm continuation wait).

**What happened?**

A per-turn native run in a remote sandbox finished its turn. The
post-turn `harness_state.backup.persist` and
`session.checkpoint.persist` steps failed because the sandbox disk was
full. The controller got no Runner suspension receipt and no failover
backup. Workspace recovery later saved the files. The run committed its
result and ended as `succeeded`. A board comment then started a
follow-up in a new sandbox. Admission failed after about two minutes
with `runner_state_identity_mismatch:
prior_owner_terminal_state_indeterminate`. Execution recovery then
blocked the task.

The cause is in durable-root admission.
`restorePerTurnRunnerdController` rebuilds the prior controller journal
from the database checkpoint. `verifyPriorRunnerdStateForSessionScope`
finds a terminal owner in the same scope with no suspended lifecycle and
no retained warm owner. It returns `terminal_state_indeterminate`.
Admission quarantines the root and fails. The database checkpoint does
not change, so every later admission rebuilds the same journal and fails
again.

A board Retry after that failed follow-up does not repeat the same
error. Runtime selection picks the failed follow-up as the resume
source, because it carries an inherited provider checkpoint. Controller
recovery then fails with
`native_controller_recovery_unsupported:metadata_format`. The task stays
blocked. This pull request prevents that failed follow-up. It does not
change source selection (see Risks).

**Expected behavior**

When the prior owner is provably finished, the follow-up must start. It
must keep the saved workspace. It must not start while the prior owner
can still be alive, while terminal delivery or provider cleanup is
unsettled, or while quarantine holds evidence from a different owner.

**Steps to reproduce**

1. Complete a per-turn remote sandbox run. Commit its result through
recovery and release its controller lease.
2. Keep its database controller checkpoint and recovery pointer. Do not
save a suspension receipt or a failover backup.
3. Admit a follow-up in a new sandbox with no local controller
projection.
4. Admission rebuilds the old journal, quarantines it, and fails with
the error above.
5. Admit the same run again. It rebuilds the journal and fails the same
way.

The new regression test runs these steps against PostgreSQL.

**Paperclip version or commit**

Reproduced on `e0276e0166`.

**Deployment mode**

Native Runner with a per-turn lifecycle, the built-in remote sandbox
transport, and the ACPX provider. The admission path is the same for
other per-turn sandbox providers.

## What Changed

- Add `retireFinishedPerTurnRunnerdOwner` to durable-root admission in
`native-session-executor.ts`. It runs only when the existing verifier
returns `terminal_state_indeterminate`, and only without a restart claim
or a retained warm owner.
- The function requires all of these. Otherwise the existing quarantine
and rejection apply.
- Per-turn lifecycle on the built-in remote sandbox transport, with a
`created` or `replacement` sandbox. A resumed sandbox, a warm session,
and a plugin task keep their existing paths.
- No local Runner lifecycle evidence for the old owner. The session root
contains only known authority entries.
- A journal for that owner with its terminal delivered and only idle
goal snapshots after it. All commands are completed. Each tool input has
a completed result. No warm transition or staged attachment exists.
- A terminal prior run in the same task. Its coordinator is `committed`
with a result, has no lease owner, and has no recovery state.
- A stopped prior controller, from `evaluateNativeControllerTakeover`
with `requireStoppedController`. A durable controller is fenced by its
released database lease, as in remote warm recovery.
- Each quarantined root for the scope passes the same journal and local
Runner evidence checks for the same owner. A retry rebuilds only the
database journal, so quarantined Runner evidence must stay decisive.
- When the proof holds, the function reads the journal again, moves the
old authority into `continuity-breaks/<time>-<uuid>` in the session
root, and logs one line. The follow-up then starts a new Runner
authority. The provider session follows the existing resume-or-replace
policy.
- Share the list of continuity archive entries with provider replacement
(`archiveContinuityState`). Provider replacement behavior does not
change.
- Add `native-recovered-owner-followup.test.ts`. It uses real
PostgreSQL, a real encrypted controller checkpoint, and the production
source selector and checkpoint rebinder.
- Document the rule in `doc/execution-semantics.md` and the test in
`doc/testing-native-remote-continuity.md`.

### Design choice

I considered two approaches:

1. Start a new Runner authority for the scope at admission (this pull
request).
2. Make the recovery path that committed the prior run also settle its
Runner and controller projection.

I chose the first approach for these reasons:

- The provider history stayed in the unreachable sandbox. A settled
projection cannot make it resumable. Admission would pass, but a
replacement sandbox would then fail on the missing backup, and a new
sandbox would only try to resume a session that is gone.
- A settled receipt written by recovery would claim a suspension that
the Runner never wrote. Other checks rely on that receipt as Runner
evidence.
- Several paths can commit a result. Only runs that commit after a
settle-at-commit fix would benefit. The admission fix also unblocks
tasks that were committed before it.
- Admission already owns this decision. It already has the scope claim,
the journal, and the database rows.

### Safety argument

- **No live prior owner.** The coordinator has no lease owner, and the
controller must be proven stopped. The follow-up runs in a different
sandbox than the old Runner. A resumed sandbox keeps the existing path,
which reads the old Runner's own lifecycle there.
- **No unsettled terminal delivery or effects.** The controller
acknowledged the terminal and completed every command, including the
suspend command. Every tool input has an answer, and the coordinator
committed the result. The unreadable Runner outbox can then hold only
post-terminal telemetry.
- **No unsettled cleanup.** Any local Runner receipt, including an
unsuspended one, keeps the hold. A coordinator recovery state keeps the
hold.
- **No ambiguous quarantine.** A quarantined root from a different owner
keeps the hold. A quarantined root with Runner lifecycle evidence,
unknown entries, or an unsettled journal also keeps the hold.
- **No lost evidence.** The move keeps the old authority in the session
root. Existing retention expires it like other continuity breaks. A
journal change during the database reads cancels the move.

## Verification

- `cd server && npx vitest run
src/services/native-runtime/native-recovered-owner-followup.test.ts`: 12
tests pass.
- Before the change, the four admission tests failed with the exact
error above. This includes a re-admission that rebuilt the journal from
the database and failed again.
- After the change, `created` and `replacement` sandboxes are admitted.
A re-admission is admitted. A task with an earlier quarantined copy of
the same owner is admitted.
- Eight controls keep the hold: uncommitted result, live controller
lease, unexpired controller lease, recovery state, pending command,
unanswered tool input, a different owner in quarantine, and an
unsuspended Runner receipt in quarantine after a retry rebuilds the
root.
- I disabled each new guard in turn. Each change made its matching
control fail.
- An exploratory run of the same fixture before the change answered the
Retry question above. The second follow-up selected the failed follow-up
as its source and failed with
`native_controller_recovery_unsupported:metadata_format`.
- `cd server && npx vitest run
src/services/native-runtime/native-session-executor.test.ts`: all 1,087
tests pass.
- Related suites pass: `task-workspace-binding`,
`native-controller-recovery` (unit and integration),
`native-harness-backup-expiration`, `native-session-resume`, and the
restart and diskless integration suites. Their container and Runner
binary cases skip locally.
- `node scripts/typecheck-low-memory.mjs`, `pnpm
check:module-boundaries`, and `git diff --check` pass.
- CI passes at `879bb6a5c7`. Two attempts lost jobs when CI runners
received a shutdown signal; the third attempt passed every job.
- I did not run the full `pnpm test:run`, `pnpm build`, or the opt-in
Linux Runner container cases locally. CI runs them.
- I did not test a live sandbox provider or a live model. I could not
inspect the original checkpoint contents. If a stranded run's journal
does not show a delivered terminal and completed commands, its follow-up
stays held.

## Risks

- A follow-up after this path cannot use the lost provider history. The
provider session then follows the existing resume-or-replace policy, and
the task context comes from Paperclip. A checkpoint that requires the
same session, such as an unfinished goal, still fails at provider
recovery.
- Post-terminal Runner telemetry in the lost outbox, such as a late
usage report, stays lost. It cannot carry a workspace effect.
- A task that already has a failed follow-up from this error can still
be blocked on a board Retry. Source selection picks that failed run and
fails with `native_controller_recovery_unsupported:metadata_format`.
This pull request does not change source selection. A same-run retry of
the failed follow-up is admitted.
- The change is limited to per-turn runs on the built-in sandbox
transport. Warm sessions, plugin tasks, resumed sandboxes, and restart
recovery do not change.
- No migration or configuration change is required.

## Model Used

- Anthropic Claude Opus 5.5 (model ID `claude-opus-5-5`) in Claude Code,
with extended thinking, repository tools, shell and code execution, and
local test runs. The runtime does not expose the 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-03 13:24:16 -08:00
DottaandPaperclip e491b23eb5 fix(runner): retire sandbox suspension receipts after admission (#18829)
## Thinking Path

> - Paperclip helps people manage AI agents and their work.
> - Native Runner sessions preserve a conversation across task turns.
> - Remote suspension records a local receipt for the stopped Runner
identity.
> - The built-in sandbox path now writes the same receipt as plugin
tasks.
> - Admission cleared that receipt only for plugin tasks.
> - A later warm turn could reject a clean journal because the old
receipt named an earlier run.
> - This pull request clears the receipt after successful admission for
both sandbox paths.

## Linked Issues or Issue Description

Refs #18343 and #18554 for the shared suspension receipt producer. Refs
#18718 for the related controller projection refresh.

**What happened?**

After a suspended built-in sandbox session resumed, its next warm turn
could fail with `runner_state_identity_mismatch:
prior_owner_terminal_state_indeterminate`. Its journal named the
completed resumed run. Its local suspension receipt still named the
older run.

**Expected behavior**

Successful admission must retire the prior suspension receipt. The next
compatible warm turn must reuse the admitted session and provider
history once. A failed admission must retain recovery evidence.

**Steps to reproduce**

1. Complete and suspend remote turn A.
2. Resume turn B through the built-in sandbox transport.
3. Keep the controller warm, with full Runner state owned by the
sandbox.
4. Admit turn C. The old local receipt conflicts with B's journal
identity.

**Paperclip version or commit**

Reproduced in the executor regression on base `367f29478d`.

**Deployment mode**

Native Runner with the built-in remote sandbox transport and a warm
lifecycle.

## What Changed

- Clear the local suspension receipt after a remote sandbox session
passes provider recovery and durable admission. Apply this to built-in
runners and plugin tasks.
- Extend the existing three-turn warm admission regression to cover a
suspended receipt and remote-only Runner state.
- Keep an active-owner control that rejects admission and retains its
receipt. Keep the existing foreign-owner, retirement, configuration,
plugin, and unchanged-session controls.
- Add a real Linux Runner continuity case that suspends A, resumes B,
and keeps B warm for C. Register the test fixture's owned local API
listener.
- Document receipt ownership and the new continuity case.

## Verification

- The new behavioral regression failed before the production change.
Turn C raised the exact identity mismatch above. The active-owner
control passed and retained its receipt.
- All 1,087 native executor tests pass after the fix.
- Both opt-in Linux process cases pass at the final commit: ordinary
warm reuse and suspension followed by warm reuse. They use PostgreSQL,
TLS, current-source Rust binaries, and a deterministic Codex fixture.
Each preserves one conversation, starts exactly three turns, and reuses
the warm Runner and provider on turn C.
- Full repository typecheck passes with `node
scripts/typecheck-low-memory.mjs`. Full production build passes with
`pnpm build`.
- All current-head CI checks pass at `a95a6fdfe8`, including the full
test shards, browser shards, Rust checks, build, and typecheck. The
policy job needed one retry after repository checkout timed out before
checks ran.
- Greptile and Ponytail each give 5/5 at the final head. There are no
open review threads or findings.
- The local full `pnpm test:run` attempt passed 351 cases in its first
chat shard. The session interruption stopped it during the second shard,
so it has no complete local verdict. The complete CI test run passes.
- Module boundary checks and `git diff --check` pass.

## Risks

- A receipt removed before admission could discard recovery evidence.
Removal stays in the successful `onSession` callback. The negative
admission control checks retention.
- A stale owner must not gain authority. Journal, run, lease, and scope
checks remain in place before the callback.
- Historical contradictory or quarantined state still requires the
existing verified recovery path. This change does not delete those
records.
- No migration or provider configuration change is required.

## Model Used

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

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` 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-03 13:42:21 -06:00
DottaandPaperclip 27d35314be feat: connect experimental Cursor Cloud to native Runner (#18069)
## Thinking Path

> - Paperclip helps people manage AI agents at work.
> - Native Runner owns execution and task authority.
> - Operators need an explicit native Cursor Cloud choice.
> - Tools/files need company, task, session and run scope.
> - This connects setup and lifecycle to Runner Core.
> - Legacy agents retain their selected runtime/history.

## Linked Issues or Issue Description

Depends on #18068. Managed-only API placement and API-owned Stop
containment are in #18310. Refs #5664, #11803, #17299, and #17471. No
duplicate native integration was found.

## What Changed

- Add explicit native setup, Test, saved settings and dispatch with
no-prompt account/catalog/callback checks.
- Scope MCP to company, agent, task, session and run through
authenticated HTTPS. Register verified artifacts/work products; preserve
unpriced accounting, completion and budgets.
- Connect Question/Plan continuation, cold handoff and admitted fresh
sessions; recheck waiting admission under the task lock.
- Present audited Stop intent while preserving physical containment
proof; bound typed Stop metadata. Preserve title edits through
preference/navigation readiness.
- Select one live/history representation per run to prevent duplicate
settled cards. Integrate upstream task/read-state/retry/schema contracts
and repair two test fixtures.

## Verification

Core #18068 merged normally as `3910352c923a`. Product head
`5a60f541c982` has passing repository CI/build/browser checks, including
required `ci / verify` and `ci / e2e`, plus a fresh exact-head Greptile
5/5 review with zero unresolved threads. Two non-required external app
checks remain pending: Paperclip Review is queued waiting for its
assigned agent; Superagent Security Scan is reviewing the diff. These
are not claimed completed. The normal merge endpoint enforces the
repository rules; no policy or status override is used. The dependent
Top PR is addressing its fresh deferred-Stop review finding.

Product `5a60f541c982` passed 270 UI and 20 selected
Cloud/native/disk-full tests, UI/server typechecks, tokens and maps.
Top’s clean normal merge passed tokens/maps/diff checks; its matrix is
pending. Earlier receipts retain Core’s 47 Rust, 4 compiled-native
retirement and 19 settlement controls, normal Runner build, and runtime
9af build/recursive typecheck. Reproduced loading/shutdown CI fixture
failures passed 7/7 browser and 121/121 unit tests, including a
broken-unsubscribe negative control. No timeout/oracle was relaxed.

The actual-parent restack adopts 75 mainline paths, including
schema/permission and generic runtime changes, beyond the prior two
fixture repairs and client projection fix. The full difference from
hosted `9af1a5de42a2` is therefore larger than four files; backend
source also differs. The merged config-read checks match Core391, and
the Cursor placement/runtime/relay/selection modules match frozen9af;
selected affected regressions pass. Paid proof keeps its actual 9af
source/image identity. The separately tested live/history card fix is
not claimed deployed on that host.

On 9af, original saved settings survived reopening and fresh no-prompt
Test authenticated catalog/model/binding; no new Save is claimed. Fresh
Linux admission passed. With `claude-sonnet-4-5`, Artifact downloaded
exact 27 bytes and matched 121 canonical identities in complete
S3-served history. First Stop’s strict quiet FAILED on a 10.373-second
gap despite independent cancellation/no late artifact. One targeted
unchanged-sampler retry retained all 24 samples: 35.076 seconds quiet /
maximum gap 4.584 seconds, 67 ordered canonical identities, and separate
CANCELLED/artifact-absent status after the original 180-second deadline.
The pre-reload duplicate settled card is disclosed. Exact-ID cleanup for
the three new provider agents is awaiting authorization, so full grades
and the fresh Artifact’s post-cleanup download remain blocked. A
historical deleted-provider artifact was newly downloaded on 9af;
original F226 Question/Plan/reconnect and cleanup proof keep their
identities.

Retained failures: stale fixture 132/133; E8 48/135 with 87 reset-hook
failures; Mac 17 SQL failures/skips; earlier
title/startup/custody/Stop/deletion failures with unknown causes/bounded
recoveries; `dea023` Stop’s warning/50 duplicate identities; pre-repair
ACK and original Core CI failures. WSP’s 251-assertion, 7/8-suite
attempt failed physical PostgreSQL teardown; the unchanged wrapper rerun
passed 128. Retry alone establishes no cause or full local-suite pass.
All paid attempts remain recorded; vendor dollar cost is UNKNOWN.

## Risks

Experimental: Cursor independently acknowledges neither the effective
model nor instruction digest before the first prompt. Explicit values
are submitted and checked through behavior; unpriced provider dollar
cost stays UNKNOWN.

Callbacks need authenticated reachable HTTPS. Scope, revocation,
budgets, completion and cleanup remain authoritative. Operator Stop
intent is separate from physical containment. Saved legacy agents retain
their selected runtime. Prior shared title-save/deletion failures remain
disclosed.

## Model Used

OpenAI Codex, GPT-6 family, assisted with reasoning, repository tools,
code execution, browser verification, and delegated code review. The
serving model’s exact API ID and context window are not exposed. No
value is inferred from the model catalog.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used and the limits of available model
metadata
- [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 linked public issues or described the issue in this PR
- [x] I have not referenced internal or instance-local Paperclip issues
or links
- [x] My branch describes the change and contains no internal ticket ID
- [x] I have run the scoped local tests reported above; pending gates
are stated separately
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation
- [x] I have considered and documented the risks
- [x] All required CI gates pass on the current head
- [x] Fresh Greptile is 5/5 with no unresolved findings on the current
head
- [x] I will address all reviewer comments before requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-03 09:01:39 -06:00
DottaandPaperclip 3910352c92 feat(runner): add experimental Cursor Cloud provider (#18068)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Runner gives each provider durable execution, tool, and completion
contracts.
> - Cursor Cloud currently has a direct adapter outside that native
contract.
> - Cloud work needs exact ownership across HTTP, SSE, Stop, and
recovery.
> - This PR adds a qualified Cloud API driver and verified proxy to
Runner.
> - Product setup and hosted placement remain separate dependent
changes.

## Linked Issues or Issue Description

Refs #5664, #11803, #17299, and #17471. Product integration is #18069.
Managed-only API placement is #18310. The related direct-adapter
model/transcript work is not replaced. No duplicate native Cursor Cloud
integration was found.

## What Changed

- Add native `cursor_cloud` / `cursor_cloud_api` with explicit
model/repository validation, bounded HTTP/SSE, durable per-turn
checkpoints and a qualified Node proxy launched through the Rust facade.
- Commit terminal custody before publication/release; coalesce
consumers, preserve ambiguous create/Stop/recovery fences, reject late
tools, and distinguish definitely-unsent oversized requests from
ambiguous responses.
- Support scoped cold attachment, current authority, reconnect and
verified artifact staging. Keep unsupported warm/external and Windows
paths closed.
- Preserve early source cursors and verify immutable legacy prefixes
before suffix replay, including supported compaction/masking. Optional
proof publication failure falls back to complete cold verification.
- Preserve canonical records at the durable native producer with
JSON-safe privacy and the existing 8MiB bound; drain admitted successor
flushes. Reconcile upstream semantic/pack/schema/lifecycle changes and
retain S3 and legacy CLI checks.

## Verification

Core head `37c442ec8dd4`: all 66 check contexts are terminal
successful/neutral/skipped, including required `ci / verify` and `ci /
e2e`; fresh exact-head Greptile is 5/5 with zero unresolved threads.
Dependent Product `81e828d20a77` and Top `dd2959689627` include the
later client handoff correction and are running their own fresh gates.

Core: 47/47 Rust, 4/4 compiled-native retirement, 19/19 settlement and
normal Runner build passed. Runtime Top `9af1a5de42a2`: build and
recursive typecheck passed. Earlier source-scoped evidence includes 398
focused tests, five relay tests and token/type gates. The 251-assertion,
7/8-suite run failed physical PostgreSQL teardown; the unchanged stable
WSP suite passed 128 through the documented wrapper.

The downstream fixture corrections passed 7/7 browser and 121/121
startup tests without timeout/oracle relaxation; a broken-unsubscribe
control fails. Hosted runtime `9af1a5de42a2` has a passing normal build
and recursive typecheck. Latest dependent heads additionally contain a
pure client fix for stale live/history overlap, with 48 focused tests,
UI typecheck and token gates passing. Its new client is not deployed in
the frozen hosted image; server/native/API/provider source is unchanged
by these four downstream files.

Fresh hosted Linux admission on `9af1a5de42a2` retired its owned
runner/proxy/socket with zero prompts. The saved configuration survived
reopening, and Test authenticated the bound key/catalog/selected model
without a prompt. Artifact with `claude-sonnet-4-5` completed,
downloaded the exact 27 bytes, matched 121 canonical identities against
complete logs and independently reached FINISHED. The active Stop retry
was clicked within its immutable boundary, produced one canonical
interrupted turn and acknowledged run-only cancellation, retained a
valid 35.076s quiet window (maximum gap 4.584s), and independently
remained CANCELLED/artifact-absent beyond the original 180-second
command deadline. All provider-observer credential callbacks, DB clients
and loaders closed; accounting is settled and explicitly unpriced. The
first fresh Stop retains its failed 10.373s sampling gap and every
sample.

On the upgraded9af server, the historical F226 artifact was reopened and
downloaded with its exact original hash after its provider agent had
been deleted/GET404 earlier today. Historical F226 full Artifact/Stop
cleanup, Question, corrected Plan and reconnect retain their original
identities. Deleting the three newly created disposable provider agents
and the new-case post-delete grade remain blocked on explicit user
approval after automatic review rejected expanding the two original
approved IDs. No new-case cleanup or complete fresh matrix pass is
claimed; this is separate from the directly observed task/Stop behavior
and retained exact cleanup proof.

Retained failures: stale fixture 132/133; E8 48/135 with 87 reset-hook
failures; Mac 17 SQL failures/skips; earlier startup-title, title-save,
custody/Stop and deletion failures with unknown causes and bounded
recoveries. `dea023` Artifact passed 17 checks but Stop failed its false
warning and 50 duplicate log identities. Pre-repair ACK failure and
original Core CI failure remain recorded; retry alone establishes no
cause or full local-suite pass. Provider dollar cost stays UNKNOWN.

## Risks

Experimental: Cursor independently acknowledges neither the effective
model nor instruction digest before the first prompt. The caller’s
explicit values are submitted and behavior checked live. Unpriced
provider dollar cost stays UNKNOWN.

Only qualified per-turn controllers are admitted. Uncertain work and
incomplete cleanup stay fenced. Existing damaged/duplicate archives
remain failed evidence. Prefix proof adds bounded object-store I/O; the
8MiB producer bound does not guarantee future providers’ output fits.

## Model Used

OpenAI Codex, GPT-6 family, assisted with reasoning, repository tools,
code execution, browser verification, and delegated code review. The
serving model’s exact API ID and context window are not exposed. No
value is inferred from the model catalog.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used and the limits of available model
metadata
- [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 linked public issues or described the issue in this PR
- [x] I have not referenced internal or instance-local Paperclip issues
or links
- [x] My branch describes the change and contains no internal ticket ID
- [x] I have run the scoped local tests reported above; pending gates
are stated separately
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation
- [x] I have considered and documented the risks
- [x] All required CI gates pass on the current head
- [x] Fresh Greptile is 5/5 with no unresolved findings on the current
head
- [x] I will address all reviewer comments before requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-03 08:38:57 -06:00
17e8bd7735 fix(runner): save native checkpoints when the sandbox disk is full (#18705)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs on a remote sandbox save two things after each turn: the
provider checkpoint and the workspace export
> - An agent can fill the sandbox disk during its own work, for example
with a package install
> - The turn still finishes, but both saves need free sandbox disk, so
they fail and the task becomes blocked with no clear reason
> - This pull request makes the checkpoint work on a full disk and makes
a full disk visible in the export failure
> - The benefit is fewer stranded checkpoints, and an operator sees at
once why an export failed

## Linked Issues or Issue Description

Related: #18715 added the shared-principal response-recovery control
synchronized below. #18644 reduced the scratch disk that the workspace
capture uses. #18172 added the disk-free streaming fallback to the
Daytona directory sync-out. Both were deployed when this failure
occurred. Neither changes the checkpoint copy, and neither can help when
the disk has no free space.

**What happened?**

A native agent ran a package install for a large monorepo in a 10 GB
Daytona sandbox. `df` in the run showed `10G 10G 4.6M 100%`. The agent
committed its work and reported `done`. Then:

1. The provider checkpoint made a filtered copy of the provider
directory inside the sandbox (`tar -cf archive; tar -xf archive -C
snapshot`) to apply its exclusions. On the full disk, tar failed. The
run recorded `runner_remote_checkpoint_snapshot_failed` without the
reason, and the partial copy stayed on the disk.
2. Workspace finalization failed three times, and file preservation
failed six times. The capture builds its archive inside the sandbox, so
each attempt failed. Every failure was recorded as
`remote_workspace_operation_failed` / `export_interrupted`, so nothing
named the full disk.
3. The task went to `blocked`. The only copy of the commit was in the
retained sandbox.

**Expected behavior**

The checkpoint does not need free sandbox disk when the provider can
stream. A workspace export that cannot fit says that the sandbox disk is
full.

**Steps to reproduce**

1. Run a native agent with a provider whose checkpoint has exclusions
(for example ACPX) in a small remote sandbox.
2. In the turn, fill the sandbox disk (for example `pnpm install` of a
large repository, or `fallocate` of the free space) and finish the task.
3. The checkpoint fails with `runner_remote_checkpoint_snapshot_failed`,
and every workspace export attempt fails with a generic code.

**Paperclip version or commit**

Observed on `410b0e27015`. This change is based on `5955dd4eb4c`.

## What Changed

- Synchronize the shared-principal response-recovery regression with its
saved comments and empty final deadline. Cover an in-flight discovery
invalidation. Keep every authority lock, exact publication, error, and
no-retry assertion. This changes test synchronization only.

- `syncRemoteRunnerDirectoryOut`: when the in-sandbox checkpoint copy
fails with a full-disk tar diagnostic, remove the partial copy and pass
the exclusions to the provider sync-out (`exclude` on the directory
mapping). The Daytona sync-out streams the directory without sandbox
disk when it cannot write its archive. Other copy failures and timeouts
still throw `runner_remote_checkpoint_snapshot_failed`. They now also
remove the partial copy.
- `remote-workspace-transport.ts`: the helper install and the capture
scripts report `remote_workspace_sandbox_disk_full: ...` when Node
reports `ENOSPC` or `EDQUOT`, or a Git or tar child reports "No space
left on device". `command()` passes only that exact message through,
like the existing unfinished-Git-operation message.
- `native-workspace-finalizer.ts`: a remote save that fails with that
message adds it to the failed operation's stderr. The failure code, the
retry budget, and the recovery flow do not change.

## Verification

- Current-head infrastructure interruption: the [Build
job](https://github.com/paperclipai/paperclip/actions/runs/41637668555/job/125923826158)
completed the actual build and production bundle boot, then the runner
received a shutdown signal during the clean public Grok npm-install
check. GitHub marked that step cancelled. The exact host-stop cause was
not retained. One bounded rerun of only the failed Build and dependent
`ci / verify` jobs passed on the unchanged head. [Build attempt
2](https://github.com/paperclipai/paperclip/actions/runs/41637668555/job/125931400745)
completed successfully, including the clean Grok npm-install check. No
product code, assertion or workflow changed for this interruption.

- CI failure retained: commit `6c5937fcae` failed [chat shard
1/3](https://github.com/paperclipai/paperclip/actions/runs/41634638570/job/125913957217)
because the shared-principal fixture saw zero saved comments after
`worker.ready` and zero active work. The other chat child passed. The
exact case passed once locally and then 30 times on the same Node
24.22.2 version as CI; those passes alone do not explain the CI
interleaving.
- Controlled RED/GREEN: invalidate the real database discovery once
while its read is in flight. The worker correctly retains a future
reconstruction deadline, but the old initial-pass/active-count barrier
reproduces the same zero-versus-one comment assertion. Waiting for both
saved comments and final `active=0`/`nextWakeAt=null` passes. Six
real-PostgreSQL controls pass on final commit `a60e5629e8`, including
endpoint independence, source revocation, and external lock
failure/recovery. No sleep, timeout increase, weakened assertion, or
product scheduling/authorization change was added.
- A local server typecheck first lacked the checkout's built Runner
declarations. After the declared Runner TypeScript build, the server
typecheck passes on `a60e5629e8`. Final-head remote verification is
complete: 54 successful checks, 3 expected skips, no failures or pending
checks, and successful `ci / verify` and `ci / e2e` aggregates. Greptile
is 5/5 on this exact commit with no unresolved review threads.

- Merge preparation: rebased onto master `b22ef22d56` and kept both
test-import additions. Added the checkpoint fallback and
capacity-failure contract to `doc/execution-semantics.md`. On rebased
commit `6c5937fcae`, 47 focused checkpoint, transport, finalization and
retained warm-projection tests passed across three files. The name
filter skipped 1,040 unrelated cases. Map and whitespace checks passed.
The final-head CI and Greptile results are recorded above.

- `npx vitest run
server/src/services/native-runtime/native-session-executor.test.ts
server/src/services/native-runtime/native-workspace-finalizer.test.ts
server/src/services/remote-workspace-snapshot-archive.test.ts` passes
(1,152 tests).
- `npx vitest run server/src/services/remote-workspace-transport.test.ts
server/src/services/native-runtime/native-workspace-finalizer-failure.test.ts`
passes (30 tests).
- New tests:
- Checkpoint, with real files: a full-disk snapshot leaves a partial
copy. The provider then gets the exclusions, the session files are kept,
the package caches are left out, and the partial copy is removed.
- Checkpoint: a permission failure and a timeout still fail, remove the
partial copy, and do not call the sync-out.
- Transport: the real capture scripts run with an `ENOSPC` preload, for
the helper install and for the archive write. Both report the full-disk
message and leave no partial attempt. A message with extra text still
collapses to `remote_workspace_operation_failed`.
- Finalizer: the operation stderr names the full disk and keeps
`workspace_sync_out_failed`. Other failures keep the bare code.
- With the source change removed, the 3 checkpoint cases and the 2
transport ENOSPC cases fail.
- `npx tsc --noEmit -p server/tsconfig.json` is clean.
- Suites that refer to the changed error codes
(`explicit-native-continuation`, `native-recovery-notice-work`,
`native-run-finalizer-telemetry`, `native-workspace-export-retry`,
`remote-workspaces.integration`) pass. In `chat-channels.integration`, 2
cases fail locally on unmodified `master` too (a bundled skill path
outside the checkout, and a chat run timing assertion). They are not
related to this change.
- Manual recovery of the observed task: the retained sandbox was resumed
with free space, file preservation was requested again, and the saved
workspace contained the agent's commit.

## Risks

- The fallback runs only after the snapshot fails with a full-disk
diagnostic. The normal path does not change.
- Provider exclusions are not anchored to the directory root. In the
fallback, a nested entry that has an excluded name (for example a nested
`tmp` or `config.toml` in a Codex home) is also left out. The fallback
never copies more than the snapshot, so credentials stay out of the
checkpoint.
- The fallback depends on the provider honoring `exclude`, which the
sync protocol requires. The Daytona and Kubernetes providers honor it.
- A workspace export on a full disk still fails. This change only names
the cause. A full fix needs one of these: free-space reserve for the
export, removal of re-creatable caches before the export, or a capture
that streams its archive. Each of these changes behavior, so this pull
request does not include one.
- No migration.

## Model Used

- OpenAI Codex, GPT-6 family, assisted with merge preparation, conflict
resolution, documentation, test synchronization and verification. The
harness does not expose the exact serving model ID or context-window
size.

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, in Claude Code with
tool use (shell, file edits, test runs). It did the root-cause analysis
from run events, tool output, and sandbox inspection, wrote the code and
tests, and ran local verification.

## 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>
Co-authored-by: Dotta <bippadotta@protonmail.com>
2026-11-03 07:09:52 -06:00
DottaandPaperclip b22ef22d56 fix(runner): show provider failure details on tasks and runs (#18622)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native Runner starts providers and records their run outcomes.
> - Provider failures can contain useful details below a generic
protocol error.
> - Those details were lost at the sidecar, command receipt, and run
projection boundaries.
> - A blocked task therefore showed a generic failure even when the
provider gave a clear reason.
> - This pull request carries a bounded, sanitized provider failure
through those boundaries.
> - Operators can read the actual cause on the task and run pages.
Existing recovery policy still controls retries.

## Linked Issues or Issue Description

Refs #17543 for the related closed JSON-RPC classification work. Refs
#17272 for provider recovery in the legacy OpenCode adapter. This change
adds display evidence for native Runner providers and keeps recovery
policy separate.

**What happened?**

A native Claude session returned an HTTP 429 message below an ACP
`Internal error`. The task and run showed only a generic startup
failure. Operators had to inspect the provider response to learn that
the account was rate limited.

**Expected behavior**

The task and run show the sanitized provider message, provider name,
startup or turn phase, and available HTTP status. Unknown categories
keep the message. Display evidence does not grant a retry or replace a
recovery decision.

**Steps to reproduce**

1. Start a native Claude run whose account is rate limited during model
selection, or return a structured provider failure from a Codex or
OpenCode turn.
2. Let the run fail and open its task page.
3. Before this change, the page shows a generic failure. With this
change, the page shows the provider explanation. The run page shows the
same explanation after reload.

**Paperclip version or commit**

Source checkout based on `f8a888a072`.

## What Changed

- Add one JSON Schema for `ProviderFailure` and generate matching Runner
and shared TypeScript readers. Keep the Runner package independent of
the app workspace at runtime.
- Normalize known ACPX, Codex, and OpenCode error fields. Preserve
unknown messages. Redact known credential values and credential patterns
before applying the 4,000-character limit. Do not copy response bodies,
headers, stacks, or prompts.
- Carry startup failures through the sidecar, Rust Runner, durable
command receipts, and private transport error details. Keep provider
text out of generic transport error strings and Sentry failure messages.
- Keep turn failures in authenticated run events. Recover the same
evidence from persisted events during server finalization. Store the
cause in the company-scoped run result.
- Show one shared notice on failed tasks and run details. Keep existing
retry permissions, scheduled retry state, and resolved-task behavior.
- Reapply registered run-secret and current-user redaction to task
display data. Batch secret lookups for only the visible provider causes.
- Document the contract and update the capability maps. Add regressions
for nested Claude 429 errors, OpenAI quota and rate limits, OpenCode
errors, redaction, durable receipt replay, restart finalization, and UI
rendering.

## Verification

- `pnpm -r typecheck` passed. `pnpm build` passed. All GitHub Actions
build, test, package, and validation jobs passed on `d49c79ab4a`. The
security checks passed. Greptile gave this head 5/5 with no open review
threads. Paperclip Review remains pending. Its review worker exhausted
six workspace recovery attempts with export_interrupted. A fresh review
request was accepted and is waiting for that saved workspace. The
ordinary Retry action rejected reuse of the stopped request. This is an
external review-worker blocker; no check was overridden. The duplicate
local `pnpm test:run` was stopped after the CI test shards passed.
- Focused server settlement, provider evidence, and restart tests: 93
passed. Runner admission and normalization tests: 103 passed. Focused
Runner wire and terminal tests: 29 passed. The real Rust/sidecar command
receipt replay test passed.
- The new projection test failed before implementation because the task
had no provider cause. The startup producer regression also failed
before extraction moved ahead of the generic error-code replacement.
Both pass with this change.
- Live check: a rebuilt local sidecar received an actual Claude HTTP 429
during startup. The new cause kept the full provider message. A
synthetic task in an isolated local app then reached the same provider
error through the native Runner path. The dedicated run result, task
API, task banner, and run detail all showed the same message after
reload. The later reporting fix keeps run.error generic; a regression
proves the display cause still survives restart and stays out of Sentry
capture.
- The first isolated app attempt failed earlier in
`admission_credential`. An ordinary manual Retry reached the provider
and produced the 429 above. This PR does not claim to fix that separate
admission failure. An initial parallel local database test setup also
failed; the serial restart suite passed.
- Security review found that copying display text into run.error could
forward it to Sentry. A regression failed with that behavior. The fix
keeps the cause in its display field. All 38 settlement/restart tests
and the server typecheck pass.
- All 29 protocol contract tests pass, including bounds and extra-field
rejection. An earlier full Rust core run passed. The later full run hit
one 5-second receipt-limit timing assertion; its isolated rerun passed.
The full Linux and Windows Rust CI jobs passed on this head.
- Review regressions pass for explicit Codex driver identity, native
Rust startup and resume receipt propagation, oversized OpenCode/ACPX
errors, task read-time registered-secret redaction, and the standalone
source closure. The focused Runner run passed 103 tests;
projection/restart tests passed 16.
- Read-time redaction edge cases passed: a missing decryption key omits
only the unsafe optional explanation; expanded redacted messages remain
visible within the Unicode character limit. All 14 task projection tests
and server typecheck pass.
- The package Build check found an old Pi assertion against
Error.message. The probe now checks the exact closed classification in
the authenticated receipt and requires the new provider cause. Both
polling transport cases pass. A local credential-free Pi probe stopped
at admission_installation because this checkout lacks the Pi
distribution; the installed Linux package probe passed in CI on this
head.
- UI coverage includes startup and turn text, unknown categories,
literal rendering of provider text, read-only retry behavior, and
resolved-task suppression. Token gates and map checks passed.
- The live check covers Claude startup on macOS. Codex, OpenCode,
terminal failures, and restart paths have deterministic regression
coverage. The change is not deployed.

## Risks

- Provider messages are untrusted text. The boundary selects specific
fields, redacts credential patterns and known environment secret values,
bounds the message, and renders plain text. New provider-specific secret
formats may need added redaction patterns.
- Optional fields preserve compatibility with older producers and stored
runs. Old runs without the field keep their current fallback text. No
database migration is required.
- The cause stays in company-scoped run data. This change does not send
provider text to first-party Telemetry or add it to OpenTelemetry span
attributes.
- Normalized categories and retryability are display evidence. Existing
recovery classifiers, retry limits, ownership checks, and approval gates
keep their authority.

## Model Used

- OpenAI GPT-6 through Codex, with reasoning, repository tools, code
execution, and browser verification. The host does not expose the exact
serving 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 and contains no internal
Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-03 06:13:26 -06:00
DottaandPaperclip 718336eb04 fix(native): refresh prior warm controller projections (#18718)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native remote sessions keep their controller state in a database
checkpoint.
> - An admission can also leave a full local copy of a prior run's
state.
> - The prior owner can commit more state without changing its run ID.
> - Warm admission skipped a refresh when the local and saved run IDs
matched.
> - This pull request refreshes that prior-run copy when no retained
warm owner exists.
> - The next turn can use the committed state while existing ownership
checks remain effective.

## Linked Issues or Issue Description

**What happened?**

A prior warm run can complete after an earlier admission reads its
controller checkpoint. The local copy can still contain a pending
command or lack the final terminal event. A later admission sees the
same run ID and skips restoration. It can then hold work against that
old copy even though the database checkpoint has settled.

**Expected behavior**

Read the canonical prior-run checkpoint before reuse when no retained
in-memory owner exists. Keep current-run state with its controller. Keep
unresolved canonical work and invalid checkpoint bindings blocked.

**Steps to reproduce**

1. Create a full prior-run local projection while a command is pending.
2. Complete that command and record the terminal in the canonical
checkpoint under the same run ID.
3. Start an ordinary remote warm continuation without a retained
in-memory owner.
4. Before this change, the local command remains pending. With this
change, the completed command and terminal replace the stale copy.

**Paperclip version or commit**

Reproduced on master `36df8933f0` before changing production code.

Related work: #18018 restored per-turn checkpoints; #18343 added
controller replacement recovery; #18554 restored missing remote
suspension receipts. This change handles a full prior-run projection
whose run ID already matches the saved supervisor. It does not change
the settlement guard or MCP compatibility policy.

## What Changed

- Refresh a prior-run warm controller projection even when its run ID
has not changed.
- Keep current-run and retained warm owner authority on the existing
path.
- Add five executor cases for a settled source, a pending canonical
command, a current run, legacy file-backed state, and an invalid
checkpoint binding.
- Add an opt-in Linux continuity case with a stale full local projection
and two follow-up turns.
- Update execution documentation and the paired session verification
recipes.

## Verification

- RED before the production edit: the executor fixture retained
`pending` in its local command record when the canonical record was
`completed`.
- GREEN on the final commit: 48 focused tests passed across executor
restoration, retained warm owner rotation, per-turn restoration, and
remote settlement guards.
- `pnpm --filter @paperclipai/server typecheck` passed. The server
TypeScript check also passed after the integration fixture was added.
- `node scripts/check-maps.mjs` and `git diff --check` passed.
- The unit fixture uses the real durable controller serializer and the
real executor entry. It stops at current tool authority before
provider/backend creation. It proves restoration and guard behavior, not
full task acceptance.
- Actual protocol RED/GREEN: the same opt-in continuity fixture uses
real PostgreSQL, TLS, the current Linux Rust Runner, and a deterministic
fake Codex backend. With only the executor restored to its parent
revision, continuation fails with
`prior_owner_terminal_state_indeterminate` in the settlement guard after
one provider turn (20.16 seconds). The PR source completes all three
turns with one provider thread and final suspension (34.77 seconds).
Hard recovery permits one provider-process replacement; the healthy
final follow-up starts no additional process. Both runs use the same
Runner binary and owned container.
- The exact PR executor source was restored after the baseline
comparison, its Git blob was verified, and the worktree is clean. The
disposable local container was removed. This fixture proves the
controlled stale-projection recovery path; it does not establish a live
model or Daytona API result.
- Full current-head CI passes, including typecheck, build, general and
serialized tests, Rust/Runner checks, all eight E2E shards, and the `ci
/ e2e` and `ci / verify` aggregate gates. Greptile is 5/5 on the current
commit, with no unresolved review threads. No original task was retried
and no staging sandbox lifecycle was changed.

## Risks

- Admission without a retained owner performs one additional scoped
checkpoint read when a full prior-run projection already exists.
- The canonical checkpoint must still pass its company, task, session
and record validation. A pending canonical command stays held.
Current-run and retained owner state are not replaced.
- The regression proves a stale local projection defect. It does not
establish that a historical incident had those same local bytes.

## Model Used

OpenAI Codex, GPT-6 family. The harness does not expose the exact
serving model ID or context-window size. Capabilities: repository
inspection, code generation, tools, and 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

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-03 06:12:22 -06:00