Commit Graph
786 Commits
Author SHA1 Message Date
DottaandClaude Opus 5.5 db41bfcb38 fix(recovery): let an accepted Retry finish after a killed copyback owner (#19721)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A native run saves the agent's workspace files after the provider
turn (copyback). One controller owns copyback at a time and records a
durable owner receipt on the run (`nativeWorkspaceFinalizationOwner`)
> - When copyback of an accepted result fails, retained saved-work
recovery exports the files from the kept sandbox. A task Retry waits for
that recovery before it continues
> - A control-plane restart can kill the controller while it owns
copyback. The dead controller can never confirm its own stop, and
nothing in the retained recovery path could release its receipt
> - The recovery worker then spent one attempt on each recheck of that
receipt until it was exhausted, and the accepted Retry kept saying
"Paperclip is finishing the saved result" with no run. The task waited
forever
> - This pull request stops spending attempts on that recheck, names the
real wait, and lets the board release the exact receipt so the same
recovery and the same Retry continue
> - The benefit is that an accepted Retry never waits silently: it
finishes, or it shows one honest action that makes it finish

## Linked Issues or Issue Description

Refs #19717 (same-host automatic proof for a killed copyback owner; this
PR covers the retained-recovery path and the case where no automatic
proof is possible)
Refs #19713, #19609, #19694 (related restart-recovery fixes; no overlap
in files except this doc)

**What happened?**

Two Daytona restart-chaos waves. In each, a native run's copyback failed
and the run failed with `native_workspace_sync_out_retry_exhausted`. The
agent's result was accepted. Retained recovery started. A SIGKILL of the
control plane landed while a recovery attempt owned copyback:

- Wave s13, task t06 (Claude, crash loop): Retry was accepted at
02:30:35. A SIGKILL landed 2 s later. All later attempts failed with
`copyback_stop_unconfirmed`. After 6 attempts the recovery was
exhausted. The Retry showed `retryWait.reason = finalizing_saved_result`
for more than 15 minutes. No run was created.
- Wave s12, task t01 (Claude): attempt 1 failed with "Workspace
checkpoint payload mismatch". The SIGKILL at 01:43:54 killed the owner
of attempt 2. Attempts 3 to 5 failed with `copyback_stop_unconfirmed`.
The Retry was accepted with HTTP 2xx and produced no run.

In both kept databases the run carries an owner receipt for the killed
process.

**Steps to reproduce**

1. Accept a native result and make its workspace copyback fail, so that
retained recovery owns the export.
2. Press Retry on the task.
3. Kill the control plane while a recovery attempt owns copyback, so
that its owner receipt stays on the run. Use another host, or a receipt
without automatic stop proof.
4. Let recovery run.

**Expected behavior**

Rechecking the receipt does not spend attempts. The task names the wait
and does not offer a Retry that cannot help. A board member can confirm
that the previous server stopped. The recovery then finishes, and the
waiting Retry continues by itself.

**Actual behavior**

`withNativeWorkspaceFinalizationOwnership` returned `owner_unverified`
for a retained repair without creating any board action
(`native-workspace-finalization-ownership.ts`,
`requireStopVerification`). `recoverRetainedWorkspace` reserved an
attempt for each recheck and failed it as `copyback_stop_unconfirmed`
(`workspace-recovery.ts`), until the budget was exhausted. The Retry
gate in `explicit-native-continuation.ts` returned
`finalizing_saved_result` for an accepted result with no verified
repair, with no check for this hold.
`resumeNativeWorkspaceAfterOwnerStop` accepted only an
`owner_unverified` action on a `running` run, so no route could release
the receipt of a finished run. The exhausted handoff told the board to
press Retry again, which repeats the same failure.

## What Changed

- `server/src/services/workspace-recovery.ts`: when retained recovery
finds an unverifiable copyback owner, it records the recheck without
reserving an attempt. The 30-minute recovery window still bounds it. The
retained action records the exact receipt as `workspaceOwnerRecovery: {
state: "operator_required", owner }`. A newer receipt replaces the
record and clears an older confirmation.
- `server/src/services/explicit-native-continuation.ts`: a Retry that
waits for an accepted saved result reports
`workspace_owner_stop_required` when the run's active retained action
has that hold. It no longer reports `finalizing_saved_result` for this
case.
- `server/src/services/task-interruption-presentation.ts`: the new wait
has a fixed message, and Retry is not offered for it, because another
click cannot release the receipt.
-
`server/src/services/native-runtime/native-workspace-owner-recovery.ts`:
the existing board route `POST
/issues/:id/recovery-actions/resume-workspace-finalization` (permission
`runtime:confirm_owner_stop`) also accepts a retained recovery action
with that hold. The confirmation must name the receipt that is still on
the run. A live owner on this host still rejects it. It clears only that
receipt, records the stop proof and an activity entry, and returns the
same recovery to the worker. A spent budget gets one new generation, as
a deliberate recovery request does. It accepts no result and starts no
agent turn. The owner check is shared with the existing path.
- `server/src/services/system-recovery.ts`: an exhausted
`copyback_stop_unconfirmed` handoff names the board confirmation instead
of another Retry.
- `ui/src/api/issues.ts`: the route receipt's `resultId` can be `null`
(a retained recovery has no live result binding).
- `doc/native-workspace-finalization-recovery.md`: an additive paragraph
describes this path.

## Verification

- Red then green on embedded Postgres:
`server/src/services/workspace-recovery-preparation-gates.test.ts`,
"lets an accepted Retry finish after a killed copyback owner instead of
waiting forever" with one case per wave (s13, s12). Each case uses a
real accepted remote export, a Retry through the HTTP route, a
killed-owner receipt, and 8 recovery passes.
- Before the fix (both cases): `attempts: 6, state: "exhausted"`. That
is the wave state.
- After the fix: no attempt is spent on the recheck. The hold is
recorded. The Retry wait is `workspace_owner_stop_required` with
`canRetry: false`. The board confirmation through the route returns 202
and clears the receipt. The same recovery completes. The same accepted
Retry then finishes the saved result (s13: the task is `done`) or
continues in exactly one new run (s12).
- `npx vitest run
src/services/workspace-recovery-preparation-gates.test.ts -t "killed
copyback owner"`: 2 passed.
- `pnpm verify` (affected selection): see the comment below. Server `tsc
--noEmit` is clean.
- No live Daytona replay. Both waves ran on old signed images. The local
proof uses the same accepted-export fixture as the existing Retry tests,
with the receipt state copied from the kept databases.

## Risks

- A copyback owner on another host that is still alive is now rechecked
within the recovery window without spending attempts. Before this change
it spent them. The window still bounds the wait.
- The board confirmation for a retained action is an operator statement,
the same as the existing owner-stop path. It is limited to roles with
`runtime:confirm_owner_stop`. It never clears a receipt other than the
one it names, and a live same-host owner still rejects it.
- #19717 changes the same ownership area and adds a UI action for the
running-run case. That UI does not show a retained action yet. After
both merge, a small follow-up can show its button for this hold. Until
then the API route and the task's wait message cover it.
- Not addressed here: the "Workspace checkpoint payload mismatch" that
started wave s12. The checkpoint outbound transfer extracts without `-p`
(`sandbox-managed-runtime.ts` checkpoint sync-out, `extractHostTarball`
in the Daytona provider), so the host umask can change file modes that
the remote manifest recorded. That is a separate signature.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 22:07:00 -06:00
DottaandClaude Opus 5.5 5cc89b1e4d fix(native): do not charge a restart resume that made progress before the next crash (#19720)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs have an execution recovery budget of three provider
attempts, so a run that keeps failing stops retrying
> - A control-plane crash (no graceful drain) makes the next server
resume the run, and that resume charges one attempt
> - #19625 stopped charging a resume after a graceful drain, but crash
restarts still charge, even when the previous resume did real work
> - On a deploy day with several restarts, a healthy run used up its
budget and the task needed a manual Retry
> - This pull request does not charge a crash resume when the previous
restart resume made recorded progress
> - The benefit is that restarts alone do not exhaust a run that keeps
making progress, and genuine loops stay bounded

## Linked Issues or Issue Description

Refs #19625

**What happened?**
In restart-chaos testing (storm wave, OpenCode run), a run crossed four
control-plane restarts: K1, K1, K2 (drain), K1. Each resume was clean:
the runner restored the session, recorded
`provider_turn_lost_on_restore`, and the #19699 continuation turn then
ran tools for minutes. The two K1 resumes each charged one attempt
(attempt 1 to 3). The drain resume did not charge (#19625). The fourth
restart found attempt 3 and blocked the run with
`native_restart_recovery_blocked` /
`execution_recovery_budget_exhausted`. Seven runs in the same wave
failed in the same way. In every case, each resume had accepted a new
turn and completed tool work before the next restart.

**Expected behavior**
A run must not fail because it survived several controller restarts that
each resumed cleanly and made progress. The budget must still stop a
resume that fails each time, a crash loop that makes no progress, and a
provider that fails repeatedly.

**Steps to reproduce**
Start a native local run. Kill the server (SIGKILL) while the run is
active and let it resume. Let the resumed turn run tools. Repeat three
times. The third crash resume is blocked with
`execution_recovery_budget_exhausted`.

**Paperclip version or commit**
`master` at `8a34c1b62c`.

## What Changed

- `claimNativeRestartRecoveries()` (`native-restart-recovery.ts`)
records `progressWatermarkSeq` (the run's highest
`heartbeat_run_events.seq`) in the history entry of each recovery claim.
- New `previousResumeMadeProgress()`: the next claim reads the watermark
of the most recent claim. It finds progress when, after that watermark,
a `turn.accepted` event names a turn, and a later
`tool.execution.completed` or `item.completed` event (not a `usage`
item) belongs to the same turn. The evidence is committed run events,
not timing.
- When that progress exists, the claim is not charged
(`chargesProviderAttempt: false`, and the `>= 3` exhausted gate is
skipped). This uses the same path as the clean-drain rule from #19625.
The history entry records `attemptKeptForProgress: true`.
- Restore activity does not count: lineage items without a turn,
`turn.failed`, work on the interrupted turn, `turn.submitted`, and an
accepted turn with only usage or a failure. So a resume that dies or
fails before it does work still charges.
- The credit stops at controller generation 12
(`NATIVE_PROGRESS_CREDIT_GENERATION_LIMIT`). A run that does some work
and then crashes the controller every time still reaches the budget.
- The first restart (which interrupts the original execution, not a
resume) still charges. A history entry from an older release has no
watermark, so it gets no credit.
- `doc/execution-semantics.md` states the rule next to the three-attempt
budget.

## Verification

New tests in
`server/src/services/native-runtime/native-runner-restart-recovery.integration.test.ts`
(embedded Postgres, real `claimNativeRestartRecoveries`; between claims
the test applies the executor's attempt charge and marks the controller
dead):

- `keeps recovering a run whose restart resumes each completed work
before the next crash`: five crash resumes, each followed by restore
noise and one completed tool in the continuation turn. Charges are
`[true, false, false, false, false]` and the attempt stays 2. Red on
master (attempt 2, 3, then blocked).
- `still exhausts the budget when restart resumes complete no work
between crashes`: resumes followed only by restore noise and a failed
continuation turn. Attempt 2, attempt 3, then
`execution_recovery_budget_exhausted`. Green on master and with the fix.
- `bounds a crash loop that makes progress before every crash`: credit
stops at the generation limit, the last attempt is spent, and the run
blocks. Red on master.
- `native-runner-restart-recovery.integration.test.ts`,
`heartbeat-native-restart-suspension.test.ts`,
`native-restart-recovery-isolation.test.ts` and
`native-remote-retry-admission.test.ts`: 218 passed, 14 skipped.
- I ran the progress query against the live database of the failing
wave. For all seven blocked runs, every resume had progress before the
next restart, so this rule would have kept each of them recovering.
- `tsc --noEmit` for `server` has no errors in the changed files.

## Risks

- A run that makes progress and then makes the controller crash each
time gets up to about ten extra resumes before the budget applies. The
generation limit bounds this.
- The rule depends on the PRP event shape (`payload.prpEvent.turnId` on
`turn.accepted` and on completed work). Events without that shape get no
credit, so the failure mode is the old behavior (charge), not an
unbounded loop.
- The query runs once per claim, on the `(run_id, seq)` unique index
range after the watermark.

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

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with
tool use (shell, file edits, test execution). Extended reasoning
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 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 22:04:19 -06:00
DottaandPaperclip 1a60d47804 Preserve settled native log files during online migration (#19719)
## Thinking Path

> - Paperclip manages work for AI agents.
> - The web server must retain run history across replacements.
> - Older native runs can have local log files with no final size or
digest.
> - The normal importer correctly refuses to call those files complete.
> - The existing raw archive mode requires the entire instance to stop
work.
> - This change gives settled sources a separate, guarded online archive
path.
> - Active work can continue while old evidence moves to object storage.

## Linked Issues or Issue Description

Refs #18864, #19678, #19688.

The offline historical log archive path cannot migrate a busy instance.
This change preserves exact surviving bytes without claiming complete
native-event coverage or changing the original run outcome.

## What Changed

- Add an explicit online option for settled native log files. Keep the
default importer and offline option unchanged.
- Require a released, closed native finalizer and no pending attempt,
allocation, physical owner, or task binding. A stale lease or terminal
run status alone is insufficient.
- Use the canonical physical finalization lock and row locks. Check the
source generation before claims and publication.
- Read only the canonical, private local file. Reject aliases, symlinks,
hard links, changed files, and changed owners.
- Verify remote bytes before publishing a read-only archive and activity
receipt in one transaction. Keep original files, errors, run status,
event rows, and completeness metadata.
- Document the command and its authority limits. Add real PostgreSQL and
loopback S3 transport tests.

## Verification

- The original implementation failed the new archive regression. It
returned blocked instead of migrating the settled source.
- Final source integration suite: 15 tests pass on disposable PostgreSQL
and the bounded loopback S3 fixture. Coverage includes active unrelated
work, failed and successful sources, a 4 MiB segment boundary, owner
changes, file changes, unresolved owners, task bindings, allocations,
and filesystem aliases.
- The qualified source TypeScript check and module boundary check pass.
- All 73 tests in five affected existing log-store, object-journal,
cancellation, and migration-worker suites pass. The first borrowed
dependency setup loaded work contexts from another worktree and failed
four tests. Explicit database subpath aliases resolved that setup fault;
no worker code changed.
- Earlier test fault injection used the wrong storage method, and the
first segment test reused a closed fixture endpoint. Those fixture
defects were corrected before the final 15-test pass.
- Local tests use Node 26. CI supplies the standard full typecheck,
build, and test gates. Production object-storage credentials, live
Railway replacement, and provider continuation are separate deployment
acceptance steps.

- Final exact-head CI: 59 successful contexts and 3 skipped, with all
typecheck, build, test, E2E, security, and review gates complete.
[Workflow
evidence](https://github.com/paperclipai/paperclip/actions/runs/42332349644).
- The first CI attempt had 12 jobs interrupted by runner shutdown. Their
logs end in cancellation, with no test assertion found. The
failed/cancelled-only retry passed all 12 jobs and recomputed
aggregates. The 38 original successful job records kept their original
execution times and were not rerun. Original interruption logs remain
retained.

## Risks

- This path preserves raw history. It cannot recover missing source
bytes or prove a complete native transcript.
- Uploads can finish before a later source check fails. The run
reference stays local in that case; a retry must still satisfy all
authority checks.
- The new option is explicit. It adds no startup worker, recurring poll,
or background scan.
- Source transactions hold scoped locks only for ownership checks and
publication. Streaming and remote verification run outside those
transactions.

## Model Used

OpenAI GPT-6 family through Codex, with reasoning, tool use, and code
execution. The host does not expose the exact model ID or context window
to this session.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` /
`Closes: #` / `Refs #` OR (b) described the issue in-PR following the
relevant issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change 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 21:37:18 -06:00
DottaandClaude Opus 5.5 51eb4ff705 fix(recovery): finalize an accepted native result after a hard kill (#19713)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A native run hands its accepted result to a controller lease, and
the controller finalizes the task from that result
> - A graceful shutdown releases that lease for the next server
(#19591), but a hard kill (SIGKILL, OOM, host crash) cannot
> - After a hard kill between result acceptance and finalization, the
next server waited for the dead controller's 20-minute lease, so the run
stayed `running` and the user's follow-up comment waited with it
> - This pull request releases that lease at startup when the controller
is proved dead by its boot ID and process identity, never by age
> - The benefit is that the saved result finalizes within seconds after
a restart, without another provider turn, and queued follow-up work
continues

## Linked Issues or Issue Description

Refs #19591 (the graceful-shutdown counterpart) and #19578
(boot-identity proof for legacy runs).

**What happened?**
A restart-chaos wave sent K1 (SIGKILL) 20 ms after a Claude run recorded
`run.result.accepted` and `run.terminal`. The run then stayed `running`
for more than 10 minutes, through four more restarts. The finalization
row was `workspace_finalizing` with the stored result. Its lease
belonged to the killed server: a different `controller_boot_id`, a
`controller_pid` with no process, and `lease_expires_at` 20 minutes
ahead. Startup recovery skips runs that already have a result. The
result-commit worker waits until the lease expires. The task's follow-up
comment could not run until then.

**Steps to reproduce**
1. Let a native local run accept its result.
2. SIGKILL the server before finalization finishes.
3. Restart the server. The run stays `running` until the old lease
expires.

**Expected behavior**
After a restart, a result lease held by a stopped controller of an
earlier boot is treated as released at once. The stored result finalizes
once, with no new provider turn, and the follow-up wake runs after it.

## What Changed

- New
`server/src/services/native-runtime/native-dead-controller-result-lease.ts`:
`releaseDeadControllerResultLeases` finds `running` native runs with an
accepted result and an unexpired lease. It requires the lease to belong
to a different controller boot. It proves the controller stopped with
`evaluateNativeControllerTakeover` (`requireStoppedController`): the
process is dead, or its PID now names another process. It then expires
that lease with a compare-and-set on the exact owner token, boot ID, and
PID, and notifies the result-commit and finalization workers.
- `server/src/services/heartbeat/recovery.ts`:
`recoverNativeRunsAfterRestart` calls this first and records a
`native.recovery.transition` event (`awaiting_result_finalization`) for
each released run.
- Only executor leases are released, because only the executor claim
writes the controller identity together with the lease. Finalizer
(`native-finalizer:`) and cleanup (`native-cleanup:`) claims keep an
earlier executor's identity, so they are never released this way and
expire on their own short leases. A finalization-resume token
(`:resume:`) on a result row always belongs to the executor that adopted
it, because a pending resume claim is taken only on a row without a
result.
- Also not released: a live owner, an owner without a recorded process,
a lease of the current boot, a run this process is executing, and remote
workspaces (`nativeWorkspaceSync` or `remoteWorkspace`), which keep
their fenced lease.
- The compare-and-set does not compare the stored expiry. Postgres
stores microseconds that a JavaScript `Date` drops, and an exact-expiry
comparison silently skipped the real lease in the live reproduction.
- `doc/execution-semantics.md`: documents the rule.

## Verification

- Red then green on embedded Postgres
(`server/src/__tests__/heartbeat-native-restart-suspension.test.ts`):
- A hard kill with a dead controller's executor lease (first claim,
restart claim, or resumed claim) now finalizes on the next startup: run
`succeeded`, finalization `committed`, task `done`, one result, one run,
no provider dispatch. These tests failed before the fix with `status:
running`.
- A lease expiry stored with microsecond precision is released. This
test failed against the first version of the fix and reproduces the live
miss.
- A live controller of another boot keeps its lease, and so does a lease
of the current boot.
- A live claimant's finalizer or cleanup lease is kept when the recorded
earlier executor is dead.
- `npx vitest run
src/__tests__/heartbeat-native-restart-suspension.test.ts`: 20 passed.
- Related suites pass: `native-session-resumption`,
`native-runner-restart-recovery.integration`,
`native-run-projection-work`, `live-recovery-work`. One
`live-recovery-work` legacy case timed out once under load and passed
when run again by itself.
- Live, local: `tests/restart-chaos/main.ts --workloads C1 --triggers
result --faults K1 --rc06-masked`. The failing case above came from such
a wave. I started its kept database and confirmed the stored lease state
and the compare-and-set miss. Against that same database, the fixed
release expires the stuck executor lease. Two later waves (12 tasks,
`--triggers result`) passed every task. Run events were not kept for
passing tasks, so I cannot confirm that a kill landed in the narrow
pre-finalization window in those waves.

- Live proof on head `8971874654`: local ACPX Claude, workload A1, seed
7411, `--triggers result` (a local variant that fires on A1 2.5–3.5 s
after the run's result proposal), `--keep`. The run passed 4 of 4 tasks.
In two tasks, K1 landed 1.5 s and 2.0 s after `run.result.accepted`. On
the next startup, each run recorded "Server restart released the
accepted native result of a stopped controller for finalization"
(`controller_process_dead`). Each finished `succeeded` about 1 s later,
with its task `done` and exactly one run per task (no extra provider
turn). Before this fix, the same shape stayed `running` until the
10-minute budget ran out (seeds 7411, 7422, and 7423 from the RC-33b
investigation in #19691).

## Risks

- Startup could take a lease from a finalizer that is still running.
Mitigations: the lease must belong to another boot, the controller
process must be proved stopped on this host (the same proof the existing
runner takeover uses), and the update matches the exact owner. Remote
workspaces are excluded. Multi-host deployments keep the existing
PID-liveness assumption that `claimNativeRestartRecoveries` already
makes.
- Low behavioral risk otherwise: finalization uses the existing
reconciler path, which #19591 already exercises after a graceful drain.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, run as an agent in
Claude Code with tool use (shell, file editing, local test execution,
live restart-chaos waves).

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 21:22:24 -06:00
DottaandPaperclip d1306f338b fix(runtime): preserve settled failed harness backups online (#19715)
## Thinking Path

> - Paperclip coordinates agents, tasks, and their saved execution
state.
> - A replacement server needs native provider backups in shared
storage.
> - The online pre-copy owner accepts successful runs with a committed
result.
> - A sealed backup from a failed or cancelled run can still contain
user history.
> - The existing owner defers those files even after their source
ownership is settled.
> - This change adds a separate preservation owner with exact source and
publication checks.
> - The copy retains the failed outcome and original files while making
the backup readable from a fresh home.

## Linked Issues or Issue Description

Refs #19137 and #19108 for the existing pre-copy and encrypted backup
owners.

**What happened?**

Online pre-copy reports `source_not_successful` for a sealed legacy
backup from a settled failed or cancelled run. The provider files remain
tied to the old disk.

**Expected behavior**

Copy the exact recorded backup when the source has no remaining writer
ownership. Preserve the run status, warnings, result, allocation,
selected session, and local files.

**Steps to reproduce**

1. Create a lease-stamped suspended native backup.
2. Settle its source as a failed run with a terminal-failure coordinator
and no owner.
3. Keep an unrelated execution active.
4. Run canonical online pre-copy. The previous owner defers the source.

## What Changed

- Add a narrow preservation owner for terminal source files. It checks
the physical finalization lock, task, coordinator, run, selected
session, source lease, and source fingerprints.
- Recheck that owner before the encrypted upload receipt and
current-backup publication.
- Reuse the finite workspace keepalive during long source checks and
defer source-local invalid archives.
- Add optional exact-root stamp verification. A valid previous backup
cannot authorize a changed current payload. Existing callers keep their
fallback behavior.
- Bind the archive input to the checked owner and record an activity
event after publication.
- Keep pending object receipts after an uncertain or failed publication.
- Add focused PostgreSQL and filesystem controls. Update the existing
migration runbook.

## Verification

- Real PostgreSQL baseline on unchanged production: the new positive
case fails with `source_not_successful` instead of publishing the
backup.
- Same case passes after the change and decrypts/restores the exact
provider history.
- Final head 4b6cf093be: three focused suites pass, 81 tests in 62.26
seconds, including existing storage and stamp consumers. The canonical
PostgreSQL wrapper exited zero and completed cleanup.
- Controls cover cancelled and failed sources, real committed results,
expired nonnull owners, physical owner/lock contention, late
source/owner changes, previous-tree fallback, and archive root/scope
binding.
- Both review regressions fail with the committed pre-fix owners: a real
30-second database idle timeout expires during a 32-second exact-source
check, and a stamp-valid invalid archive stops the pass. The fixed owner
keeps its transaction live and copies the next valid source. Source
hashes and automatic file restoration were recorded for each leg.
- Final-head strict server TypeScript and module-boundary checks pass
after the master rebase and required Runner/shared declaration rebuild.
- No source migration, provider mutation, or archive apply was executed.

## Risks

This path does not reconstruct missing backups or prove that missing
provider state is recoverable. Such sources remain blockers. It does not
take expired owners or report failed work as successful. Source hashing
adds bounded disk work while source row locks are held; unrelated
executions remain active in the PostgreSQL fixture. Existing archive
size limits, authenticated encryption, object readback, and publication
fencing remain in place. No schema, API route, scheduler, or
rollout-policy change is included.

## Model Used

OpenAI Codex (GPT-6 family; exact runtime model ID and context capacity
are not exposed in this session), with reasoning, repository editing,
code execution, and tool use.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 21:08:27 -06:00
DottaandClaude Opus 5.5 4612fa9fb4 fix(native): restart a drained Daytona run whose runner opened its session (#19711)
## 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. A graceful shutdown (every rolling deploy) suspends live native
runs "without cancelling provider work". The next boot then decides how
each run continues.
> - #19583, #19616 and #19684 let restart recovery start a remote
bootstrap again when durable evidence proves that no provider work
exists.
> - A chaos replay found another window. The drain suspended a remote
run just after its sandbox runner opened the provider session, before
the controller sent a turn and before a session snapshot existed.
> - Recovery accepted only an unfinished `session.open` in the
controller checkpoint, so it blocked the task as ambiguous. A Retry
recovered it, but automatic continuation was lost.
> - A related replay showed that the same-run bootstrap reuses the
sandbox. The interrupted runner can still hold the runner ingress port
there (`runner_ingress_bind_conflict`).
> - This pull request proves the "session opened, no turn" case and
stops the interrupted runner before relaunch.
> - The benefit is that a deploy during Daytona startup continues
automatically, without a port conflict.

## Linked Issues or Issue Description

**What happened?**

A chaos replay (ACPX Claude on Daytona) sent SIGTERM while a remote run
was starting. The drain recorded `restart_suspended`. The dying
controller still launched the runner and finished `session.open`. On the
next boot, restart recovery returned
`ambiguous_remote_recovery_evidence` with disposition `blocked`.

The decrypted controller checkpoint of that run showed:
- `run.prepare` completed and `session.open` completed (provider session
opened).
- Committed events `session.started`, `session.capabilities.updated` and
`session.goal.snapshot` (`ackedSourceSeq: 3`).
- No turn command. No run-profile session snapshot and no projected
provider events.

On the next run in that replay, a same-run bootstrap relaunched the
runner in the same sandbox (one lease for all attempts). It failed with
`runner_ingress_bind_conflict: failed to bind listener: Address already
in use`, because the interrupted runner from 51 s earlier was still
listening there. The bounded same-run retry recovered it on its last
attempt.

**Expected behavior**

When the durable controller record proves that no turn was ever sent,
recovery starts the bootstrap again. It first stops the interrupted
runner in the reused sandbox.

**Steps to reproduce**

1. Start a native run on a Daytona environment.
2. Send SIGTERM to the control plane while the runner opens its provider
session.
3. Restart the control plane after the controller lease expires.
4. See that restart recovery blocks the task with
`ambiguous_remote_recovery_evidence`.

Root cause: in
`server/src/services/native-runtime/native-restart-recovery.ts`, the
remote checkpoint proof (`remoteBootstrapProvenIncomplete`, #19583)
required an unfinished `session.open` with no committed events, and no
provider events at all. A completed `session.open` with only
session-open events had no rule, so it fell through to
`ambiguous_remote_recovery_evidence`.

Refs #19583, #19616, #19684, #19524

## What Changed

- `native-bootstrap-proof.ts`: new `runnerdStateHasNoProviderTurn`
proof. The state must hold only `run.prepare` and `session.open`
(pending or completed) and no warm, staged or adopted state. Every
committed event must be a session-open event, with contiguous sequence
numbers from 1, so no event is hidden. Only the controller issues turn
commands, and it writes each one to Postgres before sending it.
- `native-runner-checkpoint.ts`:
`nativeRunnerCheckpointRemoteBootstrapProof` replaces
`nativeRunnerCheckpointProvesIncompleteBootstrap`. It also reads a
bounded number of `event:N` parts and returns which proof holds. It
still rebuilds the state in memory.
- `native-restart-recovery.ts`: a remote run can now claim
`bootstrap_incomplete` with reason `remote_session_opened_without_turn`.
Projected provider events may only be session-open events
(`harness.ready`, `session.started`, `session.resumed`,
`session.updated`), and only together with the stronger session-opened
proof. A turn or provider event still blocks.
- `native-session-executor.ts`:
- For a remote sandbox target, the executor accepts the same proof on
the restored record.
- Before the same-run bootstrap relaunches,
`stopStaleRemoteRunnerBeforeBootstrap` stops the interrupted runner in
the reused sandbox. It uses the verified identity marker and process
check from reattachment, which signals only a process whose identity
matches this run: SIGTERM, a wait of up to 10 s, then SIGKILL.
- The stop is best effort. If no prior runner can be verified, nothing
is stopped, and the bounded same-run retry still handles any conflict.
- `doc/run-log-events.md`: document the reason and the stop.

## Verification

- Red, then green: `cd server && npx vitest run
src/services/native-runtime/native-runner-restart-recovery.integration.test.ts
-t "drained remote"`. Before the fix, the opened-session case (with a
projected `session.started`) returned `blocked` /
`ambiguous_remote_recovery_evidence`.
- These keep the hold: a `turn.start` command, a committed
`turn.started`, an acknowledged event that is missing from the record,
and a projected `turn.started`. The executor test restores the
checkpoint and checks that the local proof is false and the remote proof
is true.
- Regression: `native-runner-restart-recovery.integration`,
`native-session-executor`, `native-restart-recovery`,
`native-bootstrap-retry.integration` and `heartbeat/recovery`: 1460
passed, 14 skipped.
- Stale runner stop: controlled sandbox-runner tests
(`native-session-executor.test.ts`, "stale runner stop before a same-run
remote bootstrap") run the real reattachment identity and process
scripts against a fake sandbox runner:
  - a verified runner that exits on SIGTERM gets SIGTERM only;
  - a runner that ignores SIGTERM gets SIGTERM, then SIGKILL;
  - a runner with another identity gets no signal;
  - no live prior runner means no signal.
- Live Daytona proof (wave `w-20261107T021837-s29`, ACPX Claude,
`--faults K2 --triggers time,result`, 4 tasks, concurrency 1). The image
was built from current master
(`paperclip-daytona-runner@sha256:71f711ab…`), and its content id
matches this branch.
- All 4 tasks crossed a graceful restart, and all passed R1. Two of the
K2s fired on the `result` trigger, during a running remote turn.
- The K2 during a turn reattached to the live remote runner
(`reattach_remote_runner`, generation 2), as designed.
- Task t01 failed R6 with an orphan Daytona sandbox that no lease
references. The graceful restart came during sandbox creation. This is a
separate signature, outside this change.
- The timed faults did not land in the exact "session opened, no turn"
window. The integration tests cover that window with the evidence from
the saved replay database.

## Risks

- The proof relies on two facts: only the controller issues turn
commands, and the controller writes each command before sending it
(`#sendJson` waits for the checkpoint flush). A turn always leaves a
`turn.start` command in the record.
- The provider session opened in the old sandbox runner is not resumed.
The new bootstrap opens a fresh session. No turn ran in the old one, so
no work is lost.
- The runner stop sends signals inside the run's own sandbox. It signals
only after the existing reattachment identity and process checks pass.

## 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 20:58:32 -06:00
18be7ac696 fix(server): retire idle native sessions before tenant sleep (#18988)
## Thinking Path

> - Paperclip lets people manage AI agents and their work.
> - Native agents can retain a warm session between useful turns.
> - That owner must close and save its files before the tenant can
sleep.
> - A failed close must keep the owner visible until retirement
succeeds.
> - This PR prepares idle resources under the exact current admission
hold, then takes a fresh safety snapshot.
> - Busy turns, uncertain closure and changed holds still prevent sleep.
> - CI also exposed a chat authorization race. The fix repeats full
authorization after the exact delivery commits.

## Linked Issues or Issue Description

Refs #18639 for local work and incoming wake boundaries. Related #18829
covers suspension receipts after admission.

**What happened?**

Idle preflight did not retire warm Native owners. Three failed-close
paths could forget an owner without proving that its provider stopped.
CI also found a false chat rejection when a delivery changed from
processing to processed between two validation reads.

**Expected behavior**

Retire only idle owners under the exact bounded hold. Wait for closure
and file collection. Retain failed owners. Scan safety after
preparation. Keep diagnostic reads passive. A newly committed chat
delivery must pass the full current authorization checks before
attestation succeeds.

**Steps to reproduce**

Finish a warm Native turn. Start an idle drain and request safety with
its owner ID. Delay or reject provider closure, or replace the hold
during preparation or scanning. The report must keep the instance awake.
For the chat race, commit the exact delivery between the first binding
read and its fallback read.

**Paperclip version or commit**

Current head `7f6f02b611dcf5a9a2b05173a44e9631eaf78058` normally
integrates actual master `1aa0c3639714f4976cfa56f95aae6e2b7d1235ac`. The
merge preserves every unrelated master path. Fresh exact-head CI and
complete review remain required.

## What Changed

- Retain uncertain Native owners after failed restart, idle timeout and
failed-turn closure.
- Prepare Native retirement through the existing heartbeat facade. Reuse
provider closure and stopped-file collection.
- Track preparation under the exact idle owner and expiry. Reject new
activity, changed holds and expired holds before and after awaited work.
- Take the final durable and local safety snapshot after preparation.
Keep diagnostic inspection passive.
- Cover ephemeral workspace retirement and useful next use, supported
plugin drain and SDK persistence, busy owners, failed closure and hold
replacement.
- Keep dedicated recovery storage explicit. Use the same validated
options for support and storage. Probe-free helpers retain the original
implementation bodies and preserve default callers.
- Complete the Native fixture through real retirement and budget
evaluation. Keep its rejection, file, credential, accounting and strict
NONE assertions.
- Repeat the complete chat authorization once if the fallback sees the
exact batch now processed. Preserve owner locks, principal and binding
checks, denial handling and the original retry deadline.

## Verification

- The support-mode regression reproduces one failure with four passes
under the old policy, then passes all five controls. It retains prior
allocation and cleanup cases. The explicit mode does not probe embedded
PostgreSQL or use an admin alias.
- The deterministic chat race baseline reproduces two authorized
failures with all four current-authority denial controls passing. Both
Slack and Discord use real PostgreSQL and the real authorization
function. The negative setup commits before the attestation transaction;
it does not claim to revoke access through a row lock.
- The product fix passes normal own App declaration emission with no
public declaration changes, plus both current normal owning compiler
programs (399 and 611 roots). The same six controls pass after the fix.
The full original file plus these controls passes all 30 tests with no
skips.
- The original controlled Native continuation passes: one test, no
failures or skips, 1.718 seconds in its body and 23.138 seconds in its
launcher. The original 40-second case and 180-second outer bounds are
unchanged. It verifies held closure, strict NONE, an ordinary authorized
comment, saved session/files, useful output, encrypted credentials,
accepted results and settled accounting. All ten observed processes and
groups close; worker connections and listeners reach zero. A separate
post-owner audit finds zero sessions; the successful owned database is
normally removed.
- Earlier preparation, HTTP, lifecycle, workspace and plugin controls
remain source-specific evidence. They are not a claim that every
final-tree test ran locally.
- The original CI assertion log is retained. The controlled race proves
a possible cause; the original CI SQL schedule was not captured. Two
other jobs lost their self-hosted runners and provided no test verdict.
Earlier asset timeouts, readiness failures and unexplained later
recovery remain separately recorded. No timeout, failure predicate or
authority check was weakened.

Focused public commands:

```sh
pnpm exec vitest run server/src/__tests__/idle-sleep-preparation.test.ts server/src/__tests__/instance-settings-routes.test.ts --maxWorkers=1 --no-file-parallelism
pnpm exec vitest run server/src/__tests__/heartbeat-reviewed-chat-binding.integration.test.ts --maxWorkers=1 --no-file-parallelism
```

Local database qualification uses fresh owned test databases on existing
PostgreSQL, bounded pools and one worker. The local allocation adapter
changes only database allocation for this test module; it runs normal
migrations and client cleanup. It is not part of the published source.
No new cluster is created.

Local acceptance measured the `0a69efa6` integration with the same
committed authorization bytes. The later normal master merge adds
startup-timeout classification and recovery changes. Those paths are
outside the controlled healthy-backend witness; retirement and
authorization hunks are preserved. Exact-head remote CI at
`7f6f02b611dcf5a9a2b05173a44e9631eaf78058` has completed with 56
successful contexts and four skips. The complete current Greptile
summary is5/5 with no new actionable findings; all review threads are
resolved. Ponytail’s current turn is explicitly paused without a
current-head verdict; earlier actual findings are fixed and accounted
for. No whole final-tree local repository build/test run, live-provider
restore or deployed suspension is claimed.

Actual master subsequently advanced to
`d5c4f7dd02d408a261cedd9a1926b9900a4aedc4`. Its local normal-merge
projection with this reviewed head preserves all20 owned paths and every
unrelated master blob and mode. Current Runner TypeScript, App
declarations and their full owning compiler programs pass. Focused
Native cleanup and warm-continuation composition checks are being
completed before merge; the new upstream suspension-retry behavior is
not inferred from the older controlled-backend witness. The remote PR
head remains unchanged.

## Risks

- Privileged idle preparation can close idle warm Native sessions. It
requires the exact current hold.
- Failed closure retains its owner and can keep the tenant awake until
normal retry succeeds.
- A changed hold cannot authorize sleep. Started cleanup remains tracked
until it settles.
- The chat repair can perform one extra full authorization read. It
keeps the existing deadline and does not grant authority from the
fallback query.
- Unsupported resources and active work retain their existing blockers.
This PR does not certify every transport, plugin or deployed host.

## Model Used

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

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (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 at exact head
`7f6f02b611dcf5a9a2b05173a44e9631eaf78058`
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
at the exact current head
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Codex <noreply@openai.com>
2026-11-06 20:49:30 -06:00
DottaandPaperclip 55c9c44e41 fix: preserve database-only failed controller evidence (#19714)
## Thinking Path

> - Paperclip preserves failed work as well as successful work.
> - Ephemeral enrollment must retain each selected controller source.
> - A failed source can already exist entirely in an encrypted database
checkpoint.
> - Its original inline snapshot does not create a local filesystem
dependency.
> - The migration owner must prove that both legacy files are absent
before accepting that case.
> - This change preserves the failed evidence without creating execution
authority.

## Linked Issues or Issue Description

Refs: #19678.

**What happened?**

Startup still reports `native_controllers_require_migration` for a
failed retained source with a valid database checkpoint, its original
inline snapshot, and no local controller or supervisor. The online
archive owner reports that the source is already absent. The existing
database-only preservation case accepts only a missing inline snapshot.

**Expected behavior**

Accept the exact database-only failed evidence after full checkpoint and
identity validation. Keep a present local payload blocked. Retain the
failed run, original snapshot, coordinator, task pointer and queued
work.

**Steps to reproduce**

1. Store a valid encrypted checkpoint for a failed native run.
2. Keep its matching inline session snapshot and terminal-failure
coordinator.
3. Remove both legacy controller files.
4. Check local controller obligations.
5. The old owner reports an obligation even though the evidence is
already in the database.

**Paperclip version or commit**

The fix builds on current master `d5c4f7dd02`.

**Deployment mode**

Hosted native Runner with durable controller checkpoints. The local
regression uses the existing real PostgreSQL and encrypted checkpoint
fixture.

## What Changed

- Add a narrow database-only failed-source preservation case to the
existing migration owner.
- Require the decoded checkpoint, protocol header, admitted execution,
selected run and inline snapshot to match their exact scope.
- Require both the controller and supervisor paths to be absent. A file
or link keeps the local obligation blocked.
- Retain the earlier null-snapshot predicate and all original failed
execution evidence. Create no recovery metadata or executable authority.
- Exercise the actual startup and maintenance consumers. Document the
preservation boundary.

## Verification

- Final revision `1b1da543dafd576e3c420657e3c2c6d0c882172d`: all checks
are terminal, with 56 successful and four skipped, zero pending and zero
failures. The [required CI
aggregate](https://github.com/paperclipai/paperclip/actions/runs/42329501690/job/128275664414)
passed. Build, types, server/Runner/serialized suites, end-to-end
aggregate and Snyk passed. [Fresh exact-head
review](https://github.com/paperclipai/paperclip/pull/19714#issuecomment-6701018015)
is 5/5 with no open threads.

- Review found that the first non-recovery assertion stopped at the
queued fixture's missing current binding. The fixture now binds the
exact native issue/session before calling the real resolver and asserts
`unsupported:metadata_format` from the selected failed source. That
corrected case passes in 4.58 seconds, with wrapper exit 0 and cleanup
complete. The production predicate is unchanged. The original startup
RED remains valid. The final source/test type graph, module boundaries
and diff checks pass. Fresh revision CI and review remain required.

- The actual startup regression failed before the source change: one
case, 16.62 seconds, expected the absent-files obligation to be false
but received true. The wrapper exited 1 and completed cleanup. This
failure is retained.
- The final focused owner run passed nine cases in 10.79 seconds. It
covers the new DB-only case, controller/supervisor presence, every
inline scope field, foreign checkpoint Runner/session identities, the
unchanged null-snapshot case, and existing online/offline archival
controls. The wrapper exited 0 and completed cleanup.
- The new case verifies unchanged run, coordinator, checkpoint and
task-session rows, retained queued work, no upload, and no invented
recoverable session.
- The full qualified server/test source type graph passes with
db/shared/Runner source aliases. Module boundaries and diff checks pass.
Required broad package builds and regression suites remain in CI.
- Read-only source diagnosis validated one exact existing checkpoint
with the canonical decoder and matching header, run, Runner and session
bindings. No source profile or secret values were emitted. No hosted
mutation, admission hold, cancellation, status change, or
writers-stopped claim was performed.

## Risks

- A false database-only classification could hide local bytes. Both
exact legacy paths must be absent, and every protocol and database
binding must match.
- Corrupt or mismatched checkpoints still fail closed. Existing failed
evidence does not become successful or executable.
- The original null-snapshot case is unchanged. Present local files
still require the existing preservation path.

## Model Used

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

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 20:45:29 -06:00
Nicky Leach 03498f90ee Add explicit Runner session release and resume support (#19683) 2026-11-06 18:31:23 -08:00
DottaandClaude Opus 5.5 a894b11a2b fix(runner): retry a rejected runner suspension and log every cleanup quarantine (#19653)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A native run's close must prove that its runner durably suspended
before the session's next run may start. If it cannot, the runtime
quarantines the session (`NativeSessionCloseUnrecoverableError`)
> - In a restart chaos storm, an ACPX Claude run succeeded and its
normal close quarantined its own session. The follow-up run on that
session failed 2 s later with `native_session_cleanup_quarantined`, and
no control-plane restart came between the two runs
> - The close sent `runner.suspend` once, the ACPX sidecar rejected it
right after the turn completed, and the barrier gave up at the first
rejected command. No log line showed the quarantine or its cause
> - This pull request retries a rejected suspension within the close
deadline, and logs every cleanup quarantine with its cause when it is
created
> - The benefit is that a successful run no longer blocks its own
follow-up because of a one-time suspension rejection, and any remaining
quarantine can be traced

## Linked Issues or Issue Description

**What happened?**
Run A (ACPX Claude) succeeded. The run log of its close shows:
- `runner did not prove durable suspension before checkpoint`
- `native_session_settlement_incomplete
{"runnerSuspended":false,"providerDrained":true,"suspensionState":{"commandStatus":"failed","runnerLifecycle":"suspended",...},"semanticTools":{"pending":[]},"finalProviderState":{"activeProviderTurnId":null,"providerSettled":true}}`

The runner journal shows `runner.drain` completed, then `runner.suspend`
failed with `failed to suspend ACPX provider: ACPX sidecar command
session.suspend was rejected (retryable=false,
classification=unclassified)`. The follow-up run B on the same session
was created 200 ms after A finished and failed 2 s later at admission
(`retryQuarantinedSessionCleanups`, operator recovery branch). The
server log had no line for the quarantine. One Retry completed the task.

**Steps to reproduce**
1. Run an ACPX task whose sidecar rejects the first `session.suspend`
after the turn completes.
2. Start a follow-up run on the same session.

**Expected behavior**
The close asks again, proves suspension, and the follow-up runs. If a
quarantine is still needed, it is logged with its cause.

**Actual behavior**
The close fails on the first rejection, quarantines the session, and the
follow-up fails with no logged cause.

## What Changed

- `runner/.../durable/runner.rs` (`process_command`): a failed
`runner.suspend`, on first execution or replay, now keeps the runner in
`Continue` instead of `Suspend`. Before, the runner recorded `suspended`
over a provider whose session was still open, then exited. The RC-34
journal shows exactly that: runner lifecycle `suspended`, ACPX provider
`session_open`. A failed `runner.shutdown` still stops the runner.
- `durable-prp-control-plane.ts`: a failed or rejected `runner.suspend`
now lifts the connection's terminal-lifecycle fence, so the next pending
command (the fresh suspend) is delivered on the same connection.
Completed suspensions and every shutdown keep the fence. New test:
"delivers a fresh suspension after a rejected one on the same
connection". Before the fix, the retry was never sent and the test timed
out waiting for the frame.
- `runnerd-codex-transport.ts` (`awaitRunnerSuspensionBarrier`): after a
rejected `runner.suspend`, the barrier sends a fresh suspend command, up
to 3 attempts, 250 ms apart, inside the same close deadline. An exited
runner or a spent budget still fails closed. A rejection changes no
state: the runner returns before it records suspension.
- `native-session-runtime.ts`: new
`onNativeSessionCleanupQuarantined(listener)`. It is called when a
cleanup quarantine is created or escalates to operator recovery, with
company, run, issue, session, `operatorRecoveryRequired`, a bounded
cause (error name, code, message) and bounded settlement facts.
- `server`: `logNativeCleanupQuarantines` logs each quarantine as a
warning. The subscription is process-wide and installed once, so closes
that fail during shutdown, after HTTP closes, are still logged.

## Verification

- Red before the change: `pnpm --filter @paperclipai/paperclip-runner
exec vitest run src/live/runnerd-codex-transport.test.ts -t "rejected
suspension"`.
  - "retries a rejected suspension…" got `false`.
  - "stops retrying…" saw 1 attempt instead of 3.
- Green after the change: both pass.
- Rust: new test
`rejected_suspend_keeps_the_runner_serving_its_controller` covers a
failed suspend, its replay, and an accepted retry with a fresh command.
`failed_lifecycle_commands_replay_their_terminal_transition` now expects
`Continue` for suspend and `Shutdown` for shutdown. The `pnpm run
check:runner` unit suites pass (559). `acpx_park_readiness` has two
timing tests (`< 500ms`) that fail at this host load on unchanged master
as well.
- New runtime test: "announces every cleanup quarantine with its cause
when it is created" (operator-required quarantine, cause and settlement
reported once). `src/native-session-runtime.test.ts` passes (212).
- The suspension cases in `runnerd-codex-transport.test.ts` pass, except
"proves local suspension after an event backlog… (1024 suffix deltas)".
That case also times out on unchanged master at this host load (27–35);
I confirmed this by stashing the change and rerunning.
- Server `tsc --noEmit` passes after the runner package build.

## Risks

- A provider that keeps rejecting suspension now gets three tries
instead of one, within the existing close deadline. The final outcome
and quarantine are unchanged.
- The cause of the ACPX sidecar's first rejection is not proven. The
logged evidence fits the sidecar's "not at a safe suspension point"
guard (`turnId`, tools or inputs still set) racing the just-completed
turn. This change makes close tolerate it. It does not change the
sidecar.
- Log volume: one warning per quarantine.
- The Rust change ships in the runner binary. Hosted images need a
rebuild to pick it up.
- After a failed suspend, the runner stays alive with its provider. If
every retry fails, close still treats suspension as unproven and kills
the owned runner process, exactly as before.

## Model Used

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

## Checklist

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

Refs #19532, #19609.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 20:06:54 -06:00
DottaandClaude Opus 5.5 30857db55e fix(native): recover a run killed between runner launch and its identity record (#19694)
## 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 server
records the request to start it, then records its process identity after
it spawns.
> - If the server is killed between those two records, there is no
process ID to check after the restart
> - Restart recovery then restarted the bootstrap, but the executor
failed with `durable_identity_unreadable` because the runner never wrote
its state
> - The user's Retry then waited forever with "Retry is waiting for
Paperclip to verify that the previous execution stopped", because the
stop check needs a process ID
> - This pull request restarts that bootstrap and proves on the host
that no runner for the run exists
> - The benefit is that a restart in the runner launch window no longer
loses the task or leaves Retry permanently stuck

## Linked Issues or Issue Description

**What happened?**
In restart-chaos testing, an ACPX Claude run was created and the server
was SIGKILLed 1.3 s later, right after `native.process_start_requested`
and before the runner identity was recorded. On restart, recovery
claimed `bootstrap_incomplete` (`dead_runner_before_provider_identity`).
The executor then threw `runner_state_identity_mismatch:
durable_identity_unreadable`: the state root existed, but
`control-plane/control-plane-state.json` had never been written. The run
failed with `native_continuation_requires_reconciliation`. A Retry was
accepted (HTTP 200), but no run was ever created. The issue stayed
blocked with `retryWait.reason=process_identity_missing` for over 20
minutes and through another restart.

**Expected behavior**
When the runner never wrote state, no provider started, so the bootstrap
can start again. A Retry should be admitted once the host proves that no
runner for the run exists.

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

**Steps to reproduce**
Create a native local task and SIGKILL the server about 1 s after the
run starts, after the runner launch is requested and before its identity
is recorded. Restart the server, then click Retry.

## What Changed

- `migrateRunnerdStateRootForExecution` (`native-session-executor.ts`):
for a `bootstrap_incomplete` restart on a local target, a root is
quarantined (kept for inspection) and the bootstrap starts again only
when both of these hold:
  - the root has no `control-plane-state.json`;
  - no live runner for the run exists on this host.

A state file that exists but cannot be read, or a runner that started
but has not connected yet, still fails closed with
`durable_identity_unreadable`.
- `unidentifiedLocalRunnerProvenAbsent` (`native-session-executor.ts`)
proves that a runner is absent when both of these hold:
- no host process carries the run's `--run-id <runId>` argument
(`paperclip-runnerd` is always started with it, detached and
unreferenced);
- no saved control-plane state for this run exists, in the canonical
root or in quarantine.

A provider starts only after its runner connects and writes that state.
- `native.process_start_requested` now records the launching `hostname`.
- `recordUnidentifiedLocalRunnerStop` (`native-local-process-stop.ts`)
applies the proof only when all of these hold:
  - the run is native;
  - the run has no process ID;
- its latest launch event is `native.process_start_requested`, from this
host;
  - all of its leases are local.

  A launch recorded without a host stays blocked.
- The process listing runs asynchronously with a 5 s deadline. A failed
or slow listing proves nothing.

It then writes `native.local_process_stopped` with null process IDs and
`basis: "unidentified_runner_absent"`. A later launch request
invalidates it as usual.
- The continuation and Retry gate (`explicit-native-continuation.ts`)
uses this proof before returning `process_identity_missing`. A dry run
checks the proof but records nothing.
- `doc/run-log-events.md` documents the new receipt basis.

## Verification

- `native-session-executor.test.ts`:
- The bootstrap restart with absent state was red before the fix
(`durable_identity_unreadable`) and is green after it (root quarantined,
backend admitted).
- Unreadable state, and a live runner that has not connected, still
reject.
- Proof cases: absent → true; a live process with `--run-id <runId>` →
false; this run's saved state → false; another run ID → false.
- `explicit-native-continuation.test.ts` (embedded Postgres, real
admission):
- The `absent` and `dry_run` cases were red before the fix (blocked) and
are green after it (admitted). Only the real admission records the
receipt.
- The `live_runner`, `saved_state`, `foreign_host` and `unbound_launch`
cases stay blocked.
- Broader local run after the review fixes:
`explicit-native-continuation`, `native-session-executor`,
`native-recovered-owner-followup`,
`native-runner-restart-recovery.integration` and `task-retry` gave 1836
passed, 0 failed.
- A chaos replay of the original task (`--replay
w-20261106T232854-s7103/t26`) passed (R1 PASS). Its kill landed after
the runner identity was recorded (`resume_dead_runner`), not in the
launch window, so the tests above are what reproduce that window.
- `tsc --noEmit` for `server` passes.

## Risks

- The proof depends on `ps -axww` listing every process on the launching
host. If `ps` fails or times out, the proof fails closed (still
blocked). Remote runners and other hosts are excluded.
- A runner that was spawned but has not yet written its state could be
mid-start when admission runs. It would still carry `--run-id` in its
arguments, so the proof refuses.
- Only the bootstrap path with no state file changes. Ownership,
checkpoint and reattach checks are unchanged.

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

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with
tool use (shell, file edits, test execution). Extended reasoning
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 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 20:06:36 -06:00
DottaandClaude Opus 5.5 803cb3bfe3 fix(native): re-bootstrap an OpenCode runner drained before its first turn (#19704)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs start a local `paperclip-runnerd`. After a restart,
recovery may start the bootstrap again when the durable state root
proves that no provider work started.
> - #19665 added a proof for a graceful drain that lands after
`session.open` started a fresh session and before the first turn. It
requires every recorded provider process to have exited.
> - OpenCode records provider startup ownership twice during
`session.open`. The first record comes before launch, with `processId:
null` and `processGroupId: null`.
> - The proof treated `processId: null` as a process it could not prove
gone. An OpenCode run drained in this window still failed with
`runner_state_identity_mismatch: bootstrap_not_proven_incomplete`.
> - This pull request treats a null process id as "no process yet". The
benefit is that a graceful restart during OpenCode bootstrap continues
automatically, as it already does for ACPX.

## Linked Issues or Issue Description

Refs #19665, #19524

**What happened?**
A restart chaos wave sent SIGTERM to the control plane about 11 s after
an OpenCode run started. The drain suspended the run. At the next boot,
recovery verified that the local runner had stopped and chose
`bootstrap_incomplete`. The executor then failed the run with
`adapter_failed` and `runner_state_identity_mismatch:
bootstrap_not_proven_incomplete`. The kept state root had:
- `run.prepare` and `session.open` completed, with a freshly started
session (`status: "started"`);
- only `session.started`, `session.capabilities.updated` and
`session.goal.snapshot` committed;
- two `harness.diagnostic` events of type `provider_startup_ownership`
in the runner outbox, the first with null process ids and the second
with the server pid and process group;
- no turn command, and the OpenCode server process had exited.

**Steps to reproduce**
1. Start an OpenCode task on a local runner.
2. Send SIGTERM to the control plane after `session.open` completes and
before the first turn.
3. Let the control plane restart.

**Expected behavior**
Recovery starts the bootstrap again, and the task continues.

**Actual behavior**
The run fails. The task needs a manual Retry.

## What Changed

- `native-bootstrap-proof.ts`: the process-id collector skips
`processId`, `pid` and `processGroupId` values that are `null`. A null
value records a launch that has not produced a process yet. Any other
non-number value still fails the proof, and every numeric id must still
be proven gone.
- Tests: a red test with the OpenCode startup-ownership shape, plus
counter-tests for a live OpenCode server process, a non-numeric process
id, and a started turn.

## Verification

- Red before the fix: "allows a fresh bootstrap after a drain once
OpenCode opened its session before the first turn" failed with `expected
false to be true`. Green after the fix: the `native incomplete-bootstrap
evidence` block passes 36/36.
- I ran the proof read-only against kept real roots:
  - the OpenCode drained root now returns `true`;
- an OpenCode root from the same wave where a turn ran (108 events)
still returns `false`;
  - the three roots from #19524 and #19665 still return `true`.
- `native-runner-restart-recovery.integration.test.ts` and
`native-restart-recovery.test.ts`: 134 passed. `tsc --noEmit -p
server/tsconfig.json` passes.
- Live replay: see the PR comment.

## Risks

- Low. The change only skips null ids. Numeric ids still need a proven
exit. Any turn, provider output, resumed session, or remote runner still
fails closed.

> 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 19:59:34 -06:00
DottaandClaude Opus 5.5 c37bc749ac fix(native): keep restart continuations alive through early results and repeat restarts (#19699)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs continue a turn lost to a control-plane restart once, in
the same conversation (#15297, #19501)
> - In restart storm 3, two Codex runs each crossed one restart and
still needed a Retry
> - The continuation was attempted, but the driver failed it with
`turn_binding_mismatch`, and the run then reported "runnerd exited
before provider turn startup"
> - A second restart that loses the continuation turn itself also needed
a Retry, because the one-shot marker was already used
> - A restart should not cost a click when continuing is safe, and a
failure should name its real cause
> - This pull request fixes the start race, reports the driver's own
failure, and gives each admitted restart its own continuation
> - The benefit is that interrupted tasks finish without a Retry, within
the existing recovery budget

## Linked Issues or Issue Description

Refs #19501, #15297, #19658, #19539

**What happened?**

Restart storm 3 (seed 7103, RC06 mask on, load 9–12). Codex B5 Retry
runs crossed one K1 restart mid-turn. After the restart, recovery
resumed the dead runner. The runner restored the conversation and
recorded `turn.failed` with `provider_turn_lost_on_restore`
(`recoverable: true`). The control plane started the same-conversation
continuation (`resumeInterruptedTurn` → `startTurn`). About 1 s later
the transport closed with `protocol_failure:turn_binding_mismatch`. The
run failed as `native_runner_process_exited: runnerd exited before
provider turn startup`. The task needed another Retry.

I reproduced it with the focused cell (`--workloads B5 --faults K1
--triggers retry`) and a temporary diagnostic. The failure is "Provider
semantic result did not name the active turn": a `paperclip/runResult`
reached the Codex driver while the continuation's `turn/start` was still
pending and no turn was bound. The driver waits for a pending start
before it handles `turn/completed`, but not before it handles
`paperclip/runResult`.

**Expected behavior**

After a restart that loses a turn, the run continues automatically when
that is safe. Each admitted restart attempt can continue once, within
the provider-attempt budget. A failure names its real cause.

**Steps to reproduce**

1. Run `node cli/node_modules/tsx/dist/cli.mjs
tests/restart-chaos/main.ts --env local --adapters codex --workloads B5
--faults K1 --triggers retry --tasks 2 --concurrency 2 --seed 4040
--rc06-masked`.
2. When a lost turn's continuation receives a semantic result before its
start settles, the run fails with `turn_binding_mismatch` and the task
needs a Retry. The race is intermittent: 1 of about 14 lost turns in my
runs.

**Root cause**

-
`packages/paperclip-runner/src/drivers/codex/codex-session-notifications.ts`:
the `paperclip/runResult` branch checked the active-turn binding without
waiting for `state.turnStartSettled`. The `turn/completed` branch
already waits.
- `codex-harness-session.ts` `startTurn`: the driver's own protocol
failure closed the transport. The pending start then reported the runner
exit, which hid the cause.
- `codex-session-terminal.ts` `mapTerminalTurn`: a consumed one-shot
marker blocked every later continuation in the run, even when the newly
lost turn was that continuation itself.

The one-continuation allowance is per run, not per issue or session
lineage: attaching a new run resets the marker. The storm runs did
attempt their continuation; the start race made it fail.

## What Changed

- Codex driver: a `paperclip/runResult` waits for a pending turn start
to settle before it is bound, like a terminal.
- Codex driver: when this session's own protocol failure ends a pending
turn start, `startTurn` throws `codex_turn_start_protocol_failure:
<code>: <message>` instead of the runner exit. CodexRpc errors and
protocol-integrity errors keep their own types. The server now
classifies this as `native_session_interrupted` instead of
`native_runner_process_exited`; both are retryable.
- A later restart that loses the continuation turn itself re-arms the
one-shot marker: the driver resets it as it was before the first
continuation. It does this when it maps the lost terminal, including
during recovery from a checkpoint taken while the continuation ran. The
checkpoint gate `nativeRestartInterruptedTurnId` stays strict: a
consumed marker still means the allowance is used. Each continuation
still needs an admitted dead-runner restart. The existing
provider-attempt budget bounds the repeats, and an exhausted budget
still blocks with Retry.
- `doc/execution-semantics.md`: an additive paragraph.

## Verification

Red before the fix, green after:

- `codex-app-server-driver.events.test.ts` "binds a semantic result that
arrives before its pending turn start settles". Red: `session.failed`.
Green: the result binds to `turn-1` and the turn completes.
- `codex-app-server-driver.events.test.ts` "names its own protocol
failure when that failure ends a pending turn start". Red: "runnerd
exited before provider turn startup". Green:
`codex_turn_start_protocol_failure: turn_binding_mismatch`.
- `codex-app-server-driver.recovery.test.ts` "offers another restart
continuation when a later restart loses the continuation turn itself".
Red: `null`. Green: `turn-2`. A checkpoint with a consumed marker still
returns `null`.
- `codex-app-server-driver.recovery.test.ts` "re-arms the continuation
when recovery finds the checkpointed continuation turn lost". This
recovers from a checkpoint taken during the continuation, then starts
the next continuation. Red without the re-arm (the marker stays
consumed); green with it.
- `native-runner-restart-recovery.integration.test.ts`: the same
checkpoint (continuation active, consumed marker naming it) is claimed
as `resume_dead_runner`. This is regression coverage.

Suites run locally on the final code:

- Runner: Codex driver, `native-session-runtime`,
`runnerd-restart-continuation` and contracts (745 tests).
- Server: `native-runner-restart-recovery.integration.test.ts`.
- `tsc` for the runner tests and the server.
- A few real-process tests timed out at about 6 s while six live agents
ran. Each passed when rerun alone. The settlement suite passed both with
and without this change at the same load.

Live (local, real Codex, macOS arm64), focused restart-chaos cell
`--adapters codex --workloads B5 --faults K1 --triggers retry,time
--tasks 6 --seed 4042 --rc06-masked`: 6/6 crossed a restart and passed.
R1 passed for all 6, so each continued without a Retry.

## Risks

- A run can now continue more than once in the same conversation, once
per admitted restart. Each continuation asks the agent to reconcile
unfinished actions before repeating them. The provider-attempt budget
still caps the repeats.
- A semantic result that arrives during a pending start now waits for
that start. If the start fails, the result is still rejected by the
existing binding check.
- The start race was intermittent (1 of about 14 lost turns), so the
live cell shows absence of the failure, not a forced repro. The driver
regression reproduces it deterministically.
- No schema, API or migration change.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 19:55:18 -06:00
DottaandClaude Opus 5.5 11c07c622c fix(native): restart a remote bootstrap retry that shutdown cut before runner launch (#19684)
## 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. Restart recovery decides what to do with each live or due run
after the control plane restarts.
> - #19616 added the `remote_runner_never_launched` proof. A claimed
remote run with no runner process request and no controller checkpoint
starts its bootstrap again.
> - A graceful shutdown closes the local runtime listener before its
drain. A remote run that is still starting then fails with
`local_runtime_api_listener_unavailable`. The executor settles it as a
same-run `bootstrap_retry` and releases its lease. #19596 treats this as
a restart detach only for local runners.
> - When the server restarted with that retry due, restart recovery
claimed the row. The proof accepted only rows that still held a lease,
so recovery blocked the task as ambiguous.
> - This pull request lets the same proof also admit that settled
`bootstrap_retry` row.
> - The benefit is that a graceful restart during Daytona startup
continues automatically.

## Linked Issues or Issue Description

**What happened?**

A chaos wave restarted the control plane gracefully (SIGTERM) while an
ACPX Claude run on Daytona was starting. At 2 s after `stage.workspace`,
the run failed with `local_runtime_api_listener_unavailable`, and the
executor persisted "same-run resume persisted" with `recoveryMode:
bootstrap_retry`. After the restart, restart recovery returned
`ambiguous_remote_recovery_evidence` with disposition `blocked`. The
task rested blocked.

Evidence in the saved database at that point:
- Coordinator `phase: retryable_failure`, `attempt: 1`,
`controllerGeneration: 1`, controller identity recorded, and no lease
owner.
- `failureDetail` recorded `recoveryMode: bootstrap_retry`,
`checkpointExists: false`, `providerEventsExist: false` and
`providerSessionEstablished: false`.
- The run had no runner checkpoint rows, no
`native.process_start_requested`, no process identity, and no
runner-sourced event. It also had no PID and no provider session.

**Expected behavior**

Restart recovery starts the bootstrap again, as it does for a claimed
run that holds a lease (#19616) and for a local run.

**Steps to reproduce**

1. Start a native run on a Daytona environment.
2. Send SIGTERM to the control plane after workspace staging and before
the runner is requested. The run settles as a same-run
`bootstrap_retry`.
3. Restart the control plane while that retry is due.
4. See that restart recovery blocks the task with
`ambiguous_remote_recovery_evidence`.

Root cause: in
`server/src/services/native-runtime/native-restart-recovery.ts`, the
`remoteRunnerNeverLaunched` condition required `phase === "observed"`
and a lease owner. The settled `retryable_failure` / `bootstrap_retry`
row failed that check. The remote classifier then returned
`ambiguous_remote_recovery_evidence`.

Refs #19616, #19596, #19583

## What Changed

- `native-restart-recovery.ts`: `remoteRunnerNeverLaunched` now accepts
one of two coordinator shapes:
  - an `observed` claim that holds a lease (unchanged);
- a `retryable_failure` with `recoveryMode: bootstrap_retry`, run status
`failed`, and no lease owner.
- All other proof conditions are unchanged. The run must still have no
process start, process identity, adapter, tool or runner-sourced event.
It must still have no process fields, no provider session, no provider
evidence and no checkpoint. The same three-attempt budget applies.
- An `exact_checkpoint_resume` retry keeps its own admission path.
- Admission: before claiming a settled retry, restart recovery applies
the same admission as the resumption reconciler
(`readNativeSessionResumptionAgent`): task pause hold, agent status,
dependencies, ownership, and competing runs. An ineligible retry is
deferred with `native_retry_admission_ineligible` and keeps waiting.
- `doc/run-log-events.md`: note the settled `bootstrap_retry` case.

## Verification

- Red, then green: `cd server && npx vitest run
src/services/native-runtime/native-runner-restart-recovery.integration.test.ts
-t "bootstrap retry"`. Before the fix, the proven case and the budget
case returned `blocked` / `ambiguous_remote_recovery_evidence`.
- New cases: the proven case gets `bootstrap_incomplete`. The
coordinator returns to `observed`, `nextAttemptAt` is cleared, the run
returns to `running`, and no recovery action is created. A requested
process start or a committed runner event keeps the hold. A spent budget
blocks with `execution_recovery_budget_exhausted`. An
`exact_checkpoint_resume` retry does not take this path.
- Regression: `native-runner-restart-recovery.integration`,
`heartbeat/recovery`, `native-restart-recovery`,
`native-bootstrap-retry.integration` and
`native-remote-retry-admission`: 337 passed, 14 skipped.
- Admission: a paused agent's due `bootstrap_retry` is deferred with
`native_retry_admission_ineligible`. It is not started (red before the
follow-up commit, green after).
- Live Daytona replay of `w-20261106T234719-s12/t02`, 3 runs, ACPX
Claude B1, using the recorded K4, K2 and K1 sequence. Image: the wave's
own `paperclip-daytona-runner@sha256:2767e91c…`, with runner inputs
pinned to its revision e482f0f632 locally. The replays ran the first
commit of this PR.
- Runs 1 and 3 passed R1. Run 2 passed R3: one Retry worked on the first
click.
- Runs 1 and 2 failed R6 because the wave-end sweep found a run lease
still in `pending_cleanup`. In run 1 the environment delete also timed
out. These failures are not in this change's path.
- The restarts landed in other startup windows, so none hit this exact
`bootstrap_retry` state. The integration tests cover that state with the
same evidence as the saved failing database.
- Run 2 also exposed a separate gap. A graceful drain that suspended a
remote run after the runner launched (`awaiting_runner_reattach`) was
blocked as ambiguous on the next boot. The Retry recovered it. That case
is outside this PR.

## Risks

- The proof still depends on the executor writing
`native.process_start_requested` before a backend may spawn a runner.
The fail-closed tests pin that gate.
- Restart recovery and the same-run resumption reconciler can both see a
due row. Both claim with a conditional update on phase and attempt, so
only one can win.

## 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 19:54:04 -06:00
DottaandClaude Opus 5.5 9a52a2dfeb fix(native): retry ACPX session.open startup timeouts and give cold Claude a 60 s bound (#19661)
## Thinking Path

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

## Linked Issues or Issue Description

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

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

**Paperclip version or commit**

`master` at `6c7cc2c5d6`.

## What Changed

The bound (evidence below):

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

The automatic retry:

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

## Verification

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

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

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

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

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

**Tests:**

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

## Risks

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

## Model Used

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

## Checklist

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

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

---------

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

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

## Linked Issues or Issue Description

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

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

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

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

Refs #19532.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 18:52:46 -06:00
DottaandPaperclip a0ac81e114 fix(ui): show task image results in compact thumbnail grids (#19626)
## Thinking Path

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

## Linked Issues or Issue Description

**What existing behavior does this improve?**

Image results in task history and the live task feed.

**Current behavior**

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

**Proposed behavior**

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

**Reason and benefit**

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

**Breaking changes**

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

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

## What Changed

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

## Verification

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

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

## Risks

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

## Model Used

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

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 18:42:22 -06:00
DottaandPaperclip 72f7b42fe3 Preserve settled failed controller state during online migration (#19678)
## Thinking Path

> - Paperclip controls agent work across web-server releases.
> - Volume-free servers need retained controller state in shared
storage.
> - Successful settled sources already support online migration.
> - Some retained failed sources have no accepted result and must stay
available for recovery inspection.
> - This change copies those sources under task, coordinator and
publication fences while unrelated work continues.
> - The original files, failed status and recovery warnings remain
unchanged.

## Linked Issues or Issue Description

Refs #19038. Refs #19631.

**What are you trying to improve?**
Preserve retained failed controller files without waiting for global
idle.

**What problem does this solve?**
The online producer skips terminal failed controllers. Their files can
still be needed for recovery inspection even when no accepted provider
artifact proves executable continuation.

**What would the improvement look like?**
A settled exact source can enter the existing encrypted archive store.
An accepted committed failed source can use the existing checkpoint
import. Busy, changed, unowned or foreign sources remain blocked.

## What Changed

- Add a source owner bound to the selected task session, run,
coordinator generation, admitted input and file digests.
- Check the task, physical finalizer lock, released coordinator,
canonical resume source and active related turns before upload and
before publication.
- Preserve failed sources through the existing encrypted archive format.
Do not treat archived bytes as execution authority or old-writer stop
proof.
- Allow the existing accepted committed checkpoint import for a finished
failed run without changing its status.
- Validate admitted input bindings before looking up local controller
files.
- Document the online preservation path and the final static writer
fence.

## Verification

- Real PostgreSQL: 125 cases passed across migration, controller
recovery and durable backup storage. One added assertion inspected an
unadmitted queued fixture; it was corrected while retaining the
queued-turn preservation and stale-publication assertions. The final
migration rerun passes 51/51; the other three affected suites pass
75/75.
- Genuine prepatch RED: online failed-source preservation and accepted
committed failed-source import both fail on the previous producer and
recovery owners.
- New cases cover independent restore after deleting the original
source, an expired but non-null coordinator owner, source mutation after
upload, a new related queued turn during upload, foreign input binding
and an accepted committed failed result.
- The full rebased server source and test-tree typecheck passes after
restoring its missing ignored workspace SDK link and building that
existing dependency.
- Module boundaries and whitespace checks pass.
- Rebased onto current master; both provider-artifact imports and the
new archive owner are preserved. Rebased type and module-boundary checks
pass. All six archive cases pass on the exact rebased head (39.68
seconds). All CI gates pass on exact head
81fc974cd9. Fresh Greptile review is 5/5
with no unresolved comments. No serving migration, deletion or
deployment is performed by this PR.

- Review memory fix: 64 KiB streamed hashes avoid retaining another
controller string at each publication fence. A 256 MiB source is hashed
three times while the original string remains live under a 512 MiB heap.
The old full-string comparison reproduces an OOM; the fixed two-case
suite passes. Full source/test-tree typecheck and module boundaries pass
on the new code. All six real-PostgreSQL archive cases pass again with
streaming enabled (17.31 seconds); ownership, queued-turn and
post-upload mutation guards remain covered.

## Risks

- This is a copy-only preservation operation. It does not prove the
original process has stopped. Final enrollment must arm static ephemeral
mode on the original volume, observe the old deployment stopped and
repeat data eligibility.
- A source change during upload leaves an unpublished pending object.
The normal retention owner handles obsolete attempts; the source remains
required until a verified current archive exists.
- An archive is forensic evidence. It does not make an unaccepted failed
provider result resumable.

## Model Used

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

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 18:38:50 -06:00
Nicky LeachandPaperclip d6f4155bad Refresh checkpoint writers when controller ownership changes (#19679)
## Thinking Path

> - Paperclip manages AI agents and keeps their work attached to tasks.
> - Core saves native Runner checkpoints so a session can continue under
a later run.
> - The checkpoint records can still belong to source run A after run B
becomes the current owner.
> - The writer cache currently keeps A's first write authority even
after Core validates B.
> - This pull request renews that writer when the authority, boot,
generation, or lease owner changes.
> - Repeated registration under the same authority keeps its work
history. Stale and expired owners still cannot write.

## Linked Issues or Issue Description

Refs: #19611. Related: #19225.

**What happened?**

A checkpoint writer is cached by source run ID. After Core validates a
successor owner, a new registration still uses the cached writer for the
old owner. Saving new work can then fail with
`native_runner_checkpoint_owner_lost`.

**Expected behavior**

Use the validated current owner for later writes. Preserve the existing
writer when its authority has not changed. Continue to reject stale or
expired ownership.

**Steps to reproduce**

1. Register a source checkpoint writer and save its first command.
2. Complete the source run and give its task to a successor with a
current write lease.
3. Register the same source checkpoint under the successor and save a
new command. The unchanged code rejects this write.

## What Changed

- Store the write-authority fields with each cached writer.
- Replace the writer only when its authority run, boot, controller
generation, or lease owner changes.
- Add a PostgreSQL regression for successor handover, repeated
registration, generation renewal, stale writes, and expired leases.

## Verification

- The new regression fails on base `913102870d` at the successor's
checkpoint write.
- The fixed regression passes: `cd server && node
../node_modules/vitest/vitest.mjs run
src/services/native-runtime/native-runner-checkpoint-authority.integration.test.ts`.
- A focused strict TypeScript check covers the source, regression, and
their imports.
- The maintained general-server selector includes the regression. It
needs no native process or new CI group.
- The scoped diff passes whitespace and changed-line secret/PII checks.
- Targeted database and focused strict TypeScript checks pass. Required
CI will supply full regression coverage.
- CI and standard review are pending for this draft.

## Risks

A renewed writer starts a new in-memory work history. The authority
comparison limits that reset to a real ownership change. Each database
write still checks the current lease and generation in its transaction.
This change adds no schema, API, or automatic session-release behavior.

## Model Used

OpenAI GPT-6.1 Sol (`gpt-6.1-sol`), high reasoning, with repository
inspection, code execution, and test tools.

## 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
  N/A: This internal correction changes no documented interface.
- [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 16:36:33 -08:00
DottaandPaperclip a3eecf1fc9 Preserve admitted runtime skill bundles during server replacement (#19675)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native runs keep the exact skill files admitted for execution and
recovery.
> - Older inputs can name immutable local bundles without a skill
version ID.
> - Installed skill heads alone do not preserve those admitted bundles.
> - Server replacement needs a durable source for each retained bundle.
> - This pull request copies authenticated bundles through the existing
package owner and restores their exact projections.
> - The benefit is preservation during online migration without stopping
active work or settling recovery warnings.

## Linked Issues or Issue Description

Refs #19656. Related: #19660.

**What happened?**

Older native execution inputs can contain skill bundles with a null
version ID. Recovery skips those bundles. Startup installation checks
can pass even when those retained inputs still require the old
filesystem.

**Expected behavior**

Preserve each retained admitted bundle in object storage. Restore its
original bytes, modes and digests on an empty replacement host. Keep its
null pin and all run, controller and recovery state. Block startup and
enrollment while a concrete local asset obligation remains.

**Steps to reproduce**

Retain a native run with an admitted local skill bundle and a null
version ID. Convert its installed skill head to durable storage. Replace
the server home and attempt recovery. The old bundle path remains in the
recovered context.

## What Changed

- Add a bounded `runtime-assets` phase to the existing skill package
maintenance CLI. It copies exact admitted files under the runtime asset
publication lock, verifies the durable package, and appends a run-bound
receipt after source revalidation.
- Keep original files, null pins, task intent, warnings and controller
leases. Support an active run through copy-only preservation. Do not
start a turn or claim provider ownership.
- Verify and reuse a healthy canonical local bundle before storage
reads. Restore missing or damaged bundles through the existing package
and native asset materializers. Use the authenticated original manifest
to preserve exact read and execute modes. Accept a later contract
revision only when the run binding and exact skill selection remain
unchanged.
- Check retained recovery obligations and current task sessions during
both startup and migration eligibility. Ignore ordinary admission that
has no completed execution input.
- Protect receipt-backed packages from orphan collection. Serialize
receipt publication and deletion on the package row, then check
references in a fresh statement after the lock.
- Add real PostgreSQL and filesystem regressions. Replace a
delete-method proxy with an actual upload-renewal row-lock race. Publish
mutation activity only after commit.

## Verification

- Baseline production startup gate failed the new concrete-asset
regression as expected. The fixed gate passed. The actual before-fix
preservation owner also failed recovery from a healthy local bundle
during a simulated storage outage. A verified local-first path fixes
that regression.
- Eight preservation tests passed with real PostgreSQL, streaming
storage, archive extraction and native materialization. They cover cold
recovery, exact modes and binary bytes, active ownership, changed
sources and bindings, contract revisions, missing or corrupt receipts,
and publication/collection races.
- Six affected package collection/deletion controls and three existing
startup controls passed. The existing versioned recovery consumer passed
a sequential retry.
- One fixture attempt failed on its cleanup foreign-key order; the
fixture was corrected. One existing suite hit its unchanged 30-second
database startup hook before test execution. The unchanged single
consumer passed on retry. These failures remain recorded.
- Source, changed-test and CLI TypeScript checks passed. Rebase
preserved both the local instruction-copy and runtime-asset guards.
Current-head changed-test checks passed. Final PostgreSQL rerun passed
all eleven preservation/startup cases. Current CLI check passed.
Required CI remains pending at the trusted-runner gate.
- Current canonical bundled creation uses package preparation plus
atomic content/head publication. Native preparation requests immutable
version snapshots before delivery. Converted installations and new
canonical bundled creation therefore do not recreate the historical
null-version asset dependency.
- Fresh Greptile review on exact head
`54c7dd048c9016c1af9fc68213d5ae053ac16499` passed at 5/5 with zero
unresolved threads.
- Module boundaries, whitespace, and credential/internal-reference scans
passed. No live source writes, provider actions or migration apply were
performed.

## Risks

Missing, changed or unsafe local files remain blockers. This command
does not repair a previously published package whose objects later
disappear or become corrupt. It also refuses malformed preservation
receipts. It retains the original local files and prior evidence and
keeps removal blocked. Automatic repair requires a separate canonical
package-owner recovery operation; direct database edits are not an
accepted recovery path. Failed publication keeps the original files and
any unreferenced package for normal collection. The readiness check is
explicit startup or enrollment work, not a background scan. Its cost
grows with retained admitted asset obligations. Older readers must be
upgraded before the old volume is removed. This change preserves asset
bytes; it does not settle business recovery or qualify unrelated volume
dependencies.

## Model Used

OpenAI GPT-6 (Codex). The runtime did not expose a more specific backend
model ID or context-window size. Used reasoning, code editing, and tool
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-06 18:23:53 -06:00
DottaandClaude Opus 5.5 6c7cc2c5d6 fix(native): do not spend a recovery attempt resuming a cleanly drained run (#19625)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs have an execution recovery budget of three provider
attempts, so a run that keeps failing stops retrying
> - A graceful shutdown (deploy or SIGTERM) drains native runs: it
suspends them for reattachment by the next server
> - When the runner did not survive, the next boot resumed the turn and
charged one attempt
> - Restarts that the run did not cause used up its budget, so recovery
stopped and the task needed a manual Retry
> - This pull request does not charge an attempt for resuming a run that
a graceful drain suspended
> - The benefit is that deploys and graceful restarts no longer exhaust
a healthy run's recovery budget

## Linked Issues or Issue Description

**What happened?**
In restart-chaos testing, native runs reached
`execution_recovery_budget_exhausted` (3/3) only because of server
restarts. In one wave, two restarts crossed each of two runs. In a
second schedule (K1, then K2 when ready, then K2 again), each drain
correctly suspended the local ACPX Claude run, yet the fourth boot
blocked it with `native_restart_recovery_blocked`. Every resume after a
drain was classified `resume_dead_runner` and charged an attempt
(`nextNativeProviderAttempt` in `native-restart-recovery.ts`, applied by
the executor's execution claim).

**Expected behavior**
A restart that the run did not cause should not consume its recovery
budget. A graceful drain records `restart_suspended` and
`awaiting_runner_reattach`. The resume that follows should keep the
attempt count.

**Paperclip version or commit**
`master` at `4ef3fa4271`.

**Steps to reproduce**
Start a native local run. Send the server SIGTERM three times while the
run is active, with a full restart each time. The third resume is
blocked with `execution_recovery_budget_exhausted`.

## What Changed

- `suspendedByCleanDrain()` in `native-restart-recovery.ts` recognizes a
run whose most recent recovery claim is older than the drain's
`restart_suspended` entry. Ownership waits and deferred blocks between
the drain and the next claim do not erase it. The next claim consumes
it.
- A scan that waits for the old controller to exit (`awaiting_evidence`)
records one history entry per reason, so repeated scans cannot push the
drain record out of the bounded history.
- Restart claims carry `chargesProviderAttempt`. It is `false` for a
claim that follows a clean drain.
- `nextNativeProviderAttempt()` takes that flag. The executor passes it
when it takes the execution lease, so a drained resume keeps the current
attempt.
- The first execution is always counted: a run drained at attempt 0 (no
execution claimed it yet) still charges its first provider start.
- A run drained during its third attempt may continue that attempt. The
local resume and remote bootstrap gates skip the `>= 3` exhausted check
only for a non-charging resume, and the executor still refuses a fourth
attempt.
- A crash restart (no drain record) still charges one attempt.
Reattachment to a live runner still charges none.
- `doc/execution-semantics.md` states the rule next to the three-attempt
budget.

## Verification

- New test in
`server/src/__tests__/heartbeat-native-restart-suspension.test.ts`
(embedded Postgres, real heartbeat drain and restart recovery):
- Red before the fix: the claim after the drain had no
`chargesProviderAttempt: false`.
- Green after the fix, for drained attempts 0, 1 and 3: the crash claim
charges (`true`). After the drain, a controlled backend lets the
executor take its execution lease and records the saved
`native_run_finalizations.attempt` at provider dispatch: 1, 1 and 3.
Attempt 3 is claimed instead of blocked.
- Red at the executor: when the executor ignores the flag, the drained
attempt-1 resume saves 2.
- Ownership-wait case: the drained controller (the test process) is
still alive, so two scans wait. The history keeps `restart_suspended`
followed by one `awaiting_evidence` entry. After the controller is gone,
the claim does not charge, and attempt 3 continues.
- Classifier cases (local resume and remote bootstrap): attempt 3 after
a crash is still `execution_recovery_budget_exhausted`. Both new cases
were red on the previous revision.
- `heartbeat-native-restart-suspension.test.ts` 12/12,
`native-runner-restart-recovery.integration.test.ts` and
`native-runner-ownership.test.ts` 94 passed.
- `tsc --noEmit` for `server` passes.

## Risks

- A run that itself makes the server shut down gracefully, again and
again, would no longer be stopped by this budget. Graceful shutdowns are
operator or deploy actions, so this is unlikely. A crash loop (SIGKILL,
OOM) still charges an attempt on every restart, and a resumed turn that
fails still goes through the existing failure and replacement budget.
- This changes only the attempt count. Ownership checks, checkpoint
verification and the exhausted gate are unchanged.

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

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with
tool use (shell, file edits, test execution). Extended reasoning
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 tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 18:12:12 -06:00
DottaandPaperclip 56c2b28c04 Settle saved monitor results after a newer user request (#19644)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native results must finish their durable finalization before later
work can reuse a saved workspace.
> - An accepted result can wait on a monitor while its workspace files
are recovered.
> - A newer user request can replace that old wait before finalization
resumes.
> - The old finalizer then reports lost monitor authority and blocks the
saved-workspace lineage.
> - This pull request proves the newer user request and settles only the
old result bookkeeping.
> - The benefit is released recovery work without replaying an old turn
or changing the current request.

## Linked Issues or Issue Description

Refs #17641 and #16975.

**What happened?**

An older successful native result has saved files and a server-issued
monitor receipt. Its task is now blocked after a newer user-comment
continuation failed during preparation. The old coordinator stays in
retryable_failure with monitor_wait_authority_lost. The retained
allocation ownership check correctly refuses the unresolved predecessor,
so later file recovery also stays held.

**Expected behavior**

The normal finalizer can settle the old accepted result when exact newer
user-request evidence proves the old wait is superseded. It must
preserve the current task, user input, ownership, schedule, accepted
result, and attempt history. It must not start provider work or publish
the old wait summary.

**Steps to reproduce**

Create an accepted successful monitor result and mark workspace
finalization successful. Leave the task blocked. Record a newer
canonical user comment and its ordinary user wake, either queued or
consumed by a same-task continuation that retains the exact comment
revision and source-run lineage. Call the real native finalizer. Before
the repair, both cases remain retryable_failure instead of settling.

**Paperclip version or commit**

e482f0f632, before this repair.

## What Changed

- Add a company-scoped passive supersession proof in the native monitor
owner. It applies only when the old wait is not currently eligible. The
proof binds the old accepted result and monitor receipt to a newer
canonical unedited user comment and matching user wake. Admitted
successors must retain the exact task, comment revision, and source
lineage.
- Recheck the proof under the existing task transaction, with
nonblocking shared locks on the comment and wake; contention uses the
existing status-race retry path. Commit a preserve decision with no
effects. Keep normal monitor authority, Stop, workspace finalization,
and retained-allocation guards unchanged.
- Add real PostgreSQL coverage for queued and consumed input, current
successor ownership, newer schedules, immutable accepted results,
attempt history, foreign/missing/cancelled evidence, and changes before
commit. Real scheduler ticks create no old monitor wake.
- Include the present superseding proof in the existing assessment
identity. The old failed assessment and decision stay immutable, while
the new decision gets its own assessment. Absent proof keeps the
existing fingerprint unchanged.
- Document passive finalization after a newer user request.

## Verification

- Behavioral RED before production edits: two real PostgreSQL finalizer
cases returned monitor_wait_authority_lost/retryable_failure instead of
settling after a newer queued or consumed user request. Initial
interrupted imports and rejected database setup were not counted as
behavioral failures.
- Additional real-boundary RED: the normal finalization reconciler
reconstructed both persisted errors but failed on
status_decisions_company_assessment_uq because the changed proof reused
an older assessment identity. The owned PostgreSQL log confirmed the
constraint. Including only the present proof in the existing fingerprint
makes both reconciler cases pass while old assessments and decisions
remain immutable.
- Review RED: real comment-row and wake-row contention produced
side_effect_planning_failed. A test-local 100ms lock timeout bounds this
diagnostic. Nonblocking proof locks and `55P03`→NativeStatusRaceError
safely defer both cases, preserve current state, and commit after the
original lock owner releases. Both regressions pass without weakening
the production lock or authority checks.
- Final local GREEN at `0c40331d8d7f587c9a532fbc51d358bfacd1bd46`: five
files, 111 passing tests (126.17s), including all 54 saved-monitor
cases, actual persisted-error reconciliation with an absent retry
deadline and older boot identity, the status
arbiter/task-monitor/terminal-status suites, and six model fixtures.
Current requests, schedules, holds, accepted results and prior
assessments/decisions remain unchanged; normal attempt increments and
idempotent repeat are asserted. Real scheduler ticks create no old
monitor wake.
- All seven server typecheck layers, module-boundary and whitespace
checks pass at the final inputs. Required preflight passes in 15.348s.
No new code comments were added. Local environment: macOS arm64, Node
26.4.0, pinned pnpm 9.15.4, Vitest 5.0.3, owned disposable PostgreSQL 18
with bounded 1GiB tmpfs; no shared Docker data was pruned.
- [Final-head
CI](https://github.com/paperclipai/paperclip/actions/runs/42311889918):
59 successful checks,three intentional skips,one neutral optional
review,zero failed or pending checks. Required E2E and verify aggregates
pass. Greptile reviewed this exact head at 5/5 with no unresolved
findings.
- The [original support
job](https://github.com/paperclipai/paperclip/actions/runs/42311889918/job/128217026059)
passed 6849 assertions and hit one 30000ms timeout in an unchanged
private-evidence fixture. The exact case passed locally; all 41 cases
passed together (42.79s). The [identical-input sibling CI
control](https://github.com/paperclipai/paperclip/actions/runs/42311878548/job/128216598362)
passed all 6850 assertions and the 41-case file in 5.417s. Exactly one
targeted replay was authorized and dispatched after workflow completion
and ordinary quota reset, preserving the other 57 passing checks.
[Replay
job128227417618](https://github.com/paperclipai/paperclip/actions/runs/42311889918/job/128227417618)
passed 323 files/6850 assertions with four existing skips; the 41-case
file passed in 5.665s. No fixture, source, assertion, timeout,
credentials or full workflow was changed/replayed. The original timeout
remains recorded; its precise host-level cause was not determined.
- Inherited baseline failures were handled through normal integration of
existing #19610 (model expectations), #19627 (deterministic Slack
deadline fixture), and #19646 (current trusted Runner lane flags). All
four exact lane-command dry runs and the lane-inventory regression pass.
No force push or new production patch was used for those failures.
- Hosted recovery remains pending deployment and acceptance through the
normal pipeline; these checks do not claim the live task has already
recovered.

## Risks

- The new proof must remain passive. It cannot grant execution or
restore an obsolete task state. Missing or changing proof leaves the old
result held.
- The query reads only the same company's task and agent, with requests
newer than the old completion time and one selected result. It runs
during active finalization and adds no timer or idle polling.
- Edited comments, cancelled wakes, foreign lineage, source Stop, and
workspace failure are refused. Existing workspace snapshot, stop, and
handoff checks still own physical file recovery.
- Hosted acceptance remains pending deployment through the normal
pipeline.

## Model Used

- OpenAI Codex (GPT-6 family), with code execution and repository tools.
The runtime does not expose the exact serving model identifier or
context window size.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 18:08:17 -06:00
DottaandClaude Opus 5.5 e89be6ee83 fix(native): re-bootstrap a runner that a graceful drain suspended during bootstrap (#19665)
## 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.
> - After a restart, recovery gives a run with no provider evidence the
`bootstrap_incomplete` disposition. The executor quarantines the old
root and starts the bootstrap again, but only when the root proves that
no provider work started.
> - #19524 made that proof accept a root killed after `run.prepare` and
before `session.open` (a hard kill).
> - A graceful restart (SIGTERM) leaves different shapes. The drain
sends `runner.drain` and `runner.suspend`, and the kill can land just
after `session.open` failed or just after it started a fresh session.
The proof refused both shapes. The run failed with
`runner_state_identity_mismatch: bootstrap_not_proven_incomplete`,
although no turn was ever sent.
> - This pull request extends the proof to both drain shapes and keeps
provider work fail-closed.
> - The benefit is that a graceful restart during native bootstrap
continues automatically.

## Linked Issues or Issue Description

Refs #19524

**What happened?**
Restart chaos waves (ACPX Claude, K2 graceful restart about 10 s after
the run started) failed with `adapter_failed` and
`runner_state_identity_mismatch: bootstrap_not_proven_incomplete`.
Startup recovery chose `bootstrap_incomplete`, and the executor proof
refused the state root. Two real roots were kept:

1. `session.open` failed (the ACPX sidecar timed out), then
`runner.drain` completed and `runner.suspend` completed. There was 1
connection, no committed or acknowledged event, an empty runner outbox,
and `nextSourceSeq: 1`.
2. `session.open` completed with a freshly started session (`status:
"started"`). The committed events were only `session.started`,
`session.capabilities.updated` and `session.goal.snapshot`, and the
runner outbox held one `harness.diagnostic` (the sidecar process). No
turn command existed, and the sidecar process had exited. The database
had not yet recorded the session events, so the classifier saw no
provider evidence.

**Steps to reproduce**
1. Start an ACPX Claude task on a local runner.
2. Send SIGTERM to the control plane about 10 s after the run starts,
before the first turn.
3. Let the control plane restart.

**Expected behavior**
Recovery starts the bootstrap again, and the task continues without a
user action.

**Actual behavior**
The run fails and the task stops.

## What Changed

- `native-bootstrap-proof.ts`, pre-session proof: a `session.open` that
is pending or `failed` is now accepted, as are the runner lifecycle
commands `runner.drain`, `runner.suspend` and `runner.shutdown` in any
state. The controller journal and the runner journal must both agree.
Any committed or acknowledged event, any turn command, a completed
`session.open`, and any queued runner event still fail closed.
- New `runnerdStateIsOpenedSessionBeforeTurn`. It accepts a completed
`session.open` only when all of these are true:
  - the session was freshly started (`status: "started"`, not resumed);
- the commands are only `run.prepare`, `session.open` and runner
maintenance commands;
- every committed event and every event in the runner outbox is a
session setup or runner lifecycle event;
  - `ackedSourceSeq` equals the count of committed events, with no gap;
  - the runner journal is present;
  - every recorded provider process ID has exited on this host.
- `native-session-executor.ts`: the executor applies the opened-session
proof only for a local execution target. Remote runners stay fail-closed
for that shape, because their process IDs belong to another host. The
remote checkpoint classifier still reads only commands 1 and 2.

## Verification

- Red before the fix: "allows a fresh bootstrap after a drain suspended
a runner whose session.open failed" and "allows a fresh bootstrap after
a drain that landed between session.open and the first turn" both failed
with `expected false to be true`. Green after the fix: the `native
incomplete-bootstrap evidence` block passes 32/32.
- Fail-closed counter-tests:
  - completed `session.open`;
  - committed event, acknowledged event, or queued runner event;
  - turn command, `turn.stop`, or a turn processed by the runner;
  - `turn.started` or `provider.event`;
  - resumed session;
  - compacted events;
  - no recorded provider process;
  - missing runner journal;
  - live provider process;
  - remote runner.
- I ran the new proof read-only against the three real kept roots: the
K1 root from #19524, the K2 `session.open` failed root and the K2
opened-session root. All three return `true` for a local runner. The
opened-session root returns `false` when the processes are on another
host.
- `native-session-executor.test.ts` and
`native-restart-recovery.test.ts` filtered to bootstrap, restart, remote
and drain: 333 passed.
`native-runner-restart-recovery.integration.test.ts` and
`native-bootstrap-retry.integration.test.ts`: 106 passed. `tsc --noEmit
-p server/tsconfig.json` passes.
- Live restart chaos replay: see the PR comment.

## Risks

- After a `session.open` failure or timeout, the provider sidecar might
have created an empty session. No turn command was sent, so no provider
work can repeat.
- An opened session before any turn is discarded, and a new one is
started. The proof requires a fresh `started` session, so no earlier
conversation is lost. Resumed sessions stay fail-closed.
- The opened-session proof is local only. It requires every recorded
provider process to be gone, so a live provider process blocks it.
- Out of scope: the Retry offer when the proof fails legitimately
(RC-05/06).

> 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 17:55:47 -06:00
DottaandPaperclip 9197abf673 fix(native): preserve controller state when remote archiving fails (#19663)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native continuity replacement must preserve the state needed to
recover saved work.
> - The current replacement moves local controller files before it knows
whether the remote archive succeeded.
> - A remote failure then leaves the local controller without its
recovery files.
> - This pull request confirms the remote archive before it moves those
files.
> - A user turn that never started can use the existing bounded Retry
path after verified preservation and cleanup.
> - The task retains its saved work and can continue once under current
user authority.

## Linked Issues or Issue Description

Refs #19638, #19547, #19399 and #19659.

The prerequisite #19638 merged as
`0819f4ea78dd37fba87341b314be9dac078c1410`. This PR now targets master.
A normal dependency integration at `febed950f4` keeps only the six-file
archive repair in the PR diff. Fresh master-based CI and review are
required before merge.

**What happened?**

A native continuity replacement moved its local controller journal and
provider checkpoint. The following remote archive failed. A later
recovery attempt could no longer read the local recovery files. The
first-attempt failure also had no normal Retry receipt for the saved
user turn.

**Expected behavior**

A failed or timed-out remote archive retains the local recovery files
and starts no replacement provider. A user turn that has not started can
request one normal Retry. The existing user, provider-turn, Stop,
preservation, cleanup, budget and approval gates remain in force.

**Steps to reproduce**

1. Keep a local controller journal and provider checkpoint for a
retained remote conversation.
2. Request continuity replacement and fail or time out the remote
archive command.
3. Check the local controller files before any later recovery.
4. Settle the native failure under a current user wake and recover the
saved workspace.

## What Changed

- Confirm remote continuity archiving before moving the local controller
state.
- Add the exact remote archive failure to the shared bounded user Retry
admission from #19638.
- Verify failed, timed-out and successful archive ordering with real
local files.
- Verify ordinary failure settlement and saved-file recovery produce one
fresh successor. Keep Stop, system wake and unsettled-provider holds.
- Document the file preservation and Retry contract.

## Verification

- RED: failed and timed-out remote archives removed the local controller
files. Reading those files failed with ENOENT. The saved-user Retry
request returned false instead of true.
- Initial archive unit verification: eight cases passed, including the
three archive outcomes and five existing Cloud archive safety cases.
- Initial stacked revision: `9104ee399b`, based on #19638 at
`d22d8f9d5ee3dc36550a94d55c1d8376f3b9aab2`.
- Final master-based revision: `febed950f4`. All 13 affected cases
passed across three Vitest files after dependency integration. They
include the seven-mode real PostgreSQL completion case, saved-file
successor and negative-hold cases, and real-file archive outcomes. All
seven server typecheck layers passed again. Module boundary, whitespace
and added-code-comment checks passed. All required CI passed on this
exact head. The final snapshot has 59 successful checks, three intended
skips, one neutral result, and no pending or failed checks. Current-head
Greptile and the independent review are both 5/5, with no open findings.
- Final local revision: `9104ee399b`, macOS ARM64, Node 26.4.0, pnpm
9.15.4 and embedded PostgreSQL. The three real-file archive outcome
cases passed. The real PostgreSQL completion test passed all seven
internal scenarios, including archive failure, prior provider activity,
system wake, Stop before and after terminal settlement, and stale
runtime promotion. Own Runner/shared/SDK builds and all seven server
typecheck layers passed. Nine saved-work continuation and negative-hold
cases passed. They verify exported file contents, one user-attributed
fresh successor, idempotent hooks, no second automatic chain, and
first-use manual Retry for held cases.
- `node scripts/check-module-boundaries.mjs`, whitespace checks and the
added-code-comment audit passed.
- Focused commands from `server/`: Vitest with `-t 'retains the local
controller checkpoint until remote continuity archiving succeeds'`; `-t
'replays saved user intent through normal Retry'`; and `-t 'continues a
user turn once|does not continue automatically'`. PostgreSQL cases used
a fresh local cluster and a 120-second setup hook budget. Application
timeouts were unchanged.
- The initial stacked head passed all test, build, typecheck and E2E
jobs. Greptile and the independent review both scored it 5/5 with no
findings. These results do not replace the fresh master-based
qualification now running.

## Risks

- A remote command can fail after it changed remote state. This change
retains local evidence in that case. It does not claim a distributed
atomic transaction.
- The exact archive error can request one normal Retry only under the
existing saved-user admission. The normal path must prove stop, saved
files and cleanup before a successor starts.
- The protected launch-argument rejection remains enforced. Its exact
hosted argument difference was not available through ordinary access.
#19659 separately addresses a saved-thread feature-policy argument
difference. This PR preserves local archive evidence and does not claim
direct hosted verification of that primary cause.

## Model Used

OpenAI GPT-6 family through Codex, with reasoning, tool use and code
execution. The model is inherited; no override was used. The exact
serving model ID and context-window size are not exposed by this
runtime.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 17:52:12 -06:00
DottaandPaperclip 0819f4ea78 fix(native): verify controller ownership before retrying saved turns (#19638)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native task turns can keep a provider conversation from an earlier
run.
> - A copied conversation checkpoint names the new run before its
controller has attached.
> - A failed attachment can make recovery treat that copied identity as
current execution authority.
> - The current run needs its own durable controller checkpoint, or a
valid staged attachment, before exact resume.
> - This pull request checks that ownership and uses the normal Retry
path to continue a saved user turn after verified cleanup.
> - The task can continue once with its saved work and history.

## Linked Issues or Issue Description

Refs #19399 and #18860.

**What happened?**

A new user comment resumed a completed native conversation. The retained
runner process was gone before the new controller attached. The new run
still had a copied provider session ID. Recovery selected exact resume
for that new run and then failed its unchanged owner and lease checks.
Ordinary first-attempt failure settlement also omitted the saved-user
Retry handoff that restart recovery already uses.

**Expected behavior**

Exact resume requires current controller evidence. A saved user turn can
request one normal Retry after this infrastructure failure. The normal
path must verify provider stop, saved files, current user authority and
all execution gates before it starts a fresh session.

**Steps to reproduce**

1. Finish a native warm run and retain its provider checkpoint.
2. Admit a user comment as a new run and copy the provider checkpoint
through the real resume producer.
3. Stop the retained runner before attachment. The real remote process
check reports that the process is not running.
4. Observe that the copied provider ID selects exact current-run resume.
Observe that ordinary failure settlement saves no Retry request.

## What Changed

- Check a bounded encrypted controller header for fenced remote Runner
recovery. Keep local Runner recovery on its existing disk-backed path.
Match the run, conversation and runner identity. Preserve the existing
staged-attachment path.
- Share the existing saved-user Retry admission between restart recovery
and ordinary terminal native settlement. Keep its exact infrastructure
errors, user wake, comment, provider-turn and retry-chain checks.
- Add real PostgreSQL and stopped-process regressions. Verify malformed
and foreign checkpoint holds, current-owner resume, user authority and
Stop. Verify saved-file recovery creates one fresh-session successor.
- Document the ownership and Retry contract.

## Verification

- RED: the real checkpoint rebind producer and stopped-process check
selected exact current-run resume without a current controller
checkpoint.
- RED: ordinary settlement created zero Retry receipts for the real
native failure code; expected one.
- Review RED: a real current local controller file existed with no
database checkpoint rows. The initial gate returned ambiguous state. The
fix takes a required trusted execution-target kind and applies the
database requirement only to the remote persistence path.
- Initial local revision: `3662055880ccf8e09c3c50a340897e16c78627e7`,
macOS ARM64, Node 26.4.0, pnpm 9.15.4, embedded PostgreSQL.
- Initial server Vitest: 42 selected cases passed across controller
recovery integration, remote restart adoption, ordinary run completion
and retained workspace recovery. The cases cover the stopped-process
producer, current/foreign/malformed ownership, automatic user
continuation, Stop, unsettled turns, system wakes and a manual Retry on
first use.
- `pnpm --filter @paperclipai/server typecheck`: passed with this
worktree's Runner, shared and SDK builds and all seven server layers.
- `node scripts/check-module-boundaries.mjs`, whitespace checks and the
added-code-comment audit: passed.
- Review correction: `8c795e2e0e44b5209da3e228593c5cd35e948cae`. The
real local/remote controller case passed against a fresh PostgreSQL
cluster. It verifies local disk recovery without database rows, remote
current-owner resume, and remote missing/foreign/malformed holds. Fresh
`node scripts/typecheck-layers.mjs` passed all seven server layers. The
existing offline chat reader now requests metadata only and retains its
full independent native-state safety check.
- A broader local recheck hit PostgreSQL setup/cleanup hook timeouts.
These are retained as failed setup attempts; the fresh-cluster
regression above passed. Required CI supplies broad final-head coverage.
- Settlement-order correction:
`dafb721c3bbb7ae86fbc3c9c6c953ace29ca088b`. The real PostgreSQL
regression first proved both failures: Stop after an already-failed row
created one Retry instead of zero; a DB-native promotion with an old
legacy caller snapshot created zero instead of one. All six settlement
scenarios now pass. The hook requires the local failed outcome and the
canonical settled native row. Fresh seven-layer server typecheck passed
again.
- Required shared fixture base integrated at
`d22d8f9d5ee3dc36550a94d55c1d8376f3b9aab2`. The six runner-skills
fixture cases and all seven server typecheck layers passed. Required CI
passed for this head:
[workflow](https://github.com/paperclipai/paperclip/actions/runs/42309419592),
including all server layers, tests, build, Runner verification and E2E.
The final snapshot has 57 successful checks, three intended skips and
one neutral result. Current-head Greptile is 5/5; both concrete review
threads are resolved. The independent re-review ended without a final
response; its prior Stop finding has a real RED/GREEN repair.

The focused command was run from `server/`:

```sh
../node_modules/.bin/vitest run --config vitest.config.ts src/services/heartbeat/run-completion.test.ts src/services/native-runtime/native-controller-recovery.integration.test.ts src/services/native-runtime/native-session-executor.test.ts src/services/workspace-recovery.test.ts -t 'replays saved user intent|does not assign current-run resume authority|remote controller restart adoption|continues a user turn once|does not continue automatically'
```

## Risks

- A terminal failure of a fenced Runner adds one exact-run header read.
The read selects one encrypted header and limits inspection to 1 MiB.
Invalid or missing ownership evidence stays held.
- Saved user input can request one automatic Retry. It uses normal
preservation, current authorization, budget, approval, Stop and cleanup
gates. Unsettled provider turns and system wakes do not start an
automatic successor.
- This PR changes no provider process, credential, model or lease
authority checks.

## Model Used

OpenAI GPT-6 family through Codex, with reasoning, tool use and code
execution. The model is inherited; no override was used. The exact
serving model ID and context-window size are not exposed by this
runtime.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 17:31:23 -06:00
DottaandPaperclip bee0f0e2fa fix(server): recognize recoverable result finalization during upgrades (#19651)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native execution saves an accepted result before it finishes
workspace export and status projection.
> - The upgrade report currently recognizes only a live execution in the
observed phase.
> - A durable accepted result can therefore block an upgrade after its
execution lease is released.
> - Existing finalization owners already resume that saved work with
task binding, physical worker stop proof, and an atomic owner claim.
> - This pull request lets the report inspect those existing contracts
without taking authority.
> - Qualified remote work can continue through a normal server
replacement.

## Linked Issues or Issue Description

Refs #18037 and #19596.

**What happened?**

An accepted remote native result remains in result acceptance, workspace
finalization, retry, or committed projection. The upgrade report marks
it blocking because it is no longer a result-less observed execution
with a live execution lease.

**Expected behavior**

Report a saved result as recoverable only when the existing replacement
owners can resume it. Preserve all task, result, contract, remote
workspace, provider lease, physical owner, decision, and workspace
barrier checks. Keep unsupported or unknown work blocking.

**Steps to reproduce**

Run the new PostgreSQL post-result readiness fixture. It persists a real
encrypted Runner header and an accepted terminal result with a bound
remote workspace. The unchanged report returns one blocker.

**Paperclip version or commit**

Regression base: ee6c101854. Related shutdown recovery is in #19596.

## What Changed

- Delegate saved-result eligibility to the native finalization owner.
- Require the accepted terminal result and contract, current task
binding, remote authority, retained provider lease or successful
workspace receipt, and a supported physical worker identity.
- Require an applied decision and assessment for committed projection.
Keep finalizer leases beyond the supported outage budget blocking.
- Reuse the existing result recovery binding with selected metadata. Do
not load complete run or result payloads for the new observation.
- Keep real signed stop verification, owner compare-and-swap, retry
deadlines, and actual finalization with their existing owners.
- Share a two-second scheduling budget across saved-result inspections,
leaving unchecked candidates blocking after the budget expires.
- Add PostgreSQL, encrypted-header, signed stop-proof, competing-owner,
projection, and independent-work cases. Clarify the operator contract.

## Verification

- The valid accepted-result fixture reproduced the original blocking
report, then passed with this change.
- The initial eight post-result cases passed against PostgreSQL. They
include a real Ed25519 stop receipt, the existing verifier, a competing
PostgreSQL ownership claim, one copyback, and receipt reuse.
- Seventeen focused existing PostgreSQL shutdown and workspace ownership
cases passed.
- A broader five-file run passed 91 cases. Its one failure was an
invalid negative fixture: PostgreSQL correctly refused a contract change
protected by the accepted-result foreign key. The test now asserts that
refusal.
- A subsequent standalone run timed out in PostgreSQL bootstrap before
tests ran. The final run used the existing shared PostgreSQL command and
isolated per-suite template databases: all 92 tests passed across five
files (159.95 seconds). It covers both readiness suites, platform stop
proof, workspace finalization, and graceful shutdown. That scope passed
on `ce4e3995b6`. A follow-up restricts provider leases to the active
status and adds released/unknown-status controls. The new fixture now
uses the existing PostgreSQL bootstrap timeout constant. The affected
eight-case rerun passed on `850859b44d`. The explicit configured-stack
guard also passed its six platform-proof cases. Broad local source
typecheck still has stale borrowed dependency declarations; fresh CI
supplies that coverage.
- Review's locked-query regression reproduced RED: three saved-result
candidates took 6.081 seconds. With the shared scheduling budget, both
readiness suites passed all 16 cases (49.36 seconds) on final code
committed as `70b0b7b8f7`. The lock case leaves all three candidates
blocking, releases its connection, and observes all three as recoverable
after the lock is released.
- Greptile reviewed final `70b0b7b8f7` at 5/5 with no open threads.
Current-head CI typecheck, build, tests, and native review are pending.
No hosted replacement or enrollment acceptance is claimed.

## Risks

A false positive could select an unsafe upgrade. Missing evidence and
read errors remain blocking. Readiness does not stop admission, detach
an agent, acquire a lease, request a stop receipt, or authorize
takeover. The replacement still verifies physical stop and rechecks
current task ownership before copyback. Saved results retaining a
previous controller boot remain conservatively blocking, including
finalizers resumed by a later boot. This PR does not change enrollment
policy or migrate data.

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

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

## Linked Issues or Issue Description

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

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 17:30:00 -06:00
DottaandPaperclip 349b8b3f7b Migrate settled controller checkpoints while unrelated work runs (#19631)
## Thinking Path

> - Paperclip manages work performed by AI agents.
> - Server replacement must preserve retained Runner sessions.
> - Older sessions can retain controller checkpoints on the server
volume.
> - The existing importer requires all execution writers to stop,
including unrelated runs.
> - This change adds a separate online path for successful, settled
sources.
> - It locks the source task and finalizer while it reads and publishes
that source.
> - Unrelated work continues. The offline importer keeps its existing
authority.

## Linked Issues or Issue Description

**What happened?**

A retained controller checkpoint cannot be converted by the existing
maintenance importer while another task runs. Requiring a busy server to
become globally idle prevents online migration.

**Expected behavior**

An operator can convert a successful retained source after its finalizer
releases ownership, while unrelated tasks continue. An expired owner
remains an owner. Related active work and uncertain evidence remain
blocked.

**Steps to reproduce**

1. Retain a valid legacy controller for a successful run with an
accepted result and a released committed finalizer.
2. Start a different task.
3. Run controller preparation. The old importer refuses because
execution writers are active.

Related preservation contracts: #19038 and #19024. This change does not
replace workspace migration or authorize volume removal.

## What Changed

- Add `prestageNativeControllers` with task, finalizer, and source-run
row locks. Recheck related runs and exact checkpoint identity under
those locks.
- Allow legacy checkpoint material through normal settled-owner import
checks. Keep offline authority separate.
- Compare captured local controller and supervisor bytes before commit.
A changed source rolls back publication.
- Add `--controllers-only` to the existing finite native preparation
entry. This avoids re-reading provider archives during controller
conversion.
- Add PostgreSQL cases for unrelated work, expired owners, related
active work, source-lock contention, and source changes.
- Document the finite command and its limits.

## Verification

- Real PostgreSQL affected suites: 56 passed across controller migration
and recovery. A first per-server run was interrupted because it was
slow; the completed run used the repository's shared PostgreSQL test
wrapper.
- Initial online cases: 3 passed, covering unrelated work, an expired
owner, and related active work.
- Source lock and changed-file cases: 2 passed. Publication rolled back
on a source change.
- Final source-evidence regression: 2 passed. A first unproven source
deferred and a later valid source imported; changed-file rollback
remained intact.
- Server TypeScript check passed with the SDK and Runner declarations
built.
- Server package build passed, including the Runner vendor checks and
compiled command.
- Runtime bundling suite: 10 passed.
- Full repository CI and exact-head automated review remain required
before merge. This draft is not staging acceptance.


- PASS: 7 CLI cases after adding the controller-only dependency fixture
and explicit-mode coverage. The first CI run exposed missing mock/export
coverage; the fixture repair passed after rebase.
- The unrelated model-default failures were fixed on master; this branch
now includes that fix.

## Risks

- A source task can wait briefly on its own migration lock. A one-second
lock acquisition budget defers a contended source. No global admission
hold is acquired.
- Only successful sources with a released committed finalizer are
eligible. Failed or unsettled sources stay deferred and require their
preservation owners.
- The operator must run the command on the host that owns the original
files. The command does not certify enrollment readiness or detach any
volume.
- The existing offline path remains unchanged and still requires stopped
writers.

## Model Used

OpenAI Codex, GPT-6 family, with reasoning, tool use, and code
execution. The exact serving model ID and context window are not exposed
in this session.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] 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 17:19:23 -06:00
DottaandClaude Opus 5.5 15ee6a25a6 fix(native): re-bootstrap a claimed Daytona run killed before runner launch (#19616)
## 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. Restart recovery decides what to do with each live run after
the control plane restarts.
> - #19583 lets recovery start the same-run bootstrap again when the
Postgres controller checkpoint proves that the runner received only
bootstrap commands.
> - A live replay found an earlier window. The control plane died after
the executor claimed the run and staged the sandbox workspace, but
before it asked for a runner process. No runner checkpoint existed, so
#19583 did not apply.
> - Recovery classified that run as `ambiguous_remote_recovery_evidence`
and blocked the task. The task also showed "Workspace repair required",
which did not describe the real cause.
> - This pull request proves the "nothing reached a runner" case from
durable evidence and starts the bootstrap again. It also names the
restart in the settled hold note.
> - The benefit is that a restart at any point of Daytona startup before
runner launch continues automatically.

## Linked Issues or Issue Description

**What happened?**

A chaos replay killed the control plane about 2 s after a claimed ACPX
Claude run started on Daytona. The kill came after `stage.workspace` and
environment acquisition, before `native.process_start_requested`. The
restarted server took over because the previous controller process was
dead. It then returned `ambiguous_remote_recovery_evidence` with
disposition `blocked`. The task rested blocked as "Workspace repair
required".

Evidence in the saved database at that point: coordinator `attempt: 1`,
`controllerGeneration: 1`, previous controller identity recorded and
dead. Native workspace sync was `prepared`. There were no runner
checkpoint rows, no session checkpoint, no process PID or start time, no
provider session, and no execution-start or runner-sourced event. The
local session root was an empty directory.

**Expected behavior**

When durable evidence proves that no runner process was requested and no
controller checkpoint exists, recovery continues the run with a fresh
bootstrap. Any evidence that a runner may have started keeps the hold.

**Steps to reproduce**

1. Start a native run on a Daytona environment.
2. Kill the control plane after workspace staging and before the
executor requests the runner process.
3. Restart the control plane.
4. See that restart recovery blocks the task with
`ambiguous_remote_recovery_evidence`.

Root cause:
`server/src/services/native-runtime/native-restart-recovery.ts`,
function `classifyNativeRunnerRecoveryEvidence`, remote branch. The only
no-checkpoint remote rule required an unclaimed coordinator (`attempt:
0`, no controller identity). A claimed run with no launch evidence fell
through to "ambiguous".

Refs #19583, #16349

## What Changed

- `native-restart-recovery.ts`: add the `remote_runner_never_launched`
proof, which leads to `bootstrap_incomplete`. It applies only to a
classic sandbox run with a prepared workspace and the preparation
fencing version. The run must have a real executor claim (controller
identity and lease owner) whose controller is gone (takeover allowed).
It needs no durable checkpoint, no session checkpoint, no process
fields, no provider session, and no provider evidence. The run must also
have no `native.process_start_requested`, process identity,
`adapter.invoke`, `tool.execution.started`, or runner-sourced event. The
same three-attempt budget applies. The shared event query moved into
`hasNativeExecutionStartEvidence`.
- `native-session-executor.ts`:
`restoreRemoteBootstrapCheckpointForRecovery` removes an empty session
root that preparation created. Without that step, the fresh bootstrap
read it as `durable_identity_unreadable`.
- `execution-recovery-resolution.ts`: the new
`executionRecoverySettlementNote` names a settled
`native_restart_recovery_blocked` hold as a stop after a server restart.
It no longer shows "Workspace repair required" because a later workspace
export was retried. Other causes keep their notes.
- `doc/run-log-events.md`: document the new reason.

## Verification

- Red, then green: `cd server && npx vitest run
src/services/native-runtime/native-runner-restart-recovery.integration.test.ts
-t "unlaunched|never requested|Postgres checkpoint"`. Before the fix,
the proven case (lease live with a dead owner, and lease expired) and
the budget case returned `blocked` /
`ambiguous_remote_recovery_evidence`. The empty-root executor case
failed.
- Fail-closed cases: these keep the hold:
`native.process_start_requested`, a process identity event,
`adapter.invoke`, a runner-sourced event, a recorded PID, a recorded
provider session, and a spent budget. A live claiming controller returns
`awaiting_evidence` / `controller_still_alive`. The existing
"contradictory evidence" cases (`claimed` with no controller identity,
`launch`, `process`, and others) stay blocked.
- Wording: `server/src/services/execution-recovery-resolution.test.ts`.
- Regression set (10 files, including `workspace-recovery`,
`native-safe-replacement`, `heartbeat/recovery`,
`native-session-executor`, and `native-restart-recovery`): 2035 passed,
14 skipped.
- Live Daytona replay of the failing chaos task, 3 runs, sequential,
ACPX Claude, double SIGKILL, current master plus this change. Image:
content-matched `paperclip-daytona-runner@sha256:5a29a797…` built from
c6eecac1e0, with runner inputs pinned to it, because the image-only
build sat in the Actions queue. Result: **3/3 PASS** (R1 pass; R2, R4,
R5, R6 and T pass). Each run's faults landed after the runner launched.
Recovery waited on the live remote controller lease, then continued
without a Retry. Run 3 continued through `reattach_remote_runner`. The
saved logs for runs 1 and 2 do not name their claim kind. The earlier
pre-launch window (the subject of this PR) is not hit reliably by replay
timing. The integration tests cover that window with the same evidence
shape as the saved failing database.

## Risks

- The proof depends on the executor writing
`native.process_start_requested` before a backend may spawn a runner
(`onSessionAdmission` in `executeNativeSession`). A future change that
spawns a runner earlier would weaken this proof. The fail-closed tests
pin the event as the gate.
- A fresh bootstrap acquires a new sandbox. The staged sandbox from the
dead attempt keeps its lease until run settlement releases all run
leases. It never ran a runner.
- The note change affects only settled holds with the cause
`native_restart_recovery_blocked`.

## 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 17:17:14 -06:00
DottaandClaude Opus 5.5 7941f6b051 fix(runner): show OpenRouter's invalid model ID as a model rejection (#19548)
## Thinking Path

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

## Linked Issues or Issue Description

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

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

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

## What Changed

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

## Verification

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

## Risks

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

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 17:08:04 -06:00
ee6c101854 Reject failover backups behind the accepted event cursor (#19613)
A verified failover backup can still trail events already accepted by the controller. Compare the backup produced cursor with the registered controller state, or the supported local journal before registration, and reject stale files before remote sync. Keep the permanent replay-conflict classification ahead of generic Runner errors.

Validation: 1,192 native session and execution-driver tests and 10 workflow tests pass locally, together with the maintained low-memory Runner test project typecheck and fresh server declarations. All applicable checks on cd8c9cd07c are green (56 success, three skipped); exact-head Apex review is 5/5 with no unresolved findings. One e2e shard was retried after GitHub reported lost self-hosted runner communication, and the same-head rerun passed. No acknowledgement was lowered, provider replay authorized, or recovery files discarded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Paperclip <Paperclip-Paperclip@users.noreply.github.com>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-11-06 13:55:43 -08:00
DottaandPaperclip 44c60c7221 Verify default skill sync through two real cold sleep cycles (#19191)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Automatic skill sources check upstream every five minutes.
> - The domain deadline contract can hand future work to a durable wake
alarm.
> - A saved deadline or a worker unit test does not prove that a stopped
tenant resumes the real producer.
> - This pull request checks the default cadence through the actual app
and a matching service checkout.
> - It checks two cold starts and three distinct persisted skill
revisions with real clocks.
> - The benefit is repeatable local evidence without changing production
timing or authority.

## Linked Issues or Issue Description

Refs: #19001, #17008, #17923.

This is a test and documentation follow-up to #19001. #19001 is now
merged; this PR targets master. Feature code remains in that merged
foundation. #17008 covers the earlier source worker change. #17923
covers separate skill-cache retention; this PR does not change that
behavior.

A scoped search of public PRs and issues found no duplicate
default-cadence cold-cycle acceptance test. No matching public issue was
found. The enhancement fields follow.

**Pre-submission checklist**

This improves checks for an existing feature. It adds no product
endpoint or execution authority. The public related-work search is
complete.

**What existing behavior does this improve?**

Verification of automatic skill-source updates across a durable idle
handoff and cold restart.

**Subsystem affected**

`server/` test fixtures and development documentation.

**Current behavior**

Domain tests cover claims, access and future deadlines. They do not
prove the full default-300-second path from ordinary source attachment
through signed tenant reports, durable alarms, stopped compute and a
changed persisted skill.

**Proposed behavior**

An opt-in launcher checks bootstrap first, then two full real-clock
cycles. Ordinary CI can run the transport controls without the host
fixture. The launcher-specific worker assertion runs only when prewire
is requested, and fails closed in that lane.

**Reason and benefit**

The test can detect an early refresh, a lost deadline, a stale proof, an
ACK after physical stop, a failed cold restart or a producer that does
not publish changed contents.

**Breaking changes**

None to product APIs, schema, cadence, authorization, claims or retry
policy. Existing maintenance-helper defaults stay unchanged.

**Additional context**

The opt-in host flow needs an authorized matching service checkout, its
normal compiled acceptance bridge, Node 22 and two owned PostgreSQL test
databases. The standalone controls do not need that service or
PostgreSQL. Roadmap review places this under existing Skills Manager and
Cloud deployment verification, not a new core feature.

## What Changed

- Add an opt-in default-cadence acceptance test and checked-in launcher.
- Reuse real server startup, managed tenant authority and the ordinary
project attachment route. Control only the remote Git snapshot boundary;
keep actual authorization, scanning, claims and publication.
- Use actual signed tenant routes and the matching service's gate, sleep
controller and PostgreSQL wake-alarm store. Stop and restart real app
children through a controlled local provider.
- Preload the Node transport guard before coordinator, worker and child
module work. Check worker-local and child PID, thread and source-hash
receipts. Keep caught unexpected transport attempts fatal.
- Require both persisted refresh times and actual upstream handler
timestamps to be at or after each original deadline.
- Join physical callbacks during bridge close, stop a late wake-created
child, and preserve the original cleanup budget. Add a real
IPC/HTTP-child counterfactual.
- Accept valid owned HTTP options paths without allowing Unix sockets or
unknown origins. Remove the unused App helper export.
- Add finite transport and launcher controls. Defer only the
launcher-specific parent assertion in vanilla CI. Keep all subprocess
controls default-runnable.
- Add optional child entry, preload and bounded pool settings to the
existing test helper. Preserve its defaults and the real observer test
body.
- Remove added explanatory comments under the contribution policy.
Preserve executable syntax, directives and literals.
- Document pinned setup, fresh owned databases, bootstrap and full
two-cycle commands in `doc/DEVELOPING.md`.

The diff against the intended base contains nine test/helper/launcher
files and this one document. It adds no production feature code or Cloud
source to the App repository.

## Verification

The latest two-file CI repair preserves both original failed jobs. A
newer master workflow introduced two required step inputs absent from
its extracted-step test environment; the faithful false/false inputs now
pass all five existing target-authorization cases, after an actual
missing-input counterfactual failed. A controlled socket case also
reproduces legal reuse of the released runner port; the fixture now
holds that reservation until the preview binds, releases it in finally,
and asserts distinct ports. All original retry/result/accounting
assertions and timeouts stay unchanged. The original Linux listener
owner remains unknown; the control proves this fixture collision
mechanism, not that original attribution. Fresh current-head full CI and
complete reviews are pending.


Current merge-integration candidate:
`f6f686103b7bfd0a4ab83085634c622af3896ecb`, a normal merge after #19001
landed, including actual master
`1c9767d323cf872a266962b9e6b4bd5cbcb57f1c`. All nine acceptance code
files are byte-identical to148a82e5; both parents' documentation
additions survive. Every other path equals actual master. The only
conflict was the old foundation's startup block; current master was
retained in full. Fresh exact-head CI and complete reviews are pending.
The local measurements below belong to their explicitly named revisions;
no fresh full614-second run is claimed.

Current candidate: `148a82e54c502759785407357001c6b50c1439dd`, normally
carrying the current base `8c912e1ba4fc66397a7341ade59373af8075068f`.
All nine acceptance code blobs remain identical to qualified repair
`e092746f4da181c8414c63be71474619a84b97e7`; documentation retains both
parents. The matching built service for the fresh bootstrap is
`389878364b42dd129835d263f91882c5774d5123`, using Node 22.23.4. The App
uses Node 24.22.2 and pnpm 9.15.4.

Use the exact controlled setup and commands in
[doc/DEVELOPING.md](doc/DEVELOPING.md#controlled-default-cadence-sleep-acceptance).
Fresh owned databases and the matching compiled service are
prerequisites; the launcher does not create a cluster or build
dependencies.

- **Current affected semantic check passed**, using actual native
TypeScript 7 and the current generated CI options/references (0.640
seconds). Normal declaration references passed in 11.726 seconds, with
source-matched SDK/shared/kernel/Runner prerequisites and the frozen
current lockfile.
- **Current guarded controls: 44 passed**, plus the same single explicit
real-PG observer deferral, in 4.790 seconds. The executing worker
checked its own global PID/thread/preload source hash and matching
receipt. Guard hash:
`7daf9ae7d2f26e8fcd389b1d560fac1d4e218a8304a2c58e49f646af108165bc`. No
unexpected attempts or PG sockets occurred. Vanilla controls on the
identical repair files passed 22 cases with one explicit launcher-parent
assertion deferral (2.044 seconds).
- **Real cleanup counterfactual:** unchanged old helper failed the
actual endpoint assertion in 1.568 seconds: its late HTTP child remained
reachable after close returned. The same fixed-helper case passed in
1.485 seconds: close waited, the child exited, the port was unreachable
and no IPC error occurred. The isolated driver records only the known
legacy closed-IPC error long enough to inspect cleanup; current helper
requires zero such errors. All recorded processes and ports were
independently checked empty.
- **Fresh current bootstrap passed once**, 16.159 seconds wall / 14.12
seconds Vitest / 12.237 seconds body. The ordinary enabled producer
persisted useful revision 1, cleared its lease and retained exactly
300,000 ms between success and original next deadline. A signed
current-generation report produced the matching
generation/operation-lock SQL ACK before physical child stop (ACK
06:54:02.020 UTC; stop signal 06:54:02.025 UTC). The alarm retained the
original D and D-120 eligibility. Both fixture databases ended with zero
sessions; the coordinator independently audited and normally removed
them. This bootstrap does not execute a due wake or the new v2/v3 timing
assertions.
- **Historical full two-cycle result remains 1/1 passed in 614.213
seconds wall**, at measured App
`74f7c012ca17ad0d7302d76403f5033affb1430b` and service
`2cb54f904031dcf69a32902cd853b43f74d6f301`. Two new processes published
distinct immutable v2/v3 at original D+115 ms and D+98 ms. Exact signed
SQL ACKs preceded both physical stops; real SQL D-120 claims caused both
cold starts. Features, claims and original business deadlines stayed
unchanged.
- Both historical 10.002-second quiet windows kept app SQL, upstream and
outgoing HTTP counts unchanged. Two awake PG sockets remained. Quiet
counts do not mean all connections closed.
- The stronger timing checks are additional assertions, not
retroactively executed results. Offline inspection of the original
receipts confirms persisted successes at D+115/+98 ms and earliest
upstream handler entries at D+44/+30 ms; all five requests in each
refresh group started after D. The exact new pre-sleep request counter
and dispatch-before-handler time were not independently recorded
historically.
- Source review found 19 critical automatic Git producer/runtime files
identical to the measured App. The current active cache helper change
does not run on automatic Git publication and adds no pool. The matching
service carries unchanged measured helper/sleep/controller/alarm seams
through its normal master merge. The current bootstrap and focused
controls qualify the changed cleanup/guard and parent integration. No
fresh current-head 614-second pass or whole-tree identity is claimed.
- Runtime is serialized: worker 1, per-pool limits at most 4, configured
App potential 10 + shared service pool 2 + one observer against server
max 40. Current bootstrap point samples observed at most 3 owned clients
plus the observer; those samples are not a continuous aggregate peak.
App/service guard peaks were 2 each, with zero remaining sockets at
cleanup.

Failures remain accounted for. The first historical bootstrap cleanup
masked an unknown primary error. Later attempts exposed missing
fixture-owned domain metadata and lost origin snapshots; test-only
ownership/cleanup corrections preserved the original assertions and
timeouts. The new causal driver first lacked a final observation, then
encountered sandbox bind EPERM, then exposed the old closed-IPC error;
only the final real endpoint failure above is labeled the cleanup
counterfactual. An offline dependency miss and a generator-cleared
ignored compiler config were retained and corrected without weakening
compiler options. The former inherited policy failure (397 findings) is
resolved by the genuine current-master base/scanner, with zero findings
and seven scanner tests; no unrelated UI site was edited and the gate
was not relaxed.

Historical real TCP observer coverage passed 1/1 in 3.688 seconds.
Comment-only and opt-in-selection carries remain documented in prior
evidence; self-hash changes are metadata exceptions with fresh
executing-worker checks.

The full workspace typecheck, test, build and browser commands were not
run locally for this test-only slice. The current base passes its full
exact-head CI and Greptile 5/5; this leaf now has all 56 exact-head
check runs terminal (54 success, one neutral, one skip), the additional
legacy security status passing, complete current Greptile 5/5 and
Ponytail 5/5 reviews, and all four review threads resolved across all
pages. No live provider or deployed host/database suspension was
exercised.
## Risks

- The host test requires a matching service checkout and compiled
bridge. An unrelated service revision is not a valid substitute.
- Controlled localhost Git, S3 and physical process operations do not
qualify deployed Railway or Neon suspension, external permissions or
provider uncertainty.
- The Node guard does not prove native subprocess transport isolation.
Only finite local Git metadata and Node version commands were needed.
- Two short SQL/HTTP quiet windows do not establish strict 90-second
host acceptance. Separate plugin-host LISTEN, MCP wake and other domain
gaps remain open.
- Explicit test URLs are destructive fixture inputs. Use fresh, owned
`_test` databases. The launcher does not start a cluster or choose a
database implicitly.
- Passing CI and review do not qualify deployed suspension; the current
proof boundaries above still apply.

> For core feature work, check `ROADMAP.md` first and discuss it in
`#dev` before opening the PR. This PR adds verification to the existing
feature in its base.

## Model Used

OpenAI `gpt-6.1-sol` assisted with implementation, repository review and
local code execution. OpenAI Codex, based on GPT-6, performed managing
review and integration. The context-window sizes and exact served
backend snapshots were not exposed.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (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
- [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 — fresh merge-head gates pending
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups —
fresh merge-head review pending
- [x] I will address all Greptile and reviewer comments before
requesting merge


### Current master reconciliation (2026-11-06)

Merged actual master `e28158b353fd274f0322be76d62cd223e0df599f`
normally. All nine qualified acceptance code files and the held-port
repair remain byte-identical. Upstream independently landed the workflow
input fix in #19602; the fixture now uses that exact upstream version
once, avoiding duplicate object keys in a squash merge. All other master
changes are preserved. The earlier two-cycle local evidence remains
source-specific; no fresh 614-second run or deployed suspension is
claimed. This new head requires fresh CI and bot reviews.

### Actual combined-tree typecheck failure and repair

CI job `128156887548` failed with TS1117 at workflow-target.test.ts:22:
two independently landed master fixes declared the same `IMAGE_ONLY` and
`TESTOPS_REQUESTED` keys. The complete original log is retained. A
further normal master merge now removes the duplicate declarations,
retaining exactly one false/false pair and every original assertion. All
nine qualified acceptance code paths and the held-port repair remain
byte-identical; unrelated master changes are preserved. This is a
concrete compiler failure and source repair, with fresh exact-head
CI/reviews required.

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 15:55:05 -06:00
e482f0f632 fix(server): shed only retryable object-journal reads under load (#19176)
Client polling can exhaust the shared object journal queue and reject log persistence. Bound ordinary object-log and trace reads, keep persistence and internal verification queued, and return retryable 503 responses when reads are busy. Leave feedback exports unsent until every log page is available.

Validation: 72 latest-master affected tests and 10 workflow tests pass locally, with the workflow fixture passing strict TypeScript checks. The preceding broader run passed 280 server tests with two opt-in S3 skips. All applicable checks on d0a937768d are green (56 success, three skipped); exact-head Apex review is 5/5 with no unresolved findings. Background persistence still has no fixed queue bound during a prolonged storage outage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-11-06 13:47:25 -08:00
DottaandPaperclip c09fb60ca6 fix(recovery): retain worker-stop audit in preparation retries (#19612)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Recovery must preserve saved files before it admits another
execution.
> - A failed preparation can need a verified stop of its previous
workspace worker.
> - Recovery keeps that verified stop receipt in the run profile for
audit.
> - The preparation retry guard treats the retained receipt as unknown
execution evidence.
> - This pull request recognizes a valid same-run audit receipt while it
retains all current execution and file checks.
> - A saved Retry can then continue once after recovery completes.

## Linked Issues or Issue Description

**What happened?**

A native preparation failed before provider dispatch. The recovery
worker verified that the previous workspace worker had stopped and saved
its files. The same HTTP Retry stayed deferred because the run profile
retained `nativeWorkspaceOwnerStopProof`.

**Expected behavior**

The saved Retry should continue once after all preparation, stop, file,
ownership, and authorization checks pass. The old run and its audit
receipt should remain unchanged.

**Steps to reproduce**

1. Fail native preparation with a retained remote workspace and no
provider activity.
2. Let the recovery worker acquire the old workspace owner through
verified platform stop evidence.
3. Submit one HTTP Retry and let preservation complete.
4. Observe that the old guard retains the retry hold even after
saved-file verification passes.

**Paperclip version or commit**

Reproduced against `d728be8fbf`. Related work: #18908, #19021, #19026,
and #19466. This change covers the retained workspace-worker stop audit
left by a later repair.

## What Changed

- Add a typed reader for a host-persisted same-run worker-stop audit
receipt. Keep signature and expiry validation at ownership acquisition.
- Recognize only that validated audit field in failed-preparation
settlement. Keep unknown profiles and active owners blocked.
- Exercise real preservation, HTTP Retry delivery, native dispatch,
saved files, accepted results, and task completion. Add malformed,
foreign-run, unstopped-worker, and active-owner cases.
- Document the distinction between retained audit evidence and current
execution authority.

## Verification

- Failing before the fix: the current-worker preservation case passed;
the stopped-worker case produced zero successor runs after the same
Retry, where one was expected.
- Passing after the fix: 47 recovery cases, 10 native
dispatch/preservation cases, and 6 platform-proof tests. The two
preservation success cases overlap between the first two groups.
- Server typecheck passed all seven layers after the final test changes.
The Runner and SDK dependencies were built in this isolated worktree.
- Final comment-free commit `44f8f33f8d917dddb51852283a479786102eab4d`:
a fresh combined run passed all 61 affected cases in 67.92s. Fresh
server typecheck passed all seven layers.
- The earlier `95d0376c` combined run passed 60 cases and hit one
`socket hang up` in the existing `catalog_provider_warning` HTTP test.
That case passed in a bounded isolated run (12.48s), and the final
61-case combined run passed. The socket failure remains recorded as an
intermittent HTTP harness limitation. No retry or weaker assertion was
added.
- Final commit `44f8f33f8d917dddb51852283a479786102eab4d` completed all
required CI gates: 57 checks succeeded, 3 intentional skips, and the
advisory Paperclip review is neutral with a substantive 5/5 verdict.
Greptile is 5/5 and there are no unresolved review threads. [Final CI
run](https://github.com/paperclipai/paperclip/actions/runs/42293625696).
The follow-up commit only removes six new code-comment lines to follow
CONTRIBUTING.
- The native provider and signed platform API are controlled test
boundaries. The tests use real PostgreSQL, recovery, ownership, file
persistence, retry admission, dispatch, and finalization services.
Deployed recovery remains unverified until normal rollout.

## Risks

- The main risk is mistaking an audit receipt for current stop
authority. The reader checks its exact shape and run binding. The
receipt cannot replace stopped leases, saved-file receipts, settled
preparation, current authorization, or the absence of an active owner or
provider evidence.
- The negative matrix runs with the audit field present. Provider
activity, unknown usage, bad file receipts, Stop, actor changes, and
unsettled leases still block continuation.
- No schema or migration change is required.

## Model Used

OpenAI GPT-6 in Codex. The exact deployment model ID and context-window
size are not exposed in this session. Used reasoning, code editing,
shell execution, and test tools.

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

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

## Linked Issues or Issue Description

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

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

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

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

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

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

## What Changed

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

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

Deliberately kept on `gpt-5.6-sol`:

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

## Verification

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

## Risks

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

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

## Model Used

- Claude Opus 5.5 (Anthropic), model ID `claude-opus-5-5`, running in
Claude Code with tool use (shell, file edits, web fetch of the OpenAI
pricing page).

## Checklist

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

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 15:35:34 -06:00
Nicky LeachandClaude Opus 5.5 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
DottaandClaude Opus 5.5 dc1adb7209 fix(tasks): advertise a native resume only while the scheduler would claim it (#19523)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - When a native run cannot reattach to its provider after a
control-plane restart, the native coordinator resumes the same run by
itself after about 30 seconds, up to three attempts
> - The run projection shows this correctly as "Retry scheduled" with
only `inspect_run` permitted
> - The task interruption shown on the issue read only system-recovery
evidence and `scheduledRetryAt`. It never read the coordinator's
`nextAttemptAt`, so it showed `state: failed, canRetry: true` and
offered a manual Retry while the automatic resume was still pending
> - The two surfaces disagree, and a user click can race the automatic
resume. That is one way a Retry gets parked behind "execution checks"
> - #19503 made the task interruption read the coordinator's pending
resume time. Review found that the coordinator also pushes that time
forward while it defers an ineligible candidate, so a reassigned or
blocked task could hide Retry for good
> - This pull request advertises the resume only while the scheduler's
synchronous admission checks pass
> - The benefit is one consistent story: while the automatic resume is
pending, the task shows it with its time and no competing Retry. If
recovery ends, exactly one Retry returns

## Linked Issues or Issue Description

Follow-up to #19503. Refs #19487, #19497.

**What happened?**
A native run failed with `native_session_interrupted` after a restart.
Its finalization row had `phase: retryable_failure` and `nextAttemptAt`
30 seconds later. The run projection said "Retry scheduled". The issue
interruption said `state: failed, canRetry: true, nextRetryAt: null`.

**What did you expect?**
While the automatic resume is pending, the task shows that it is
resuming, with the time, and no Retry. When the resume fails for good or
never runs, exactly one Retry that works on the first click.

**Steps to reproduce**
1. Run a native task and restart the control plane so the native attach
fails.
2. Read `GET /api/issues/:id` while the coordinator's resume is pending.
3. Observe `interruption.canRetry: true` while `execution.phase` is
`retry_scheduled`.

## What Changed

- `task-interruption-presentation.ts`: the pending-resume read now also
requires the task to be in a resumable status, assigned to the run's
agent, not owned by another execution or checkout run, the run's
question not cancelled, and the agent invokable (not paused, terminated
or pending approval). These mirror `readNativeSessionResumptionAgent`.
- `native-session-resumption-statuses.ts`: one shared status set used by
both the scheduler admission and the read model.
- No time cap: provider cooldowns and remote backoff schedules stay
visible as a pending resume.

## Verification

- `task-interruption-query.test.ts` (embedded Postgres): a reassigned
task and a blocked task show one Retry; a paused agent asks to be
resumed; in each case `readNativeSessionResumptionAgent` also refuses
the candidate, so the read model and the scheduler agree. A resume an
hour out (provider cooldown) stays `retry_scheduled` with no Retry. Red
on master for the reassigned case.
- All 57 presentation and query tests pass. Server typecheck passes.

## Risks

- Low risk. Read model only.
- A review-assignment resume (reviewer is not the assignee) shows Retry
instead of the pending resume. That is the safe fallback.
- Deferrals for a pause hold, dependency readiness or a competing run
are not mirrored. Those gates also hold a manual Retry, so no competing
action appears.
- The `POST /issues/:id/retry` endpoint is unchanged.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with
tool use (shell, file editing, embedded Postgres 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
- [ ] 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:38:29 -06:00
DottaandClaude Opus 5.5 4938a930d6 fix(budgets): stop pending or restart-lost accounting from hard-stopping companies (#19497)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Budget admission and run accounting decide whether any task in a
company may start
> - A control-plane restart that kills a provider turn mid-flight can
lose that turn's final usage receipt. ACPX Claude reports usage only at
turn end, so the receipt never becomes complete
> - Accounting kept such a run "awaiting receipt" forever. Every later
admission in the company then refused with "completed runs await
accounting", and the budget evaluator opened a hard-stop incident that
paused the company. No product action could clear it: raising the budget
refuses while runs await accounting, accounting retry finds no new data,
and allowing unpriced usage does not apply to pending runs
> - Restart chaos waves showed this as a dead task Retry, a new task
blocked as "over budget" at 22 of 500 cents, and a hard stop that
cancelled queued Retries and an unrelated running task
> - This pull request closes the debt from evidence: once native
recovery reaches terminal failure and the provider stop is verified, the
known part settles as an unpriced charge. Until then the run holds new
work as recovering accounting, without a hard stop
> - The benefit is that a restart no longer bricks a company with a hard
budget. The operator's unpriced-usage policy decides admission, and
unknown spend still never shows as zero cost

## Linked Issues or Issue Description

Refs #19399, #19426, #19431 (earlier restart Retry fixes; this is the
budget layer under the same journey).

**What happened?**
After a control-plane SIGKILL during an ACPX Claude turn, the killed run
ended `native_provider_terminal_failed` with
`usage_json.accountingReceiptReady = false` and `cost_accounting_pending
= true`. It stayed that way for good. The task Retry was accepted but
parked with `budget.blocked` ("Company cannot start work while completed
runs await accounting"), which the UI showed as "Retry is waiting for
execution checks". A new Codex task was escalated to `blocked` with "the
original recovery target is over budget" while the company had spent 22
of 500 cents. After the next restart the budget evaluator opened a
hard-stop incident (`amount_observed` 22.08 < 500), paused the company
and cancelled both queued Retries.

**What did you expect?**
A restart should not stop the company. Unknown usage from a stopped
provider should become an honest, operator-governed state, and Retry
should start a run.

**Steps to reproduce**
1. Give a company a hard-stop budget.
2. Start an ACPX Claude task and SIGKILL the control plane mid-turn, so
the resumed turn fails.
3. Observe the run stay `cost_accounting_pending` with an incomplete
receipt, and every new admission refuse.

## What Changed

- `budgets.ts`: completed runs that await accounting now hold new work
only. They no longer open a hard-stop incident, pause the company, place
an agent budget hold, or deliver Stop to running work. Only spend at the
limit, or unpriced usage under a blocking policy, does that. Chaos wave
A and a pre-restart lifecycle cell showed the wedge: a run that had just
succeeded stayed terminal for about 100 ms before its charge committed,
the evaluator paused the company in that gap, and the Stop it delivered
left a cancelled run whose receipt never closed.
- `run-cost-accounting.ts`: a failed native run is a closed receipt
source only with positive stop evidence: its finalization is
`terminal_failure` with no result (not a finalization-deadline failure),
it no longer records a process identity, and its latest server-authored
process event is `native.local_process_stopped`. One SQL predicate
serves the reconciler scan and settlement. Accounting now settles its
known part as an `unpriced` charge instead of waiting forever. The
background reconciler selects these runs. The reservation settles. Other
incomplete receipts stay pending as before. A cancelled run keeps its
existing accounting drain, so Stop recovery can still deliver its final
receipt.
- `native-local-process-stop.ts` and its transactional callers:
releasing a verified local or remote process stop wakes the accounting
worker, so the closed debt settles promptly.
- `usage-receipts.ts`: a charge settled without its receipt is marked
`accountingReceiptUnavailable`. A later complete receipt that carries
its own price (including a rate-card price) corrects it. A receipt with
no own price and unchanged usage does not reopen the hold.
- `budget-exposure.ts`: the blocking unpriced-usage policy no longer
counts a charge settled from a lost receipt with no observed usage. That
is unknown, not "usage with no reliable price". Complete receipts that
report no tokens (for example Cursor Cloud) still count. A live wave
under the default policy hard-stopped its company at 0 of 500 cents on
one such charge.
- `budget-exposure.ts`: a terminal-failure native run with an
incomplete, not capture-failed receipt counts as recovering accounting.
Admission holds new work ("New work must wait while a native run
recovers its unfinished accounting") without a hard-stop incident, a
company pause or cancellation of running work.
- `recovery/service.ts`: the stranded-work comment names the actual
budget gate instead of always saying "over budget".
- `task-interruption-presentation.ts`: a Retry parked by
`budget.blocked` says it waits for the budget, not for "execution
checks".
- `doc/SPEC-implementation.md`: documents the closed-receipt contract
next to the existing incomplete-receipt rule.

## Verification

- New embedded-Postgres suite
`server/src/__tests__/closed-native-receipt-accounting.test.ts`, built
from the persisted shape of the killed run:
- Red before the fix: the run stayed pending, `reconcileRunCosts`
accounted 0, and admission refused with "completed runs await
accounting".
- Debt stays pending with no verified stop event, with a
finalization-deadline failure, with a recorded process identity, and
while the coordinator can still produce a result.
- Under a blocking policy, a lost receipt with no usage does not
hard-stop; one with tokens still blocks. A run with a still-recorded
process identity paused the company with a hard-stop incident.
- Green after: the stopped run settles as one `unpriced` charge with the
reservation settled; admission is clear under an `allow` policy and
names "no reliable price" under `block`; a run with a recorded process
identity or a non-terminal coordinator stays pending; the closing window
holds admission without an incident or pause; a final receipt that
arrives later still enters late-receipt reconciliation.
- Pending accounting from a run that just finished holds admission with
no incident, no pause, no enforcement version bump, and an unrelated
running run stays running. Red before the `budgets.ts` change: the
company was paused.
- A later priced receipt corrects the unavailable charge to its price;
an unpriced one leaves it settled.
- `task-interruption-presentation.test.ts`: the `budget.blocked` Retry
wait message.
- `budget-enforcement-scoping.test.ts`: the accounting-hold release
cases now assert that pending accounting places no budget hold and that
admission clears after settlement.
- Existing suites pass locally: all budget suites (control publication,
enforcement scoping, enforcement worker, evaluation work, live attempts,
history, service, live usage, budget cancellation),
native-stop-accounting, explicit-native-continuation,
native-case-handoff, cost-data-quality, heartbeat-cost-accounting,
run-cost-accounting, native-usage-delivery. Server typecheck passes.
- Runner e2e cell
`lifecycle-baseline.runner-codex.local.lifecycle-dependency-restart-neutral`,
which failed on master with a false agent budget hold before any
restart, passes on this branch.
- Live restart-chaos wave (local runner, K1 SIGKILL, codex + 2× ACPX
Claude + OpenCode, 4 restarts, company policy `unpricedUsagePolicy:
allow`) with the accounting fix: every killed run settled (no
`cost_accounting_pending` left), the company stayed active, the OpenCode
Retry completed the task on the first click and the Codex Retry finished
its task. That wave also exposed a 1-second false hard stop in the
closing window (observed 0 of 500 cents) that cancelled the running
OpenCode turn; the `budget-exposure.ts` change fixes it and has its own
red/green test.
- Confirming live wave with both commits, same plan (2× ACPX Claude,
OpenCode, Codex; 4 K1 restarts): both Claude tasks passed R3 (one Retry
click started a run that completed the task), OpenCode passed R1
(continued by itself), and the Codex Retry completed its task. Zero
budget incidents, zero cancelled runs, zero runs left pending
accounting, company active; three killed runs recorded as `unpriced`
charges. The only open signature is an unrelated workspace-durability
check (`R2:pre_fault_files_lost` on the Codex long-bench task).

## Risks

- Behavior change: a stopped native run's lost receipt now becomes an
`unpriced` ledger charge with its known usage. If that usage has tokens,
the default `block` policy still stops new work with "recorded usage has
no reliable price". If it has none, the charge is visible in Costs but
does not stop the company. Other unpriced charges keep their existing
effect. Unknown spend is never recorded as a priced receipt. Budget
enforcement stays best effort, as the spec says; killed OpenCode and
Codex requests already lose their in-flight usage.
- Remote runs and runs without a verified local stop keep their debt
pending (an admission hold only, no hard stop).
- Contract change: pending accounting no longer pauses a scope or stops
running work. New work still waits with "completed runs await
accounting". A scope's real hard stop (spend at the limit, or blocked
unpriced usage) is unchanged.
- If a provider stop can never be verified, the run stays "recovering
accounting": new work waits without a hard-stop incident. Before, the
same state paused the company. The hold is visible in admission errors
and Costs.
- A receipt that arrives after settlement posts a late correction. The
original unpriced charge stays in the ledger.
- No schema or migration change.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with
tool use (shell, file editing, embedded Postgres tests, live
restart-chaos harness).

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 14:27:51 -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
Devin FoleyandPaperclip 50038fac70 fix(native-runtime): contain backup verification deadline aborts (#19556)
Reject an already-aborted backup verification before constructing its file stream so cancellation does not raise an uncaught AbortError. Validation: 60 focused tests and all applicable CI pass.

Greptile scored the exact head 5/5, and no review threads remain unresolved.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-11-06 12:04:56 -08: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 c6eecac1e0 fix(heartbeat): keep a shutdown-suspended native recovery for the next boot (#19530)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The Runner Full-Stack E2E workflow tests the Runner in Daytona
sandboxes from a signed, content-addressed image
> - Every change to an audited Runner input needs a new image before a
Daytona cell can test it
> - The image job took 10 to 16.5 minutes today, and a restart chaos
campaign waits on it after each runner-side fix
> - Measurement shows two serial waits: a full Rust dependency compile
(about 5 of 6.6 minutes) and a 3.5 to 4 minute download of cached base
layers that starts only after the builder stages finish
> - There is also no way to build only the image: every dispatch reaches
a paid gate that the operator must reject
> - This pull request removes both serial waits and adds an `image_only`
dispatch input
> - The benefit is a shorter fix-to-Daytona-test loop, with every image
security check unchanged

## Linked Issues or Issue Description

**What happened?**

The `Publish verified Daytona image` job is on the critical path of the
control-plane restart chaos campaign. Builds today took 16.5 min (run
42240434011), 15 min (42253920727), 10.5 min (42259118276) and 16 min
(42265637650). Run 42264611573 failed: its RunsOn runner lost
communication with GitHub 7.6 minutes into the build step, during the
Rust compile. GitHub has no log for that job.

**Expected behavior**

A Runner source change should not recompile the locked third-party Rust
graph. Cached image layers should download while the builder stages
compile. An operator should be able to publish only the image, without a
paid gate.

**Steps to reproduce**

Read the BuildKit timeline in the logs of job 127970125495 (run
42240434011). See the stage table below.

## What Changed

- `docker/daytona-runner/Dockerfile`: compile the locked Rust dependency
graph in its own layer against placeholder targets. The layer depends
only on `Cargo.toml`, `Cargo.lock` and the crate manifest. The step
deletes the placeholder outputs and fingerprints, so the next step
always compiles the real Runner sources. The final stage still receives
only the stripped executable.
- `docker/daytona-runner/Dockerfile`: move the content-ID /
source-revision check above the two `COPY --from` steps. It does not
depend on either builder stage, so BuildKit now downloads the cached
base and CLI layers in parallel with the compile. Both copies use `COPY
--link`.
- `.github/workflows/runner-full-stack-e2e.yml`: add the boolean
`image_only` dispatch input. `authorize`, `target_lock`, `catalog` and
`daytona_image` run unchanged. Every other campaign job gets `&&
inputs.image_only != true`. `authorize` rejects `image_only` together
with TestOps inputs.
- `tests/runner-e2e/workflow-image-only.test.ts` (new): pin the job
classification, the top-level guard on each campaign job, the guard on
every protected environment, and the TestOps rejection.
- `tests/runner-e2e/README.md`: document the layer layout and the
`image_only` command.

## Verification

Measured on the same RunsOn `m7i-flex.xlarge` (4 vCPU, 16 GiB, native
x64) runner class. "Before" is master job 127970125495 (run
42240434011). "After (cold)" is this branch, job 128077687214 (run
42271236140), with no dependency layer in the registry cache yet. Its
paid gate was rejected.

| Stage | Before | After (cold dependency cache) |
|---|---|---|
| Whole image job | 16.5 min | **11.6 min** |
| Rust dependency graph | 311 s (inside one 399 s step) | 395 s (own
layer; reused from cache after a master build) |
| `paperclip-runner-core` compile | ~87 s | 65 s |
| Cached base and CLI layer download | 236 s, **after** cargo finished |
257 s, **in parallel** with cargo |
| Copy runnerd + provider pack | 20 s + 50 s | 9 s (`--link`) |
| Provider pack stage | 143 s (parallel) | 152 s (parallel) |
| Export + push | 82 s + 10 s | 81 s + 11 s |
| Dependency layer size | n/a | `target` 852 MB + registry 333 MB
(builder stage only) |

With a warm dependency cache, a Runner source change compiles only the
65 s crate step. The critical path then becomes the ~4 min cached-layer
download or the provider pack. The expected job time is about 7 to 8
min. I measure this after merge with an `image_only` dispatch (master
first, to write the cache, then a Rust-only branch change).

Disk: the instance starts with 38.76 GiB free. The dependency layer adds
about 1.2 GB uncompressed to builder storage. The final image does not
change.


- `vitest run tests/runner-e2e/workflow-image-only.test.ts
tests/runner-e2e/workflow-security.test.ts
tests/runner-e2e/daytona-image.test.ts`: the new test and all image
tests pass. Three tests in `workflow-security.test.ts` (`gates every
provider-secret job with stable actor IDs`, `keeps provider credentials
inside explicitly gated paid workflows`, `isolates private archive trust
from workflows that execute target-branch code`) fail on `origin/master`
without this change, because of `chat-catalog-testops-case.yml`. This
pull request does not change them.
- `node --test tests/runner-e2e/product-regression-testops.test.mjs`:
15/15 pass.
- No local Docker build. The Dockerfile is measured with a
branch-targeted CI dispatch whose paid gate was rejected.

## Risks

- The dependency layer keeps the Rust `target` and registry trees in a
builder-stage layer, and default-branch builds export it to the registry
cache. This adds builder disk and cache upload when `Cargo.lock`
changes. The build prints the size (`du -sk target
/usr/local/cargo/registry`).
- The dependency cache helps only after a default-branch build writes
it. Branch builds read it but never write it. This is the existing cache
policy.
- `image_only` changes the workflow, so it works only after merge to
master.
- No check is removed. Lockfile verification, cosign sign and verify,
the anonymous pull check, the provider-pack manifest match, the
default-branch-only cache write and the actor allowlist are unchanged.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, through Claude Code
with tool use (shell, 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 13:01:16 -06:00
Devin FoleyandPaperclip fd4b197f83 fix(ui): hide runner selection during agent setup (#19517)
## Thinking Path

> - Paperclip lets people manage AI agents for work.
> - Agent setup connects a harness, model, account, and computer.
> - The system already selects a qualified runner for a new agent.
> - Setup still asks users to select that internal runner.
> - The runner dropdown can be the only field in Advanced.
> - This PR removes the dropdown and hides an empty Advanced section.
> - Users keep their saved execution settings and see fewer setup controls.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

Agent creation, onboarding, built-in agent setup, company import, and agent configuration.

**Subsystem affected**

The UI, setup error messages, tests, and Storybook fixtures.

**Current behavior**

Users can select a runner from a dropdown. Some setup screens place only that dropdown in Advanced. Errors can direct users to that control.

**Proposed behavior**

Remove runner dropdowns from setup and configuration. Use the existing automatic selection for new agents. Retain saved runner settings. Show Advanced only when other fields remain.

**Reason and benefit**

Users can configure agents without an extra internal execution choice. Setup also avoids an empty Advanced section.

**Breaking changes**

Users can no longer select a runner through these UI dropdowns. The API and CLI retain their runner options. Existing agents keep their saved execution settings.

**Additional context**

Related: [Default new agents to the qualified runner for their harness](https://github.com/paperclipai/paperclip/pull/15422). This PR changes the existing setup UI. It adds no harness or runtime default. The master merge retains the custom gateway placement from [#19510](https://github.com/paperclipai/paperclip/pull/19510).

## What Changed

- Remove the shared runner dropdown and its setup/configuration callers.
- Hide Advanced when it has no remaining fields. Keep command and argument settings where supported. Preserve custom gateway setup under Advanced on the Claude and OpenAI API key paths.
- Keep setup failures visible and keep the retry action outside the removed control.
- Update errors and documentation that referred to the removed dropdown.
- Update component tests and Storybook fixtures for the new UI states.
- Make Legacy first-task evals resume an API-configured setup record. Record that path separately from fresh automatic onboarding.

## Verification

- Latest master integration (`187c0f6d6e5`): 276 onboarding/provider-connection tests passed across six files. UI typecheck and build passed. Token gates, query-error rendering checks, and the Storybook catalog check passed. The conflict resolution retains master's bounded connection tests and cancellation while removing the obsolete runner instruction. The first test command used the wrong working directory and failed before collecting tests; the corrected invocation from `ui/` passed.
- Earlier branch verification passed: full workspace build and typecheck, 1,037 focused UI/service tests, 49 heartbeat provider contract tests, eval typecheck, and 30 map/catalog tests.
- The full local `pnpm test:run` run passed all six chat integration shards (1,931 tests). It was stopped for the master merge before the complete suite finished. The [complete CI run](https://github.com/paperclipai/paperclip/actions/runs/42264662642) passed on the previous head `72e1699`.
- Current head `a097c8369d079c839e78661a2206cfa62c5210e0`: [full CI](https://github.com/paperclipai/paperclip/actions/runs/42273168268) passed. PR checks total 55 passed and 3 skipped, with none failed or pending. Greptile reviewed this head at 5/5 with no actionable findings or unresolved review threads.
- Live browser/provider evals have not run. The Legacy eval change has no live qualification claim.

## Risks

- Removing the dropdown changes the setup UI. Existing API and CLI runner choices remain available.
- Conditional Advanced sections must retain valid command and argument fields.
- Legacy first-task evals now prove setup resumption. They do not prove the fresh-agent automatic default.
- No schema change or data migration is included.

## Model Used

OpenAI `gpt-6.1-sol`, through Codex in T3 Code. Reasoning effort: `xhigh`. The model used file editing, shell commands, and test tools. 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
- [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 10:58:42 -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
DottaandPaperclip e3fb4bc73a fix(native): recover saved listener startup failures (#19479)
## Thinking Path

> - Paperclip manages AI agents and their work.
> - Native recovery must preserve saved work and respect current task
authority.
> - A closed local API listener can stop startup before a provider
starts.
> - #19462 gives new failures a bounded bootstrap retry.
> - Older saved failures still have a generic terminal code and a
recovery hold.
> - This pull request verifies the old producer outcome and uses the
existing safe replacement path.
> - Eligible tasks continue once. Unknown provider outcomes and user
holds remain blocked.

## Linked Issues or Issue Description

Refs: #19462

**What happened?**

A restart can leave an old listener startup failure in
`terminal_failure` with `native_session_interrupted`. Startup recovery
does not claim it after #19462. A completed file recovery can also
retain its no-replay hold.

**Expected behavior**

Continue automatically when durable records prove that no provider
started, the source stopped, and the current files were saved. Keep the
existing attempt budget and normal admission checks.

**Steps to reproduce**

1. Close the actual API listener after a fresh native coordinator claim.
2. Persist the old executor's generic failure. Save the workspace and
confirm source termination.
3. Restart recovery. The baseline schedules no successor. The repair
schedules one successor after all receipts pass.

**Paperclip version or commit**

Baseline: `0252421d5c`. Old failure producer: `2f35eb1080`.

**Deployment mode**

Built from source. Local macOS arm64, Node 26.4.0, real embedded
PostgreSQL, independent filesystem execution. No external AI service is
used by the controlled acceptance.

## What Changed

- Recognize the exact saved local-listener guard through one proof
predicate shared by discovery and the locked transaction.
- Require a fresh first attempt, matching execution bindings, no prior
provider lineage, no provider events or tool calls, no checkpoint or
admission receipt, and no cancellation.
- Reuse current snapshot, exact stop, source block, attempt budget, and
successor idempotency checks.
- Wait for completed source file-recovery receipts. Retire only that
same source's verified preservation hold in the successor transaction.
- Bound discovery to 100 candidates per pass, keep completed history
quiet, and wake through existing transactional producer notifications.
- Retain retries after transient database and final ownership-proof
errors; advance the bounded cursor and force a rescan so a failing page
cannot hide later work.
- Let only the replacement duty discharge a proved-empty notification
during drain through its existing read-only readiness check, once per
notification generation. Keep real work and concurrent changes
outstanding without running replacement during drain.
- Wake replacement at canonical local-stop proof writes and the existing
dispatch eligibility boundary; these hints grant no new execution
authority.
- Add database regressions, scheduler quietness checks, real controller
handoff coverage, and an opt-in controlled Runner continuation.

## Verification

Final production head: `0df41b2fef2644195d1224d36984af3827da3e3b`.

Local environment: macOS arm64, Node 26.4.0, TypeScript 7.0.2, Vitest
5.0.3, frozen-lockfile dependencies. Tests execute freshly;
`--fsModuleCache` reuses only content-validated module transforms.

- Native safe replacement: 62/62 PASS on isolated PostgreSQL 17 (63.18
seconds), including clean per-suite database disposal. All 62 assertions
also passed on embedded PostgreSQL 18 beta, but its Darwin cleanup hook
timed out; that invocation is not counted as a pass. Docker's disk was
full, so the disposable PostgreSQL 17 data used a size-limited RAM
mount, with no host mounts. The owned container was removed afterward.
- Actual stopped-process producer suite: 31/31 PASS. A real exited child
produces the stop receipt, and its replacement notification settles only
after the outer transaction commits.
- Workspace and separate-process drain regressions: 33/33 PASS (75.90
seconds).
- Shared coordinator and execution-control lifecycle checks: 39/39 PASS
(16.43 seconds). Pending work, concurrent changes, cold startup,
shutdown, and passive drain boundaries remain covered.
- Actual suppressed Cloud routine/drain workflow: PASS (34.64 seconds),
using the unchanged test and timeouts. Its matching local RED reproduced
CI's 462-query, zero-pending-query, one-idle-hold timeout after an
admitted write committed during drain (69.46 seconds).
- Actual historical controller handoff: PASS (48.40 seconds). A separate
controller loads the original executor from `2f35eb1080`, writes its
generic listener failure, and exits. Recovery at the final head admits
exactly one successor. The real Rust Runner and controlled Codex process
complete governed `paperclip_finish`; attempt two commits and the saved
file remains intact. Only the child loads the old artifact, and both
isolated artifacts are restored afterward. Rust source subtree
`77961b70f177ec3c38d11abcaec92fd3d0e96ec0` matches the built binary. No
external AI service is used.
- Seven corrected existing fixtures pass: transactional stop-proof calls
(5 cases) and notification inventory (2 cases). Their earlier CI
failures exposed fixture calls outside the documented transaction and an
outdated expected queue set.
- Server typecheck: PASS.

Red/green proofs also cover the original saved failure on baseline
`0252421d5c` (zero successors instead of one), held-history quietness,
bounded forward scanning through 100 parked rows and concurrent wakes,
real snapshot/cleanup producer notifications, actual issue-row lock
timeout, and task/history eligibility notifications. An obsolete
prior-run question can be skipped to request another proof check; actual
skip while the task is paused or the source owns provider tool activity
schedules no successor.

The failed-page regression starts with 25 persistent verifier errors and
a healthy candidate 26. Baseline RED repeats the first page. Repair
GREEN advances the healthy candidate, revisits the original failures
after their fault clears, creates all 26 successors exactly once, and
becomes quiet.

Greptile is SUCCESS, 5/5 on the exact final head (2026-11-06 17:09:21
UTC). Final-head CI is GREEN: 56 successful checks, three intentional
skips, one neutral result, and no pending or failing checks. [Attempt
two and
results](https://github.com/paperclipai/paperclip/actions/runs/42258352876/attempts/2)
include passing build, typecheck, Rust, native server, package, browser
E2E, serialized server, general server/chat tests, and final
verification. Five jobs in the first attempt were interrupted together
at 17:13:12 UTC; the native-server log records an explicit runner
shutdown signal. The single infrastructure retry reran only those five
jobs and dependent verification, preserving the 49 successful checks on
the unchanged head. No timeout was weakened, and setup failures or
interrupted invocations are not counted as passes.

Focused commands from the repository root:

```sh
./server/node_modules/.bin/vitest run --project @paperclipai/server server/src/services/remote-workspaces.integration.test.ts server/src/services/native-runtime/native-runner-restart-preparation.integration.test.ts -t 'native safe-replacement decision|reconciles the drained remote backend' --fsModuleCache --reporter=dot
./server/node_modules/.bin/vitest run --project @paperclipai/server server/src/__tests__/execution-control-work.test.ts server/src/__tests__/delivery-work-coordinator.test.ts -t 'execution-control lifecycle|work coordinator' --fsModuleCache --reporter=dot
node node_modules/typescript/bin/tsc -p server/tsconfig.json --noEmit --pretty false
```

The historical acceptance sets `PAPERCLIP_TEST_HISTORICAL_LISTENER=1`
and uses an original-executor child artifact with a `try/finally`
restore. Normal regressions require no Rust binary.

## Risks

- Compatibility is deliberately limited to positively proved fresh
remote startup failures. Other old ambiguous failures remain held.
- Existing provider work, a changed task block, incomplete preservation,
or revoked authority must prevent execution. Normal retry admission
remains in control.
- This PR does not deploy the change or mutate a hosted task. Hosted
recovery still requires the repaired revision and all current proof
gates.

## Model Used

OpenAI GPT-6 through Codex, with reasoning, tool use, and code
execution. The exact backend model identifier and context window are not
exposed in this session.

## Checklist

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

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

## Linked Issues or Issue Description

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

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

**Root cause**

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

## What Changed

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

## Verification

Red, then green, at each layer:

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

Suites run locally on the final revision:

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

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

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

## Risks

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 11:39:29 -06:00