Commit Graph
609 Commits
Author SHA1 Message Date
DottaandClaude Opus 5.5 ef83d566ca fix(native): ask for the result once after a restart continuation ends without one (#19771)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs continue a turn that a control-plane restart lost, in
the same conversation (#19501, #19699)
> - In restart storms 5, 6b and 8, ACPX Claude runs crossed restarts,
ran the continuation, and then failed with
`native_session_retry_exhausted`
> - The continuation completed, but Claude ended it with "I'll wait for
the notification when it completes" for a background command, and did
not call `paperclip_finish`
> - The next same-run attempt then sent no turn at all and failed with
"session returned no semantic result"
> - This pull request makes the continuation ask for the result, and
asks once more in the same attempt if the continuation still ends
without one
> - The benefit is that a restart-interrupted task settles without a
Retry, and the restarts that the run survived cannot block the request

## Linked Issues or Issue Description

Refs #19501, #19699, #19720

**What happened?**

Storm 8, run 84c532f7 and its Retry 0faead66 (ACPX Claude, task CHA-5).
The steps are the same in both runs:

1. `resume_dead_runner`, then `session.resumed`, then `turn.failed` with
`provider_turn_lost_on_restore` (recoverable).
2. The restart continuation runs. Claude starts the bench in the
background and ends the turn with "I'll wait for the notification when
it completes". The turn completes with no `paperclip_finish`.
3. Lifecycle: "provider turn completed without a semantic result;
same-run disposition recovery persisted" (attempt 2).
4. 30 s later, attempt 3 restores the session. It sends no turn. After
about 2 s it fails with `native_finalization_missing: session returned
no semantic result`, and the attempt count makes it
`native_session_retry_exhausted`.

**Expected behavior**

The continuation tells the agent to finish and report its result. If the
continuation still completes without a result, Paperclip asks for the
disposition one time. Restarts that the run already survived do not
block that request. An agent that never reports a result still fails
closed.

**Root cause**

- `packages/paperclip-runner/src/drivers/codex/codex-harness-session.ts`
`startTurn` (lines 224 and 248): the restart continuation uses the
driver's one-shot disposition marker
(`dispositionOnlyRecoveryAvailable`). #19699 uses the marker on purpose,
so that a crash can adopt the accepted continuation turn. The marker
then stays consumed with the continuation turn id.
-
`packages/paperclip-runner/src/drivers/codex/codex-session-terminal.ts`
(line 226): only a *lost* continuation gives the marker back. A
*completed* continuation with no result does not.
- `packages/paperclip-runner/src/native-session-runtime.ts`
(`dispositionRecoveryWasSubmitted`, about line 2761): on the next
attempt, the consumed marker looks like a disposition request that
already ran. Recovery replays the continuation's terminal and starts no
turn. So the disposition request never happens.
- `server/src/services/native-runtime/native-session-executor.ts` (lines
9755-9761 and `nativeSessionFailureDisposition` at 7768): the
`semantic_result_missing` same-run retry charges a provider attempt on
the same counter as restart resumes. In this run, attempt 3 was the last
allowed attempt.
- The continuation prompt (`restartContinuation`) did not tell the agent
to report a result, or not to stop and wait for a background
notification.

## What Changed

- Codex-protocol driver (used by Codex and by ACPX through runnerd):
when a restart continuation completes without a semantic result, the
driver gives back the one-shot disposition marker. The disposition
request is not a continuation, so it consumes the marker for good.
- Native session runtime: after that, the same attempt sends one
disposition request (`Paperclip semantic-result recovery after a restart
continuation ... invoke exactly one paperclip_finish or
paperclip_block`). It does this only when the driver shows that the
continuation borrowed the marker and gave it back. Other drivers keep
the current behavior.
- Restart continuation prompt: it now says to finish in this turn, not
to end the turn to wait for a background command or a notification, and
to report one `paperclip_finish` or `paperclip_block`.
- `doc/execution-semantics.md`: one additive paragraph.
- No server change. The disposition request runs in the same attempt, so
it uses no provider attempt. This avoids `native-restart-recovery.ts`
(#19747).

## Verification

Red before the fix, green after:

- `codex-app-server-driver.recovery.test.ts` "keeps one disposition
request after a restart continuation completes without a result". Red:
marker stays consumed on `turn-2`. Green: the marker is given back, one
disposition turn starts, and a resultless disposition turn does not
re-arm.
- `native-session-runtime.test.ts` "settles a restart continuation that
ended without a result through one disposition turn (Codex driver)".
Real Codex-protocol driver with a fake transport. Red: only one
`turn/start`, then `native_finalization_missing`. Green: the disposition
turn `turn-3` reports the result and the run completes.
- `native-session-runtime.test.ts` "asks once for the disposition when a
restart continuation ends without a result" and "fails closed when the
driver keeps the continuation's disposition marker".

Also run:

- `npx vitest run src/native-session-runtime.test.ts src/drivers/codex/`
in `packages/paperclip-runner`: 593 passed.
- `src/live/runnerd-restart-continuation.test.ts` (real runnerd and fake
Codex, killed runner): 5 passed.
- `tsc --noEmit` for `packages/paperclip-runner`.
- `pnpm verify` exits 2 because the doc change is a global CI input.
Required CI supplies the broad coverage.

Not run: a live replay of storm 8 t05 with Claude. The chaos harness
needs provider keys from the operator's shell.

## Risks

- Low to medium. A run whose continuation ends without a result now gets
one more provider turn in the same attempt. The bound is one disposition
request per continuation. Each continuation still needs an admitted
restart, and the provider-attempt budget bounds those.
- If the controller crashes after the continuation completes but before
its checkpoint, the checkpoint keeps the stricter consumed marker. That
case fails closed as before.
- The prompt text changes for restart continuations. It does not change
a protocol schema or a profile digest.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 03:22:37 -06:00
DottaandClaude Opus 5.5 6adc8b1049 fix(recovery): keep restarted Retry attempts as task directories and settle failed re-preparation (#19759)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Task recovery lets a user click Retry after a run fails, and a
native run restarts its own preparation after a control-plane restart
> - A Retry for a task without a project runs in a fresh task directory
under `isolated-workspace-attempts`
> - When a restart hit that Retry during bootstrap, preparing it again
treated the task directory as the base of a new git worktree and failed
> - The failed run then kept its native recovery claim open, so the task
showed Retry but every Retry click created no run
> - This pull request keeps the restarted attempt in its task directory
and settles a failed re-preparation
> - The benefit is that a Retry finishes after a restart, and a failed
one can be retried again

## Linked Issues or Issue Description

Refs #19666

**What happened?**
A task without a project failed after a restart
(`native_session_interrupted`). The user clicked Retry. The Retry run
started in a fresh task directory. A restart hit it during bootstrap.
Restart recovery prepared the same run again and failed with
`workspace_validation_failed`: "requested isolated_workspace with
git_worktree, but base workspace
`.../isolated-workspace-attempts/<company>/<issue>/<run>` is not a git
checkout". The task rested blocked with Retry shown. A later Retry click
created no run: the receipt stayed deferred with "New work must wait
while a native run recovers its unfinished accounting." A second storm
wave showed the same failure on a Claude C1 task.

**Expected behavior**
Preparing a run again after a restart reuses the task directory that run
already bound. A git worktree base is only the project's checkout or the
task's own worktree. A run whose re-preparation fails leaves a working
Retry.

**Paperclip version or commit**
`master` at `51eb4ff705` and `131b566ca4` (both include #19666).
Restart-chaos waves on 2026-11-07.

**Steps to reproduce**
1. Use a task with no project and isolated workspaces enabled. Let a
native run fail after a restart.
2. Click Retry. The run starts in
`isolated-workspace-attempts/<company>/<issue>/<run>`.
3. Restart the control plane while the run bootstraps.
4. The re-preparation fails with `git_worktree_base_not_git_checkout`.
Click Retry again: no run starts.

Root cause:
- `server/src/services/fresh-workspace-continuation.ts`
`readFreshWorkspaceReadiness` returns `null` for a native run that
already has admitted input and a bound workspace (from #19693). This is
correct: the restart must resume the admitted workspace.
- `server/src/services/heartbeat/environment-selection.ts`
(`boundTaskDirectory`) excluded every native recovery binding from the
task-directory path. With readiness no longer fresh,
`useIsolatedTaskDirectory` was false. The default `isolated_workspace`
strategy (`git_worktree`) applied, with the task directory as its base.
- The restart recovery claim set the native coordinator to `observed`
with its lease owner. `failRunSetup` failed the run, but nothing
released that claim. `budgets.ts` reads an open coordinator as
unfinished accounting, and Retry admission reads it as
`controller_settling`.

## What Changed

- Environment selection treats a native recovery binding as the task
directory when it is the task's current binding and a bound isolated
task directory. The directory strategy is kept; it never becomes a git
worktree base. A recovery binding the issue no longer points to keeps
its existing path.
- New `settleFailedNativeRecoveryPreparation`. When a
`bootstrap_incomplete` or `resume_dead_runner` restart claim fails its
re-preparation before any provider dispatch, it settles the claimed
coordinator as `terminal_failure` and wakes deferred work. It only
changes a coordinator that is still `observed`, owned by that claim,
without a result, for a failed run. Reattach claims (live runner) are
not changed.
- `doc/task-recovery.md` describes both rules.

## Verification

- `environment-selection.test.ts`, RC-58 cases: a restarted fresh
attempt bound to the task re-prepares as its task directory
(`useIsolatedTaskDirectory` true). It failed before the fix and passes
now. A binding the issue moved away from keeps the old behavior.
- `native-runtime/native-recovery-preparation-failure.test.ts` (embedded
Postgres): before settlement, Retry admission reports
`controller_settling` (the dead Retry). After settlement the coordinator
is `terminal_failure` with the run's error code, and admission no longer
waits on the controller. Settlement is idempotent. Another owner, a
reattach claim, a run that is not failed, and a coordinator finalizing
elsewhere are left unchanged.
- Local regression: `environment-selection`, `workspace-preparation`,
`retry-workspace-continuity`, `native-recovery-preparation-failure`,
`explicit-native-continuation` (588 tests passed).
- Restart-chaos: local restart-chaos runs on `3a7abef4e5` passed, but
neither one reproduced the exact failure (a restart during the Retry's
bootstrap). The race depends on timing, so the unit tests above are the
red/green proof for both defects.
- `--replay w-20261107T011010-s7403/t02` (codex C1 K4 with the original
fault sequence): PASS, R1-R6 and T. The replay's Retry did not cross a
bootstrap restart.
- Wave `w-20261107T064432-s7210` (codex+claude, C1+B1, K1/K4/K6,
`--triggers retry`, concurrency 2): 8/8 PASS. It had 4 Retries,
including C1 tasks with a fault fired right after the Retry click. No
`git_worktree_base_not_git_checkout` and no dead Retry. The server log
has no "incomplete native runner bootstrap" restart in this wave either.
- `ce7e3f9b19` registers execution work before the settlement writes
(Greptile finding).

## Risks

- A native recovery now re-prepares a bound task directory through the
task-directory path. That path still validates the binding, owner,
status and directory identity before use.
- Settling a failed re-preparation ends that run's recovery claim. It
applies only when the previous runner is verified gone
(`bootstrap_incomplete`, `resume_dead_runner`) and no provider dispatch
started in this attempt. A Retry still passes its normal stop, ownership
and admission checks.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 02:17:24 -06:00
DottaandPaperclip 909f4d1a33 fix(native): recover exact historical provider pack code (#19779)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native sessions retain the provider pack that admitted their saved
state.
> - An image update can replace that pack's executable, proxy, or
contract.
> - Older sessions can predate the durable three-file launch artifact
receipt.
> - The current image's archive cannot satisfy a different pinned
manifest.
> - This pull request obtains the exact public code from its recorded
release image.
> - Full source, archive, and installed-file checks still control
provider admission.

## Linked Issues or Issue Description

Refs #19558, #19622, #19755.

A saved OpenCode session can fail with
`runner_remote_provider_archive_invalid` after an image update. Its
original provider state and checkpoint remain valid, but its pinned pack
differs from the controller's pack. The old manifest can name a release
that still has the exact code in GHCR. Matching the OpenCode version
does not prove matching proxy or contract bytes.

## What Changed

- Resolve a pinned historical pack from the recorded source-SHA image in
the public Paperclip GHCR repository. Verify the platform manifest, raw
config, source identity, dedicated code layer, selected pack manifest,
archive contents, paths, and hashes.
- Keep a private temporary cache of verified public code. Rebuild it on
an empty host. Bound each source to 4 GiB and three minutes, with two
active imports or cache verifications, 32 distinct queued sources, and
disk-headroom checks. Share warm-cache verification by manifest while
each caller keeps its own cancellation.
- Select this verified archive through the existing uploader. Preserve
installed-pack and preinstalled-pack checks. Refuse unavailable
historical transport before clearing the target. Keep the current-source
identity guard from #19755. Refuse an oversized layer argument before
any target effect.
- Attach the source wait to the actual per-turn startup. Stop, lease
loss, and startup completion cancel it. Keep existing Runner admission
and turn limits.
- Document the registry retention requirement and the distinction
between public code recovery and encrypted provider-state backup.

## Verification

- Genuine baseline RED: the real uploader rejects the selected old pack
before any remote effect. Treatment installs the exact old code through
that uploader.
- Current head: all 19 source/integrity/cache/deadline controls, all 45
archive controls, and 29 actual-caller and manifest/startup controls
pass (93 total). Four review regressions fail against the prior head and
pass with the fixes. The server and colocated test TypeScript graph
passes. Module-boundary and whitespace checks pass.
- Actual public GHCR cold reconstruction: 1,054,587,008 compressed code
bytes, 115.4 s, about 248 MiB maximum owner RSS. All 114 file layers and
the overlay passed source validation.
- Linux Node 24.22.2 fixture: the real historical pack passes the normal
uploader, full installed-file verifier, and pinned OpenCode version
check. It used one transfer, no application startup, no network, and no
provider calls. Revision12b0d2ba passed in 220.2 s with about 282 MiB
owner RSS. The earlier prototype passed in 108.5 s; both measurements
are retained. The three-minute source limit and the fixture's
four-minute limit were unchanged. The later review fixes add shared
cache verification, hash cancellation, and an early size guard. They do
not change extraction or the accepted Linux fixture input.
- Earlier fixture failures are retained: unsafe hardlink classification
was corrected with confined-link controls; a cached image lacked the
required zstd CLI; Docker Desktop bind storage caused GNU tar
directory-identity errors. The passing fixture uses the official Debian
zstd binary and an owned Linux tmpfs target. Production extraction code
is unchanged.
- Fresh Greptile review is 5/5 on
7b1a249478 with no open findings. All
required CI gates passed on the reviewed head. [CI
run](https://github.com/paperclipai/paperclip/actions/runs/42353011485),
[fresh
review](https://github.com/paperclipai/paperclip/pull/19779#issuecomment-6704065994).
No hosted session was retried by this change.

## Risks

- Cold historical recovery reads about 1 GiB of public code for the
measured release. Registry latency or missing images can block it. The
source deadline remains three minutes.
- GHCR administrators can delete release images. Source-SHA tags and
this temporary cache are not a permanent code-retention guarantee. A
provider-state S3 backup does not replace legacy executable bytes.
- Targets must support the existing qualified archive transport and
selected platform. Failed checks preserve the saved state and prevent
provider dispatch.
- The Linux fixture proves code installation and verification. Hosted
conversation continuation still needs acceptance after normal delivery.

## Model Used

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-07 02:06:02 -06:00
DottaandPaperclip ae2d2956b3 Reject relative local Runner paths before native admission (#19776)
## Thinking Path

> - Paperclip manages AI agents and their work.
> - Heartbeat selects the runtime that will execute a task.
> - A new local Native run uses the controller's Runner binary setting.
> - A relative binary path is invalid. The resolver already rejects it,
but that check occurs after Native admission.
> - This pull request checks the same setting before it creates Native
execution authority.
> - Remote providers and admitted Native recovery keep their existing
behavior.

## Linked Issues or Issue Description

**What happened?**

A relative `PAPERCLIP_RUNNER_BINARY` setting can reach local Native
admission before the binary resolver rejects it. The run has transferred
preparation ownership even though this setting cannot start the local
Runner.

**Expected behavior**

Reject this invalid setting before creating a completion contract or
Native coordinator. Keep the existing preparation owner and the run row
unchanged.

**Steps to reproduce**

Set `PAPERCLIP_RUNNER_BINARY=relative-runner`. Select a new local
`paperclip_runner` Native run. The selection must report the existing
absolute-path error before Native admission.

Refs: #19466 and #19340. Those merged changes repair Retry authority and
saved-workspace recovery. This change only moves local path validation
before new admission.

## What Changed

- Add a pure absolute-path check beside the existing binary resolver.
Both use the actual controller environment setting.
- Run that check before a new local Runner admission. Skip remote
execution, API-only providers, and an already admitted Native run.
- Test the rejection, unchanged preparation ownership, controller
setting selection, remote behavior, and immutable recovery. Keep the
existing resolver checks.

## Verification

- The full binary test file passed: 32 tests.
- The full runtime-selection file passed against a dedicated PostgreSQL
database: 62 tests. It exercises real admission transactions, contracts,
coordinator rows, and instruction-copy controls. It replaces context
materialization and remote workspace synchronization at their existing
test boundaries.
- After normal integration with current master, the normal Shared/SDK,
database, eval-kernel and Runner builds passed, together with all seven
normal application TypeScript programs. The original
recovery-preparation case passed again on this integrated revision
against a fresh dedicated database (1 passed; 61 filtered). The earlier
full 32-test binary and 62-test selection results carry only where their
source and executed boundaries are unchanged.
- The first private selection wrapper failed during module resolution,
before any case or database use. A TypeScript wrapper with the same body
recovered that setup failure. This was not a product regression.
- Reproduce with `pnpm --filter @paperclipai/server exec vitest run
src/services/native-runtime/native-codex-runner.test.ts --maxWorkers=1
--no-file-parallelism`.
- Set `PAPERCLIP_RUNTIME_SELECTION_TEST_DATABASE_URL` to a dedicated
disposable test database before running `pnpm --filter
@paperclipai/server exec vitest run
src/services/heartbeat/runtime-selection.test.ts --maxWorkers=1
--no-file-parallelism`.

## Risks

- This check validates path syntax only. An absolute path can still fail
the existing file-access or artifact checks later.
- A wrong branch condition could reject remote or existing Native work.
The tests cover these exclusions.
- This does not prove that a real provider can start or that a deployed
service is healthy. It changes no database schema or retry authority.

## Model Used

OpenAI `gpt-6.1-sol` through Codex, with source inspection and
code-execution tools. The host does not expose the context-window size.

## Checklist

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

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-07 01:59:09 -06:00
Devin FoleyandPaperclip 2d4f80504e fix(runner): retain exact ACPX checkpoints after rejected turn starts (#19762)
Preserve the exact idle ACPX conversation when a matching turn.start
response rejects work with retryable SESSION_RESUME_REQUIRED. Require
verified identity, no pending work, confirmed process cleanup and the
provider lifetime fence before normal drain and suspension can publish a
reusable checkpoint. Unknown acceptance and arbitrary closed states remain
closed.

The real Runner regression completes the next turn on attempts 2 and 3,
requires successful terminal status for the exact submitted turn, and
checks expectedIdentity on both resume requests. Controlled wire faults
reject failed, cancelled and missing-identity recovery.

Validation: 193 affected Rust tests on unchanged Rust source/fixtures;
134 recovery integration tests, 14 environment-gated skips; 21 Claude effort
tests; all server typecheck layers. Exact-head CI: 59 successful checks and
statuses, 6 intentional skips. Greptile Apex 5/5, no unresolved findings.
Hosted deployment and task acceptance remain separate.

Fixes: #19751
Refs: #19366
Refs: #19745

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-11-06 23:45:35 -08:00
Nicky LeachandPaperclip beb3e55072 Continue a released Runner after confirmed graceful eviction (#19760)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Core can release a completed Runner while its host retains the
session.
> - A host can later retire that Runner under its retention policy.
> - Graceful retirement saves a suspended session before physical
shutdown.
> - A cached Core owner still expects the old physical task and rejects
a new allocation.
> - This pull request retires that dead local controller after verified
stop.
> - The benefit is ordinary session continuation for Codex and qualified
Claude.

## Linked Issues or Issue Description

Refs #19611, #19683, #19196, #19733 and #19157.

After a host gracefully evicts a released task, the next run can receive
a new allocation. A cached controller rejects it with
`native_session_released_owner_scope_mismatch`. The detached-controller
path can already recover the suspended session.

## What Changed

- Require the exact source, old task and current account/workspace scope
before releasing a cached controller.
- Use the existing physical-stop receipt checks. Detach only the local
controller, then use the same suspended-state admission as the detached
path.
- Preserve the current run's instruction reservation. Retain a failed
detach for cleanup.
- Add actual Core, PostgreSQL and native Runner tests for Codex and
qualified Claude through ACPX. Route them once through the native CI
lane.

## Verification

- The original real-entry Codex case failed at the cached-task scope
check after native retirement, confirmed process exit and a recorded
stop receipt.
- The current head's native CI lane passed all 16 eviction cases for
Codex and qualified Claude. These check cached and detached owners,
current instruction preparation, reused-allocation inspection, missing
stop/state, foreign identity and non-suspended state. They verify the
same provider session, two ordinary turns, one accepted source result
and no replacement submit on refusal.
- Local checks passed the composition matrix, 12 targeted executor
controls, 43 CI lane-selection checks and server TypeScript. All eight
refusal cases now assert the expected error and whether the checkpoint
request or host read was reached. This addresses the first standard
review's test finding.
- Commands: `pnpm exec vitest run
src/services/native-runtime/plugin-runner-eviction.integration.test.ts`,
focused `native-session-executor.test.ts` controls, `node
../node_modules/typescript/bin/tsc --noEmit -p tsconfig.json`, and `node
--test scripts/__tests__/run-vitest-stable-shard.test.mjs`.
- [Full
CI](https://github.com/paperclipai/paperclip/actions/runs/42349579107)
passed on head `9a25709ef43365b1f1849413b935785838eca19b`. [Standard
review](https://github.com/paperclipai/paperclip/pull/19760#issuecomment-6703629255)
is 5/5 on this head, with no unresolved findings. Whitespace and
added-line secret/PII checks passed.

## Risks

- Controlled provider processes and an authenticated local host fixture
model the Launcher edge. This does not qualify live vendor transcript
storage, mounts or a deployment.
- This covers graceful eviction before allocation or before stopped-task
inspection during `describe`. Loss after that inspection or an ambiguous
claim remains held for reconciliation. No unavailable reply proves
physical stop.
- Missing or invalid suspended state still refuses continuation. The
deterministic refusal rule from #19733 remains unchanged. Abrupt death
without a suspended checkpoint stays with the separate #19157 work.
- No new capability, mode, schema, dependency, deployment or activation
is added. The existing draft #19157 and dependent branches remain
unchanged.

## Model Used

OpenAI GPT-6.1 Sol (`gpt-6.1-sol`), high reasoning, through Codex with
repository tools and code execution. The runtime does not expose a
context-window size.

## Checklist

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

> - Paperclip is the open source app people use to manage AI agents for
work
> - Remote OpenCode runs stage a controller-owned provider pack into the
sandbox before the provider starts
> - Since #19558, an ordinary OpenCode continuation retains the pack
that an earlier release admitted for its saved provider session
> - After a controller release changes the pack, the controller no
longer ships those bytes, and the continuation must stay blocked
> - The configured-pack upload path did not detect that case: it read
the build-time archive receipt for the old digest and failed with
`runner_remote_provider_archive_invalid`, a generic, retryable error
that suggests corrupt image files
> - This pull request compares the configured pack's identity with the
selected identity before any upload and fails as
`runner_remote_provider_artifact_incompatible`
> - It also puts a fixed reason in each real archive integrity error
> - The benefit is a correct, permanent classification that matches the
bundled-image path, no wasted retries, and a diagnosable message for
real archive corruption

## Linked Issues or Issue Description

Refs #19558, #19743

**What happened?**

On a hosted deployment, two remote OpenCode runs of a review agent
failed before the provider process started. The error was
`runner_remote_provider_archive_invalid` (classified as a generic
`native_session_interrupted`). Both runs continued an issue whose
provider session was admitted by an earlier release. In both runs, the
run log shows `provider_pack.compatibility`, `provider_pack.verify`, and
`provider_pack.verify_preinstalled` fail, then
`provider_pack.archive.verify` fails after 4–6 ms. Fresh runs of the
same agent on the same controller passed pack preparation with the
current pack.

The cause is not a cache race. The archives are created at image build
time (`Dockerfile` `runner-provider-pack` stage) and the server never
rewrites them. The retained authority selects the old pack digest.
`readProviderPackLayers` then compares the receipt's
`providerPackDigest` (current release) with that old digest and throws
`runner_remote_provider_archive_invalid`. Directory staging (no archive)
instead clears the staged remote pack, uploads the different pack, and
fails only at the final verification.

**Expected behavior**

A continuation pinned to a pack that the controller does not ship fails
as `runner_remote_provider_artifact_incompatible`, as the bundled-image
path already does ("the image provider pack did not match the bundled
release identity"). It does not clear or upload anything. Real archive
corruption keeps `runner_remote_provider_archive_invalid` with a fixed
reason that the run report shows.

**Steps to reproduce**

1. Build a provider pack and its archives for digest A.
2. Call `uploadVerifiedProviderPack` with that pack and a selected
(retained) digest B.
3. Before this change: the call fails with
`runner_remote_provider_archive_invalid` (v1 and v2 archives), or
resolves after it clears and re-uploads the remote pack (directory
source).

## What Changed

- `uploadVerifiedProviderPack` takes the configured pack's
`sourceDigest`. If it is not the selected `expectedDigest`, the call
fails with `runner_remote_provider_artifact_incompatible: selected
provider pack <12 hex> is not the controller's pack <12 hex>; ...`. The
check runs before the remote clear, the archive read, the directory hash
and any upload.
- The executor passes the configured pack's identity
(`readRemoteProviderPackIdentity(root, false)`, the same reader that
selects the current pack).
- Archive integrity errors keep the
`runner_remote_provider_archive_invalid` prefix and add a fixed reason:
`receipt_unreadable`, `receipt_identity_mismatch`, `receipt_invalid`,
`layer_invalid`, `layers_unreadable`, `archive_unreadable`,
`archive_not_file`, `size_mismatch` or `sha256_mismatch`. The run report
keeps only the outer message, so the reason must be in the message.
Filesystem causes stay on `cause` and do not put private paths in the
message.
- The v1 reader now uses the shared `verifyArchive` helper.
- `doc/execution-semantics.md` documents the rule.

## Verification

- Red before the fix (production files unchanged, new tests only): `npx
vitest run src/services/native-runtime/provider-pack-archive.test.ts`
gave 5 failed / 38 passed. The v1 and v2 cases failed with exactly
`runner_remote_provider_archive_invalid`, which is the hosted error. The
directory case resolved after it cleared and uploaded the remote pack.
The reason tests got a bare `runner_remote_provider_archive_invalid`.
- Green after the fix: the same file gave 43 passed.
- `npx vitest run
src/services/native-runtime/native-session-executor.test.ts`: 1245
passed. This includes the #19558 continuation cases.
- `remote-provider-pack`, `remote-provider-pack-authority` and
`remote-provider-pack-verification` tests: 69 passed.
- `pnpm --filter @paperclipai/server typecheck`: all layers passed.
- Review follow-up (`c8156841a9`): an unreadable numbered layer now
reports `archive_unreadable`. The layered-corruption test deletes
`0.tar.zst` and checks for that reason before any transfer. The archive
file gave 43 passed.
- Hosted evidence came from read-only run, event and issue-run reads
only. No hosted retries, recovery or deploys.

## Risks

- This change does not unblock an affected session. It makes the failure
honest and permanent: `runner_remote_provider_artifact_incompatible` is
already a permanent, pre-session failure code, so the run is not retried
with the same result. To recover, the session needs its original
artifacts (the captured provider-artifact path, which #19743 repairs for
new admissions) or an authorized continuity break.
- Low risk for other paths. Fresh launches select the configured pack's
own identity, so the digests are equal. Restored provider artifacts and
compatible installed packs return before this upload path.
- Callers that match `runner_remote_provider_archive_invalid` with a
substring still match. No code compares the exact message.
- Not tested end to end against a hosted sandbox. The executor-level
wiring of `sourceDigest` has typecheck coverage but no dedicated
executor test.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic. Agentic tool use with
shell, code editing, test execution and read-only authenticated 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
- [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-07 01:10:29 -06:00
Nicky Leach 167e39c4ef fix(runner): keep warm run.attach events on ordinary accounting (#19707) 2026-11-06 23:09:57 -08:00
DottaandClaude Opus 5.5 6d112c74cf fix(native): do not charge a restart resume that never started the provider (#19747)
## 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
> - #19720 stopped charging a crash restart when the previous restart
resume made progress
> - In a crash loop (several kills within seconds), the controller can
die again before the resumed execution starts its runner
> - The next claim then charged that resume, although it used no
provider work
> - This pull request does not charge a resume that never started; the
next claim replaces it
> - The benefit is that fast restart bursts do not exhaust a healthy
run's budget, and the generation cap still bounds real loops

## Linked Issues or Issue Description

Refs #19720, #19625

**What happened?**
In a restart-chaos storm wave, 8 runs still reached
`execution_recovery_budget_exhausted`, mostly in K6 crash loops (three
kills in about 20 seconds). Example: a Codex run was claimed at
generation 9, attempt 3. The controller was killed 6 seconds later,
before the executor recorded `native.process_start_requested`. The next
claim found no progress after generation 9's position, charged, and
blocked the run at attempt 3. The recorded history of the blocked runs
shows that almost every charge after the first restart was for a resume
with no start evidence (no runner request and no runner event) before
the next claim. Two runs were blocked at generation 12, the existing
cap.

**Expected behavior**
A resume that never started the provider consumed nothing and must not
be charged. A resume that started and then made no progress must still
be charged. The generation-12 cap stays.

**Steps to reproduce**
Start a native run. Kill the server during the run. Let a restart resume
do work. Then kill the server repeatedly within seconds, before each new
controller starts the runner. After one or two such bursts the run is
blocked with `execution_recovery_budget_exhausted`.

## What Changed

- `native-restart-recovery.ts`: `restartResumeKeepsAttempt()` replaces
`previousResumeMadeProgress()`. It reads the previous claim's recorded
`progressWatermarkSeq` and `progressAttempt`
(`lastRecoveryClaimResume()`).
- New `nativeExecutionStartedSince()`: after the watermark, it looks for
`native.process_start_requested`, `native.process_identity_recorded`,
`adapter.invoke`, `tool.execution.started`, or any event with a runner
`source_event_id`. This is the same start evidence as
`hasNativeExecutionStartEvidence()`, limited to events after the
watermark.
- When the previous resume never started, the claim keeps the attempt if
the run is already at the attempt that resume was to run as. If the
executor never applied that charge (killed before its execution lease),
the claim charges once, so the run reaches that attempt and never goes
above it.
- When the previous resume started, the #19720 progress rule applies
unchanged.
- The generation-12 cap applies to both rules.
- `doc/execution-semantics.md` states the rule.

## Verification

New tests in
`server/src/services/native-runtime/native-runner-restart-recovery.integration.test.ts`
(embedded Postgres, real `claimNativeRestartRecoveries`):

- `does not charge for a resume killed before it started the provider
(after the executor took its lease)`: one progress resume, then four
crash claims with no start evidence. The attempt stays 2. A resume that
starts and fails then charges to 3. Red on master (charged to 3, then
blocked).
- `... (before the executor took its lease)`: the same with the lease
never taken. Green on master and with the fix, as a guard.
- `charges the owed attempt once when an unstarted first resume is
replaced`: the replacement claim charges once (attempt 2).
- `bounds a crash loop that never starts the provider`: blocks after the
generation cap. Red on master (blocked at the third claim).
- The #19720 tests now record `native.process_start_requested` in their
resumes, as a real resume does.
- `native-runner-restart-recovery.integration.test.ts`,
`heartbeat-native-restart-suspension.test.ts`,
`native-restart-recovery-isolation.test.ts`,
`native-remote-retry-admission.test.ts`: 231 passed, 14 skipped.
- `tsc --noEmit` for `server` has no errors in the changed files.

## Risks

- A crash that happens before the runner start, every time, gets free
resumes until generation 12. Then the budget applies. This is the same
bound as the progress rule.
- Start evidence is the executor's durable
`native.process_start_requested` record, written before any backend may
spawn a runner. If an older path launched a provider without it, that
resume would be treated as unstarted until the generation cap.

> 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-07 01:00:34 -06:00
DottaandPaperclip f673bd3cb5 fix(native): verify legacy harness filesystem independence (#19748)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native sessions retain provider checkpoints and recovery state
across turns.
> - Server replacement must preserve every remaining local data
dependency.
> - A legacy backup stamp can outlive a destroyed provider and an absent
local backup.
> - Surviving skill files can already have exact immutable package
copies.
> - Raw controller files also contain stream events that database
checkpoints omit.
> - This pull request records verified filesystem independence under the
original-volume maintenance owner.
> - Replacement startup checks durable bindings while existing resume
failures and policies remain intact.

## Linked Issues or Issue Description

Refs #19715, #19729, #19733.

**What happened?**

Startup treats every retained legacy backup stamp without a current
backup row as a local filesystem dependency. This also blocks sessions
whose provider was already destroyed and whose remaining local skill
bytes have exact durable copies.

**Expected behavior**

An authorized maintenance owner can prove that an exact retained source
has no uncovered local harness payload. Startup can use that proof after
replacement. The proof must preserve failed recovery state and must not
invent a provider backup.

**Steps to reproduce**

1. Retain a terminal native session, its checkpoint, and its legacy
backup stamp.
2. Record exact canonical destroyed-provider receipts. Keep only
admitted immutable skill files on the original host.
3. Publish the owner-verified independence receipt. The old startup
predicate still blocks; the updated predicate validates its durable
bindings.

## What Changed

- Add an internal original-volume capability. It requires a private
nonce anchor, independently reviewed attachment evidence, current
reserved runtime identity, and a witness that is at most 120 seconds
old.
- Verify retained and selected-source inputs, checkpoints, leases,
destroyed-provider receipts, released ownership, and exact immutable
skill references.
- Inspect canonical persistence roots and compare surviving skill bytes
and modes. Refuse any provider directory present outside the current
persistence profile, including an old Codex home after a
preconfigured-home switch. Unknown data, backups, history, owners, or
source changes still block publication.
- Preserve raw controller and supervisor bytes through the existing
encrypted evidence archive owner. Match recovery records to the database
checkpoint, authenticate the exact archive row, and compare the original
bytes again before publication. The archive grants no executable
authority.
- Publish one audited receipt with source locks and a final comparison.
Keep original files, stamps, warnings, statuses, and session pointers. A
late capture retains a valid published winner and refuses an invalid
receipt.
- Validate published receipts from current database bindings on
replacement hosts. Add no startup filesystem probe, object read,
provider call, or recurring worker.

## Verification

- Genuine baseline regression: restore the original startup predicate
while keeping the actual receipt owner. The positive
PostgreSQL/filesystem case fails with expected `false`, actual `true`
after valid publication. Restore the changed predicate; the case passes.
- Final rebased head `e78f4b4424`: 42 affected PostgreSQL/filesystem
cases pass. This includes 32 owner guards, three paired continuation
cases, six existing failed-source archive controls, and one existing
lease-verified backup migration control. The other 113 tests in the
selected existing files were explicitly unselected.
- Genuine controller baseline: the unchanged prior owner publishes no
raw archive and accepts missing checkpoint parts. Both new regressions
fail. The repair passes encrypted byte-for-byte controller/supervisor
restore, missing checkpoint, foreign source, active source, and
changed-after-upload controls.
- Genuine omitted-provider baseline: the unchanged owner accepts old
Codex, OpenCode, and ACPX history after a preconfigured-home switch. All
three refusal cases fail. The fix passes those cases and permits
preconfigured execution with no omitted directory.
- Genuine late-publication baseline: both concurrent winner tests fail
before the repair. The repair keeps the existing valid receipt, refuses
an invalid one, and keeps one audit event. A delayed raw upload also
retains the first authenticated archive and its receipt.
- Capability controls: 12 passed, including empty replacement refusal,
stale or future evidence, changed deployment, missing volume, path
aliases, changed anchor, and current remount identity.
- The actual server-to-Runner boundary preserves explicit recovery
policy on original and empty homes. The separate actual CREATED-sandbox
preparation callback preserves checkpoint identity and policy without a
backup. Provider I/O and final transport construction are controlled.
- Warm sessions retain the same canonical refusal on both homes, make no
backend/provider attempt, and retain the saved checkpoints.
- Server TypeScript, module boundary checks, diff checks, and a
private-data scan pass. The Runner TypeScript build was refreshed after
rebase to include two new upstream exports.
- Fresh Greptile review is 5/5 with no unresolved threads on
`e78f4b44240b0fc33c2fd7cae2d5b4a2578e3153`; check `128330103712`
completed successfully. Required CI remains pending. No live
preservation or enrollment action ran.

Focused command from `server/`:

```sh
node ../cli/node_modules/tsx/dist/cli.mjs ../packages/db/src/test-postgres-command.ts -- node ../node_modules/vitest/vitest.mjs run src/services/native-runtime/native-harness-filesystem-independence.integration.test.ts src/services/native-runtime/native-harness-filesystem-independence-continuation.integration.test.ts src/services/native-runtime/native-controller-migration.integration.test.ts src/services/native-runtime/native-harness-backup-storage.integration.test.ts --testNamePattern 'owner-verified|legacy harness continuation|preserves a failed source online|migrates a lease-verified' --hookTimeout 90000 --maxWorkers 1 --fsModuleCache
```

## Risks

- This proves that no uncovered harness payload remains local. It does
not recover provider history or certify that no other copy exists.
Existing governed-wait and warm-session failures remain.
- Unknown providers, retained owners, extra payloads, missing
references, or changed source bindings fail closed. Invalid receipts
require investigation; this change does not repair or replace them
automatically.
- The maintenance capability requires independently reviewed Railway
attachment evidence. The original anchor remains stable across normal
releases; each new staging invocation needs current deployment and mount
evidence.
- Startup validation adds bounded per-scope database reads. It pages
candidate scopes and caps each source history at 500 rows. Measure the
affected cohort's actual startup latency on the qualified serving build
before enrollment.
- Raw archives use the existing backup ledger, encryption, retention,
and restore machinery. They preserve evidence only. They do not repair
missing provider history or grant a new resume policy.
- Local tests control provider and transport boundaries. Live staging
remains a separate step after review, merge, normal delivery, and a
fresh original-volume capability.

## Model Used

OpenAI GPT-6 through Codex, with reasoning, repository tools, and local
code/test execution. The exact runtime variant and context-window limit
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-07 00:57:20 -06:00
96bc021edb fix(server): certify settled legacy processes for future retries (#19377)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Bounded retries must preserve current authority, budgets and
deadlines.
> - The tenant can wait for supported future work after the source
process and its cleanup stop.
> - Retained process identity alone cannot prove that cleanup finished.
> - This change records host-owned settlement and checks its exact
identity before a retry can cover its wait.
> - Uncertain process state, changed authority, live claims and cleanup
debt keep the tenant awake.

## Linked Issues or Issue Description

Refs #19001. Related dependencies: #19019 and #18988. Related #19340
covers explicit Native task Retry; this change covers future Legacy
process retries.

Current integrated source: `1645970a9322d1213ba25fc7b52366c285682977`,
normally merged with actual master
`131b566ca4fa866c4348739c75cfe48c805b3398`. The prerequisites above are
merged. This PR targets `master`.

**What happened?**

A future issue, company or review retry can retain its failed source
process identity. The deadline reader could not prove that host cleanup
finished. Supported waits stayed blocked. Ordinary issue retries also
needed an exact source-settlement check, and represented coalesced
requests needed exact input coverage.

**Expected behavior**

A supported future retry covers only its existing wait, under current
company and actor authority, after exact host settlement. Missing,
malformed, stale, cancelled, Native or unfinished source state stays
blocked. No retry gains new execution authority.

**Steps to reproduce**

Fail an ordinary process or fail environment preparation before adapter
entry. Let normal cleanup schedule the existing retry. Inspect its
settlement and deadline, stop and reconstruct the owner, and admit one
useful successor at the saved deadline. Hold cleanup or change authority
to verify that the reader refuses coverage.

## What Changed

- Add nullable internal `legacyProcessSettlement` state through normally
generated migration `0439_cultured_impossible_man`. No backfill or
adapter result creates the record.
- Write exact host settlement after stopped-process and cleanup checks.
Compare the current run and reject live claims or cleanup debt. Optional
persistence failure preserves normal cleanup and keeps sleep coverage
blocked.
- Add a separate preparation receipt for a claimed Legacy run that
failed before any adapter or Native dispatch. Require the same boot, run
times, preparing stage, released claim and completed cleanup. A null PID
alone proves nothing.
- Clear settlement on a new claim, process metadata or Native selection.
Omit the internal record from public serialized data.
- Check exact source lineage and current authority for ordinary issue,
company and taskless retries. Cover completed coalesced requests only
when the admitted retry represents their exact inputs.
- Add the closed `current_review_retry` wait with current reviewer,
stage and existing issue authority checks. Require a nonempty current
stage ID.
- Add actual producer, cold reconstruction, SQL negative, redaction and
optional-persistence controls.
- Repair CI fixture expectations for the additive column, startup mock,
budget owner and popup return URL. Bind the controlled Native comment
server explicitly to loopback.
- Replace temporary Slack clock logging with an exact persisted-deadline
assertion. Wait until that real deadline before the existing single
default poll. Keep both forced polls, business assertions and timing
limits.

## Verification

- On final integrated source `1645970a`, normal Shared/SDK
prerequisites, Runner TypeScript and the entire normal App layered
typecheck pass. Tracked source and lockfile remain unchanged.
- All 26 whole-file settlement, recovery-reader and bootstrap controls
pass. Seven separate real PostgreSQL producer/negative cases pass at
`cf16c6a`; each has one selected passing case and 18 filtered
registrations. Filtered registrations are not acceptance.
- Master then changed the preparation source-lock scope. Its three
intersecting cases were rerun on final `1645970a`: an actual failed
company process, an actual failed ordinary issue process, and held
preparation cleanup. Each preserves its source identity and original
deadline and admits one useful successor. Issue and preparation cases
exercise normal automatic retry; the company case uses the normal
explicit bounded-retry producer.
- The unchanged 26 controls and four other PostgreSQL cases retain their
`cf16c6a` proof after a scoped source comparison. Optional
settlement/preparation persistence failures keep certification blocked
while normal cleanup completes. An entered adapter without process
identity stays uncertified. This is scoped carry, not a claim that every
earlier case ran again on the final head.
- All recorded test owners and controlled groups closed. Final read-only
SQL audits returned no test sessions. One earlier postcase resource
barrier observed a transient unattributed database backend; that
database and original evidence remain preserved. Its later disappearance
is recovery, not an explained cause. No assertion, deadline or test body
was relaxed.
- Migration0439 was generated normally after master history. All
executable SQL and journal history are preserved; the five retained
snapshots are0435–0439. Every Legacy-owned patch and unrelated
actual-master blob/mode is preserved.
- Published exact head `1645970a9322d1213ba25fc7b52366c285682977` passes
all current required checks: 68 successful contexts and eight expected
skips after exhaustive pagination and superseded-run accounting. The
fresh complete Greptile review scores this exact head 5/5. All four
inline review threads are resolved. The latest Ponytail turn stopped
without a verdict; one normal retry was requested, with no active turn
or new finding observed. No old-head score is used. The original
old-base policy failure, its dependent aggregate failure, and the
corrected description-gate failure remain recorded. No full local
repository suite is claimed; remote typecheck, test and build gates
pass.

To repeat the real database coverage, set both
`PAPERCLIP_RECOVERY_DEADLINES_TEST_DATABASE_URL` and
`PAPERCLIP_RECOVERY_DEADLINES_TEST_ADMIN_DATABASE_URL` to the same fresh
dedicated test database and run the selected
`recovery-deadline-acceptance.test.ts` cases with one worker. An unset
URL, skipped case or filtered registration is not acceptance.

The fixtures use controlled local processes and real PostgreSQL.
Reconstruction creates new service owners inside the same test process.
They do not prove deployed Cloud alarms, physical host/database
suspension, new-host recovery or Native capacity settlement. The
original30-second retry remains below the Cloud270-second minimum wake
lead.

The merge projection also includes actual master
`c3180398faca815fed7a62674bd4b0c4e88884ae`, whose common completion path
now sends deferred/recovery notifications. Its unpublished normal merge
tree is `16b028d57150f8caa576e913f38343a365b542dd`. Normal Shared/SDK,
Runner TypeScript and the full App graph pass on that combined source.
One unchanged ordinary-issue acceptance case was rerun there: an actual
failed process retains the original automatic retry deadline, admits one
useful successor, and passes the settlement, authority and no-duplicate
assertions. It passed in 45.055 s; 18 filtered registrations are not
acceptance. All known owners and groups closed, and the independent
final database audit found zero remaining test sessions. The successful
database was removed normally. Recorded worker socket peak is
unavailable for this run; parent-process zero values are not used as
worker measurements. All 29 Legacy-owned paths and every unrelated
master blob and mode are preserved.

Two outside-diff review observations concern existing waiting-chat
compaction and cross-host Native result-lease recovery. Their files are
byte-identical to current master. This focused Legacy change does not
fix or qualify those separate paths.

## Risks

Settlement proves exact local closure. It does not cover live provider
work, Native ownership, cancellation, costs, claims, cleanup debt or
changed authority. Independent checks remain.

Ship normal migration0439 with the reader and writer. Preserve
executable history and the newest five snapshots0435–0439. Generate the
MCP child's next migration normally after this parent.

The prerequisites are merged; the final remote checks must use this
integrated exact head. This proof uses controlled local processes and
external PostgreSQL. It does not establish deployed Cloud alarm
acceptance, physical host or database suspension, cold host recovery, or
R5 Native capacity settlement. The30-second retry is below the Cloud
minimum wake lead.

This focused repair follows the roadmap's bounded recovery and Cloud
deployment work. It adds no new provider policy.

## Model Used

OpenAI `gpt-6.1-sol` assisted with readers, fixtures, source review and
verification through Codex. A coordinating GPT-6-family agent
implemented and reviewed shared lifecycle wiring and integration. Tools
included Git, native TypeScript checks and controlled tests. Exact
served revisions and context 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 and its available capability
details
- [x] I have checked ROADMAP.md and related work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have linked existing public work and described the issue
- [x] I have not included internal issue IDs or private links
- [x] My branch name describes the change
- [x] Required focused checks pass at their explicitly recorded
source/carry scopes above
- [x] I have added or updated meaningful tests
- [x] I have recorded the internal contract and migration requirements
- [x] I have documented risks and evidence limits
- [x] All required published exact-head Paperclip CI gates are green
- [x] Published exact-head Greptile is 5/5 with no open findings in this
focused diff; separate unchanged master observations are identified
above
- [x] I will address reviewer findings before requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Codex <noreply@openai.com>
2026-11-07 00:56:04 -06:00
c3180398fa fix(native): resume a remote Runner after a settled suspension
fix(native): resume a remote Runner after a settled suspension

A scheduled checkpoint retry can retain a remote reattach claim after
closing its failed predecessor cleanly suspends and exits the Runner.
Resume only that exact settled source after confirmed exit and a fresh
identity/state check. Preserve pending work and uncertain-exit fences.

Admit the last already-charged provider attempt and share one bounded
recovery deadline with the final state read, retaining closed diagnostic
labels on read failures. Cover fresh ACPX Runner relaunch on attempts
two and three through actual processes and the durable close barrier.

Six affected suites pass 1,579 tests, with 14 environment-gated skips;
all server typecheck layers pass on Node 25.6.1. The complete provider
turn continuation failure remains preserved as follow-up #19751.

Refs: #19366

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <Paperclip-Paperclip@users.noreply.github.com>
2026-11-06 22:39:40 -08:00
Nicky Leach 08b049f381 Admit qualified Claude sources through the existing Core claim (#19196) 2026-11-06 22:25:22 -08:00
DottaandClaude Opus 5.5 e620ce3678 fix(heartbeat): deliver each accepted follow-up exactly once through restarts and admission holds (#19741)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A user can comment, click Retry, or rely on automatic recovery while
an agent works on a task
> - Restart-chaos waves showed accepted follow-ups being dropped, or
delivered twice, around restarts and temporary company admission holds
(accounting still settling after a restart)
> - A dropped comment means the user's request is never acted on. A
duplicate reopens work that is already done
> - This pull request makes accepted work wait through a temporary hold
and resume when it clears. It also stops a comment from being promoted
again after a completed run already received it
> - The benefit is that each accepted follow-up is delivered exactly
once, eventually, unless the task is cancelled or a reached hard stop
applies

## Linked Issues or Issue Description

Refs #19702 (never-started tasks during a temporary hold), #19700
(bounded accounting hold), #14910 (stale reopen from a deferred wake; no
file overlap).

**What happened?**
Restart-chaos waves found these shapes:
1. **Comment wake cancelled.** A comment was saved behind a run and
promoted when the run ended. A temporary company hold refused the
unstarted queued run, and claim cancelled it terminally. The recovery
wake that followed was cancelled the same way, and the task rested
`blocked` without the comment.
2. **Dead Retry.** A Retry click during the hold stayed
`deferred_issue_execution` after the hold cleared. Nothing woke it, and
the issue held no execution lock.
3. **Refused assignment.** A new task's first wake was refused during
the hold. Nothing woke stranded-work recovery when the hold cleared, so
the task stayed `todo` with no run.
4. **Silent comment after recovery.** A comment was saved behind a run
that restart recovery finalized. A company-scoped hint waited out the 30
s deferred-receipt rescan, so the task looked done with the message
unread.
5. **Comment delivered twice.** A run whose provider process was lost
had a saved comment. Its retry started after the comment, received it in
its prompt (task history), did the work, and finished the task. The
saved wake was then promoted anyway, which reopened the done task.

**Expected behavior**
An accepted follow-up (comment, Retry, recovery) is delivered exactly
once, eventually, unless the task is cancelled. A temporary refusal
never ends in a terminal cancel, skip, or block. Reaching a hard stop
remains the board's decision.

## What Changed

- `heartbeat/queue.ts`: on a budget block at claim, an unstarted queued
run stays queued when the block is temporary. A reached hard stop or
blocked unpriced usage still cancels it.
- `recovery/service.ts`: stranded-work recovery waits out a temporary
hold for any task. It does not escalate to `blocked`. This extends
#19702 beyond never-started tasks.
- `budget-block-ownership.ts`: a shared `isBoardOwnedBudgetBlock`, moved
out of the recovery service. `budgets.ts` is unchanged.
- `run-cost-accounting.ts`: settling a run's accounting wakes queued-run
dispatch, the deferred-receipt worker, and stranded-work recovery. Held
work waits passively; this is what resumes it.
- `deferred-execution-notifications.ts`: new
`notifyDeferredWakesForIssue`. `native-run-projection.ts` (a run
reaching a terminal status) and `environments.ts` (lease release) now
hint each saved wake of the issue by id, so delivery does not wait out
the company rescan interval.
- `wake-queue/adapters/postgres.ts`: release treats a saved comment as
delivered only when a succeeded run of the wake's agent on that issue
started after the comment's latest revision, and either received the
comment as its own wake input (`wakeCommentIds`) or was a retry of the
interrupted turn whose task history included it. In that case it cancels
the saved wake with "Queued messages were already delivered to a
completed run" and leaves the task as it is. Task history alone never
consumes a message saved for its own turn. One query checks the whole
batch.
- `run-cost-accounting.ts`: settling late receipt debt also sends the
hold-release notifications.
- `doc/execution-semantics.md`: documents the rule.

## Verification

Red on `master` and green with this change, on embedded Postgres:
- `server/src/__tests__/heartbeat-held-comment-wake.test.ts`:
- A promoted comment refused by the hold stays pending, with no
cancelled run and no `blocked` task. It is delivered once after the hold
clears through real `accountRunCost`.
- A Retry clicked during the hold starts exactly once after accounting
settles, which wakes its owner.
- A new task whose assignment was refused during the hold starts once
accounting settles, which wakes stranded-work recovery.
- A comment that a later succeeded retry already received as history, or
that a later run received as wake input, is consumed without reopening
the task.
- Guard tests (green on both):
- A comment is still delivered in each of these cases: the later run
never received it, it was edited after that run started, it was posted
while the run was already running, or a non-retry run only saw it as
history.
  - A task whose continuation was refused waits out the hold.
  - A reached hard stop is still refused.
- `server/src/__tests__/native-run-projection-work.test.ts`: projecting
a recovered run terminal hints the saved comment's own wake (red on
`master`).
- Related suites pass locally (14 files, 482 tests): wake-queue, queued
comments routes, refused assignment wake, native run projection.
- Live replays (local, unmasked, kept instances):
- `--replay w-20261107T031005-s7501/t04` (Claude C1, K1): PASS. The
comment's run was created at 03:56:24 during the hold, stayed queued,
started at 04:06:24 when the hold cleared, and succeeded. It is the only
run carrying the comment.
- `--replay w-20261107T002911-s7401/t01` (before the per-wake hint): it
reproduced shape 4. Run 592d772f was killed 250 ms after
`run.result.accepted`, released and finalized at restart, and the task
rested `done` with the comment still saved. Calling the existing sweep
on the kept database promoted it, which confirmed that the missing piece
was the trigger.

## Risks

- A queued run can now sit queued for the length of a temporary hold,
which #19700 bounds. It does not hold the issue's execution lock, so
later comments still coalesce into it.
- The delivered-comment rule depends on the run's recorded prompt
context. A succeeded run that received the comment in its history counts
as delivery, even if the agent did not act on it. This matches
exactly-once delivery semantics.
- Settling accounting now emits three extra notifications per run. They
are coalesced per company.

## 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 replays).

## 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-07 00:24:09 -06:00
DottaandPaperclip 18b6b71d83 fix(native): refresh per-turn projections from the selected checkpoint (#19744)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Per-turn native sessions save controller checkpoints and suspended
Runner backups under separate owners.
> - A local controller projection can still name the first turn after
the next turn has saved its checkpoint and backup.
> - The old per-turn restore path selected that stale local reference
instead of the newer canonical source.
> - Admission then saw different turn identities and archived the valid
backup with the old projection.
> - This change uses the canonical selected checkpoint and the existing
finished-owner proof before it replaces a stale projection.
> - The benefit is that a third turn keeps the valid saved conversation
and backup.

## Linked Issues or Issue Description

**What happened?**

After two successful per-turn remote runs, a third turn could fail
before provider execution with unavailable provider resume state. The
selected database checkpoint and suspended backup belonged to the second
run. The local controller projection still belonged to the first run.

**Expected behavior**

The selected checkpoint must govern controller rehydration. Preserve the
older projection and the selected run's backup. Do not overwrite
current, newer, foreign, changed or unsettled ownership evidence.

**Steps to reproduce**

1. Save two real controller checkpoints for one per-turn conversation
and finish both owners.
2. Leave the first checkpoint's readable projection or external marker
beside the second run's suspended backup.
3. Admit the third run through the real canonical selector and backend
owner.
4. Before this change, admission archives the active backup with the
stale projection and leaves no active controller state.

Related: #18018 added per-turn controller restoration. #18718 refreshed
warm projections. #18846 added finished-owner retirement. This change
keeps the selected per-turn source from entering that retirement path
because of an older projection. The open warm-only change #18436
concerns a different restart branch.

## What Changed

- Resolve the canonical source before refreshing a prior per-turn
controller projection.
- Re-read it under the existing sorted coordinator locks. Replacing an
older owner reuses finished-owner checks for both runs and verifies
their full session scope. A same-source marker only rehydrates its exact
selected identity before the normal retained-sandbox verification.
- Recheck the exact initial local bytes after asynchronous reads and
lock waits. Preserve the old control-plane directory in the existing
archive, while keeping the newer backup and provider files active.
- Restore a current-run external marker only from its own checkpoint
before the existing bootstrap retry proof. A missing current checkpoint
cannot select a previous turn. Retain readable current-run, legacy
file-backed, backup integrity and provider resume checks.
- Stage the existing OpenCode proxy bytes into the controlled
fake-sandbox filesystem in the real ingress-retry fixture. Keep remote
artifact path and digest checks unchanged.
- Add real PostgreSQL/filesystem coverage for Codex and ACPX, readable
projections and external markers, missing or corrupt parts, held owners,
foreign scope, newer history and a current owner arriving during a real
coordinator-lock wait.

## Verification

- Baseline RED: the two new readable-projection cases failed against
unchanged production code because active control-plane state was absent
after admission. The real selector selected the second run. Setup
failures were separate and are not counted as RED.
- Exact final existing per-turn restore, active-owner refresh and
retained-owner wait controls: 19/19 passed in 5.91s.
- Exact final local source (`fd8d9d9021`, based on `131b566ca4`): 58/58
real PostgreSQL/filesystem and native Runner tests passed in 31.58s.
This includes both existing ingress bootstrap retries through the real
Runner and fake Codex/OpenCode providers, plus 56 focused owner cases.
Cargo validated and rebuilt current source from a private APFS clone of
an unqualified local cache; the original cache was untouched. A private
local configuration resolves workspace DB, Shared and Runner sources
without replacing behavior. The local CLI allowed a 90s setup hook; no
default timeout changed.
- Exact final full server and test-tree TypeScript check with Runner
source paths: passed. The stock fresh-worktree check first failed on
unbuilt Runner declaration entries; no product types were changed to
hide that setup gap.
- Exact final module-boundary and diff-whitespace checks: passed.
- Native-server CI RED on the previous head: both real ingress retry
cases failed because a current-run marker was returned without
hydration. The aggregate gate failed from that one underlying lane. The
correction preserves the own-checkpoint bootstrap path. The first local
OpenCode attempt hit a controller-host fixture path guard before its
intended failure; staging the same proxy bytes into the fake sandbox
made both native cases pass.
- Review regression RED: the real retained-sandbox fixture failed at
`sandbox_resumed` on the first PR head. GREEN: the corrected owner
hydrates the same-source marker and reaches the existing exact
retained-lease/suspended-runner read. A mismatched item identity still
refuses before any remote read.
- Local environment: macOS arm64, Node 26.4.0, PostgreSQL 18.1. Provider
execution is deliberately outside this admission fixture. The real
three-turn workflow still needs verification on the normally deployed
fixed source.

## Risks

- Refresh requires settled, committed, released and stopped or fenced
owners. Missing or changed evidence remains blocked.
- The refresh holds existing coordinator locks while it validates
bounded controller state. It performs no provider calls.
- The old control-plane bytes remain in the archive. A restore write
failure still fails admission; it does not reset provider state.
- Final-head full CI is green, including typecheck, build, Rust,
native-server, server and browser suites; Greptile is 5/5 with no
unresolved threads. Hosted deployment and continuity verification remain
separate acceptance steps. A successful replacement conversation is not
proof of provider continuity; hosted continuity acceptance is still
pending.

## Model Used

OpenAI Codex, GPT-6 family, with code execution and repository tools.
The host does not expose the exact 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-07 00:14:17 -06:00
DottaandClaude Opus 5.5 24cbea136f fix(native): export OpenCode launch artifacts through a confined workspace copy (#19743)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native recovery saves the qualified OpenCode launch files so a cold
allocation can resume the same session (#19622)
> - Core reads those files from the sandbox with the provider's bulk
`syncOut`
> - The launch root is under the sandbox account home, not under the
workspace remote dir
> - Daytona, Kubernetes, and CreateOS confine every export source to the
workspace remote dir, so the export always fails
> - Every remote OpenCode run on a durable-storage instance now fails
40-90 s after start with `native_session_interrupted`
> - This pull request exports a confined scratch copy of the three
launch files from inside the workspace runtime dir
> - The benefit is that remote OpenCode runs work again, and the
provider confinement guard stays unchanged

## Linked Issues or Issue Description

Refs #19622

**What happened?**

After #19622, every remote OpenCode run on Daytona fails at provider
admission. The run error is `Daytona sync source path escapes the
workspace remote dir:
/home/daytona/.paperclip-runtime/paperclip-runner/provider-artifacts/<digest>`.
The error code is `native_session_interrupted`. The last successful run
of the affected agent was immediately before #19622 reached the
deployment.

**Expected behavior**

Core saves the qualified launch files and the run continues. The
provider confinement guard still refuses any export source outside the
workspace remote dir.

**Steps to reproduce**

1. Configure an OpenCode agent on a Daytona environment with durable
harness storage.
2. Start a run.
3. The run fails when Core captures the provider artifacts, before the
first turn.

## What Changed

- `captureRemoteNativeProviderArtifacts` now takes the workspace
`remoteCwd`.
- Before the export, a sandbox command copies the three launch files
into a new scratch directory at
`<remoteCwd>/.paperclip-runtime/paperclip-runner/provider-artifact-export/<uuid>/`.
The command refuses a symlink or a non-regular file. It uses `cp -P`, so
it never follows a link.
- `syncOut` reads from that scratch copy. A `finally` step removes the
scratch copy after success or failure. A failed removal is logged and
does not replace the transfer result.
- The launch root does not move. OpenCode still binds to the same
absolute paths. Core still verifies each downloaded byte against the
manifest SHA-256 values.
- `.paperclip-runtime` is already excluded from workspace sync, so the
scratch copy never enters the user workspace.
- The Daytona `assertConfinedSandboxPath` guard is not changed.

## Verification

- New regression
`server/src/services/native-runtime/native-provider-artifact-capture-confinement.test.ts`.
It runs the real sandbox commands on a local directory tree that has a
home-rooted launch root and a separate workspace. Its `syncOut` applies
the same workspace confinement rule as the Daytona provider.
- Before the fix: it fails with `Daytona sync source path escapes the
workspace remote dir:
.../home/daytona/.paperclip-runtime/paperclip-runner/provider-artifacts/dfe9e4b1d660/bin/nodeCommand`.
This is the staging error.
- After the fix: it passes. The published bytes match, every export
source is under the workspace runtime dir, and the scratch directory is
empty after the capture.
- Safety cases: a failed transfer removes the scratch copy and does not
publish. A launch file replaced by a symlink is refused before any
export, and nothing is published.
- `native-session-executor.test.ts`: the durable OpenCode admission case
now asserts that the capture receives `remoteCwd` and that every
`syncOut` source is under `/workspace/`.
- `pnpm vitest run` for the capture, confinement, remote OpenCode
artifact, and session root entries tests: 14 passed.
- `vitest run native-session-executor.test.ts -t "opencode|provider
artifact|durable"`: 62 passed.
- `pnpm --filter @paperclipai/server typecheck`: passed.
- Not run: a live Daytona run with this revision. The fix is not
deployed to staging.

## Risks

- Low risk. The capture now runs two short `sh` commands in the sandbox
and holds a second copy of the launch files during the transfer. The
files are bounded by the existing 512 MiB limit and are deleted
afterwards.
- The scratch copy uses `cp -P`, `mkdir`, and `rm -rf` from POSIX `sh`.
The Daytona image already uses these tools for the workspace sync.
- If the sandbox stops between the copy and the removal, a scratch
directory can remain under `.paperclip-runtime`. Workspace sync excludes
that directory.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code with tool use (shell, file edit, and read-only browser access to
staging).

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 00:04:21 -06:00
DottaandClaude Opus 5.5 9f0ebf6f70 fix(accounting): close a run whose turn was lost on restore, and stop succeeded runs holding all work (#19724)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Budgets hold new work while the cost of completed runs is unknown
> - In a restart chaos wave, a native ACPX Claude run crossed a server
kill, resumed, and succeeded. Its usage receipt never became complete.
> - For the 10-minute closure bound, admission refused all work in the
company. This included tasks that never crossed a fault.
> - This pull request fixes the receipt for a turn that was lost on
restore. It also stops a succeeded run from holding all new work.
> - The benefit: one late usage report no longer freezes a company, and
hard stops still apply.

## Linked Issues or Issue Description

Refs #19700 (the accounting closure bound that this follows).

**What happened?**
A native ACPX Claude run crossed a server SIGKILL in the middle of a
turn. The Runner restored the session, and the run succeeded 54 seconds
after it started. All 17 usage receipts stayed incomplete. Accounting
settled the run only at the 10-minute closure bound. During those 10
minutes, admission refused all company work with "Company cannot start
work while completed runs await accounting".

**Root cause**
ACPX reports provisional usage (`runDeltaComplete: false`) until a turn
ends. The kill lost the turn that was active, so that turn never got a
complete report. The restored Runner failed that turn with
`provider_turn_lost_on_restore`. Native turn accounting
(`createNativeTurnAccounting`) required every terminal turn to have
usage, so this turn kept every later receipt incomplete. This was true
even after the continuation turn completed with full usage. The
admission gate then counted the succeeded run as a pending terminal run
for the whole company.

**Expected behavior**
The receipt closes when the run's turns end. A turn that was lost on
restore makes the usage a lower bound. A succeeded run with only usage
outstanding does not hold other work.

## What Changed

- `native-usage-accounting.ts`: a turn that failed with
`provider_turn_lost_on_restore` and has no complete usage no longer
holds the receipt open. The snapshot marks `accountingUsageLost`.
- `native-session-executor.ts` and `native-usage-delivery.ts`: such a
receipt carries the provenance note "a provider turn was lost on restore
and never reported its usage; observed usage is a lower bound".
- `anthropic-pricing.ts` and `codex-pricing.ts`: the rate-card pricers
keep a producer's provenance note.
- `native-usage-delivery.ts` and `usage-receipts.ts`: remote producer
delivery keeps a `lostOnRestore` marker on the receipt's producer event,
so replay also excuses the lost turn. Receipts saved without the marker
still replay.
- `budget-exposure.ts` and `budgets.ts`: admission waits only for
pending runs that ended without succeeding (`waitingRunCount`). A
succeeded run whose provider reported observed usage is bounded like a
running run: its observed cost counts toward spend and reservation
capacity, and its settled charge can still reach the hard stop.
- `budget-exposure.ts`: a closure that timed out because of a
control-plane restart keeps the unpriced-usage exemptions, whatever its
token count. Restart evidence is a replaced receipt source, a native
recovery transition that is not an ordinary same-run retry, or a turn
lost on restore. A second chaos run showed the need. It recovered and
succeeded, but its final usage report was lost with the restart, so it
settled as unpriced 0 cents. The blocking policy then opened a hard
incident and paused the company. A timed-out closure with no restart
evidence still counts.
- `run-cost-accounting.ts`: a lost receipt that carries a positive
provider-reported cost settles at that cost as an estimate marked as a
lower bound, not as unpriced.
- `paperclip-runner-tool-authority.ts` and `budgets.ts`: a running run's
own tool mutations are evaluated as admitted work. Hard stops and pauses
still refuse them. Accounting holds, recovering runs and budget inputs
that await evaluation do not. In a third chaos run, a live run's write
to its task document was refused during a temporary hold, so the run
proposed a blocked result.
- `doc/SPEC-implementation.md`: documents these rules.

Policy choice: a succeeded run's provider work is over, so it has less
unknown spend than a running run. Running runs never held admission.
Failed, cancelled, timed-out and interrupted runs still hold new work,
because their provider may not have stopped. A per-agent gate would
still freeze unrelated work of the same agent. A per-reservation gate
does not help when `reservationCents` is 0, which was the case in this
wave.

## Verification

- Live: a restart chaos wave ran master plus this PR (c2a48fb1e0) with
no masking. It passed 6/6, and every task crossed a restart. There were
0 accounting holds, 0 refused agent writes, 0 company pauses, and every
cost settled as estimated.

- Red then green: `native-usage-accounting.test.ts`, "closes a run whose
earlier turn was lost on restore, as a lower bound". It uses the event
sequence of the chaos run.
- Red then green: `closed-native-receipt-accounting.test.ts`, "does not
hold other work for a succeeded run with only usage outstanding". It
also checks that the hard stop still applies.
- I changed "neither pauses nor holds admission while a succeeded run's
charge commits" to the new policy. A failed run in the same state still
holds new work.
- Red then green: `closed-native-receipt-accounting.test.ts`, "never
hard-stops a blocking policy for a restart-derived timed-out receipt".
It uses the zero-token row shape of the chaos run and a case with
unpriced tokens. Without the fix, the company was paused.
- Red then green: `runner-api.integration.test.ts`, "lets a running run
write during a temporary accounting hold, but not after a hard stop".
- Red then green: `closed-native-receipt-accounting.test.ts`, "does not
hold other work for the s7504 succeeded row shape".
- Red then green: `closed-native-receipt-accounting.test.ts`, "settles a
timed-out restart receipt at its provider-reported cost, without holding
or pausing". It uses the OpenCode/OpenRouter row shape from a fourth
chaos run.
- `anthropic-pricing.test.ts`: the rate-card provenance keeps the
lower-bound note.
- Local: `closed-native-receipt-accounting`, the budget suites,
`native-usage-accounting`, `native-usage-delivery.integration`,
`native-runner-restart-recovery.integration` and `anthropic-pricing` all
pass (187 tests). Server `tsc --noEmit` is clean. `pnpm verify` asks for
CI coverage because the spec changed.

## Risks

- A succeeded run whose final charge is higher than its observed cost
can let one more task start before the charge commits. The hard stop
then pauses the scope as it does for a running run that overruns.
- A run that includes a lost turn settles with a lower-bound usage
figure. The provenance says so, and a later receipt still reconciles
through the late-receipt path.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 23:57:48 -06:00
DottaandClaude Opus 5.5 464e6e16e0 fix(native): resume copyback after a restart kills its same-host owner; one board action otherwise (#19717)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs save the agent's workspace files after the provider turn
finishes (copyback)
> - One controller owns copyback at a time. The run records that owner
in `runnerProfileJson.nativeWorkspaceFinalizationOwner` (host, PID,
token)
> - A control-plane restart can kill the controller during copyback. The
dead controller can never confirm its own stop
> - Recovery then waited for a board confirmation. No surface offered
that confirmation, so the run stayed `running` and the task stayed
`in_progress` with no visible action
> - This pull request lets the next controller on the same host prove
the stop by itself. It also gives the board one clear action when that
proof is not possible
> - The benefit is that a restart during copyback resumes by itself, and
a task never waits silently for a person

## Linked Issues or Issue Description

**What happened?**
In a Daytona restart-chaos wave, a kill during workspace copyback left
the run `running` and its task `in_progress`. Recovery reported
`native_workspace_finalization_owner_unverified`. The run was still
`running` when the wave timed out.

**Steps to reproduce**
1. Start a native remote (sandbox) run.
2. Kill the control-plane process (SIGKILL) after the provider result is
accepted, during copyback.
3. Restart the control plane on the same host.

**Expected behavior**
If the previous controller and its copyback children are proven stopped
on this host, recovery resumes copyback without a new agent turn.
Otherwise, the task shows one board action to confirm that the previous
server stopped.

**Actual behavior**
The recorded owner was cleared only by
`resumeNativeWorkspaceAfterOwnerStop`. Same-host recovery also required
a completed-copyback barrier, which an interrupted copyback never has.
The task presentation showed `working`, and the UI had no control for
the confirmation.

Refs #19578 (boot-ID fencing for unstarted runs; same proof style).

## What Changed

- Root cause:
`server/src/services/native-runtime/native-workspace-finalization-ownership.ts`
sent a dead same-host owner to `requireStopVerification` when no
`workspace_finalize` operation had succeeded. That case is exactly a
restart during copyback.
- New `native-workspace-owner-stop-proof.ts`. The owner receipt now
records the controller's process group (`processGroupId`). Copyback
children (tar, Git) are not detached, so they keep that group. A
successor on the same host releases the receipt only when the receipt
names another controller boot, the PID is dead or recycled (process
start time), and the group is empty or has no member outside the
successor's own process tree that started after the old controller. Age
alone never proves a stop.
- A still-live group member returns `local_stop_pending`. The
finalization worker rechecks it on its existing ownership recheck
interval. The board action stays visible meanwhile.
- A successful local proof writes `issue.workspace_owner_stop_verified`
with the proof and marks the recovery `resuming`, like the existing
platform proof.
- A receipt from another host, a receipt without a process group, or a
host without `ps` keeps the fence. The task interruption is now
`waiting_for_user` with `userAction: "confirm_owner_stop"` (new shared
enum value), not `working`.
- UI: the task chat shows one **Confirm the previous server stopped**
action. It asks for stop evidence and calls the existing
`resume-workspace-finalization` API. It is shown only to roles that hold
`runtime:confirm_owner_stop` (company owner or administrator, local
board, instance administrator). The inbox names the same action.
- The copyback advisory lock, the coordinator finalization lease, and
remote save fencing are unchanged.
- Docs: `doc/native-workspace-finalization-recovery.md`.

## Verification

- Red then green on embedded Postgres
(`server/src/services/native-runtime/native-workspace-finalizer.test.ts`):
- same-host controller killed with SIGKILL, its process group empty:
before `owner_unverified`, after `acquired` with the proof logged and no
agent wake
- a non-detached orphan child of the dead controller still runs:
`local_stop_pending`, no copyback; after the child exits, `acquired`
  - a receipt without a process group: still `owner_unverified`
- a foreign-host owner: the task presentation was `working` (red), now
`waiting_for_user` / `confirm_owner_stop`; after the board confirmation
it is `working` again
- The existing orphan test (a detached child) and all other finalizer
tests pass: `npx vitest run
src/services/native-runtime/native-workspace-finalizer.test.ts
src/__tests__/native-workspace-finalization-work.test.ts
src/services/task-interruption-presentation.test.ts` (135 passed).
- UI: `npx vitest run src/components/WorkspaceExportRecovery` (34
passed), including the action call body and the non-administrator case.
- `pnpm check:token-gates` clean. Server and UI typecheck clean for the
changed files.
- Live Daytona restart-chaos wave: results are added in a comment below.

## Risks

- A process-table check that reports a member wrongly as live only keeps
the existing fence. The board action is still available.
- A subreaper that adopts orphans does not change their process group,
so the proof does not depend on the parent PID.
- A child that a future change spawns with `detached: true` would leave
the group. Copyback paths do not do that today. The existing
detached-orphan test still fails closed, because a receipt without a
process group is not released.
- Old receipts have no process group. They keep the board action.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 23:31:07 -06:00
DottaandPaperclip 7a72caeda1 perf(workspaces): batch online companion discovery (#19735)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Online workspace migration checks all local aliases before it
captures a source.
> - Each alias must retain its prepared runs and recovery holds in that
check.
> - The current reader scans the same company run table once for every
alias.
> - Shared Git sources repeat that scan even after the complete alias
group is known.
> - This pull request reads those narrow companion rows once per
inspection.
> - The existing owners still validate every source and recovery hold
before migration can proceed.

## Linked Issues or Issue Description

**What happened?**

Online source qualification repeats the prepared and finalized run query
for each workspace alias. A shared Git source with four aliases makes
four copies of this query.

**Expected behavior**

One inspection should read the complete alias group once. It must return
the same source and recovery proofs, and a later inspection must read
fresh rows.

**Steps to reproduce**

1. Create a shared Git source with four aliases and a valid settled
prepared run.
2. Add a finalized companion and a recovery hold. Add a foreign-company
run that names the same workspace ID.
3. Inspect the source and count companion discovery queries. Before this
change, the regression sees four queries instead of one.

Related public work: [source ownership guards in
#19688](https://github.com/paperclipai/paperclip/pull/19688) and
[bounded accounting closure in
#19700](https://github.com/paperclipai/paperclip/pull/19700).

## What Changed

- Read prepared and finalized companion rows in one company-scoped query
after the complete alias group and foreign aliases pass validation.
- Group the narrow rows by workspace ID. Keep run order and all existing
source and recovery owner checks.
- Add a PostgreSQL regression for proof equivalence, one scan, company
isolation, and a new source obligation between inspections.
- Align an existing Stop-accounting test with the durable closure
deadline from #19700. Keep the incomplete debt, quiet database,
cancellation, and single-charge checks. Product workers and timers are
unchanged.

## Verification

- Before the source change, the new PostgreSQL test passed its proof
comparisons and failed because four companion scans ran instead of one.
- The two workspace suites pass: 77 tests in
`remote-workspace-online-source.test.ts` and
`online-legacy-workspace-recovery.test.ts`.
- CI exposed an old Stop-accounting assertion that expected no timer
while an incomplete receipt had an intentional closure deadline. Both
automatic CI attempts failed. The unchanged local variants passed, so
that run did not reproduce the failure. Waiting for the real owner
deadline then proved the stale assertion red.
- After the test alignment, both Stop-accounting variants and all 77
workspace tests passed together: 79 tests across three files. The three
unrelated Stop-recovery cases were not selected locally. Fresh CI covers
the complete suites.
- The affected suites cover retained recovery owners, foreign aliases,
physical source locks, and changes before publication.
- Tests use a disposable PostgreSQL 18.1 database. The local hook
allowance is 90 seconds. Product timeout settings are unchanged.
- Final head `0eed2e5062cbfb62df7f76168c27d2172fc2d7de`: [full
CI](https://github.com/paperclipai/paperclip/actions/runs/42341640259)
passed, with 56 successful checks and four configured skips. [Fresh
Greptile
review](https://github.com/paperclipai/paperclip/pull/19735#issuecomment-6702476739)
is 5/5 with no open threads. This PR makes no claim about deployed
latency.

## Risks

- Each inspection now holds the narrow companion rows for the complete
alias group in memory. The existing alias group bound still applies.
- The query keeps the same company, workspace, and sync-state
predicates. There is no runtime filter, row limit, schema change, or
cache between inspections.
- No public command or API changes. Public documentation does not need
an update.

## Model Used

OpenAI Codex, exact model ID `gpt-6.1-sol`. Used reasoning, code
editing, shell tools, and test execution. The context window size is not
exposed by this environment.

## 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 23:26:53 -06:00
DottaandClaude Opus 5.5 fc7e3a6503 fix(recovery): never replace a native session after a deterministic refusal (#19733)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native agent runs keep a saved provider session, and same-run
recovery resumes it after a failure
> - When resume fails and the checkpoint allows it, recovery archives
the saved continuity state and opens a fresh replacement session
> - Recovery used that path for every resume failure, including
deterministic refusals that left the saved state unchanged and repeat on
every retry
> - In five staging tasks a refusal (a protected launch argument, or a
predecessor tool call with no result) escalated into a replacement. The
remote archive move failed part way, the next attempt found no runner
state, and each task was blocked for workspace repair
> - This pull request stops before replacement after a closed-class
refusal, keeps the saved state, and hands the run to an operator with a
named reason
> - The benefit is that a deterministic refusal can no longer damage
saved state or open a replacement over an unsettled external action

## Linked Issues or Issue Description

Refs #19659, Refs #19516, Refs #19334

**What happened?**
Five Codex tasks on Daytona sandboxes failed on staging. The first
attempt failed with a deterministic refusal:
- `run.attach cannot change protected launch arguments` (four tasks,
fixed separately in #19659), or
- `native_runner_source_semantic_reconciliation_required` with a
predecessor `paperclip_call_assigned_tool` call still pending (one
task).

The same-run retry then opened a replacement session.
`archiveContinuityState` failed with
`runner_continuity_break_archive_failed` and no detail. The third
attempt failed with `runner_remote_state_unavailable: exit=1`, and the
task was blocked with "Workspace repair required".

**Steps to reproduce**
1. Save a native session whose checkpoint policy is
`allow_replacement_after_resume_failure`.
2. Make the resume fail with a refusal that does not change state (an
attach refusal, or a pending tool call).
3. Same-run recovery opens a replacement session and archives the saved
continuity state.

**Expected behavior**
A deterministic refusal stops the run for an operator. The saved state
stays intact. A run with a pending side-effecting call never reaches
replacement. Transport drops keep the existing retry behavior.

**Root cause**
`executeNativeSession` checked only the checkpoint's replacement policy.
The closed failure class (#19334) only had an effect when replacement
was forbidden. Attach refusals and pending tool calls had no class at
all, and the source-reconciliation refusal was a plain `Error`.

## What Changed

- Runner (`provider_backend.rs`, `durable/mod.rs`): `run.attach`
refusals that would change the durable Codex profile or a protected
launch argument carry the closed classification
`provider_attach_refused`. An attach blocked by
`durable_pending_tool_calls` carries `provider_side_effect_pending`.
These refusals already leave the saved state unchanged.
- Contracts (`harness-driver.ts`): new closed classes
`PROVIDER_ATTACH_REFUSED` and `PROVIDER_SIDE_EFFECT_PENDING`. A typed
`NativeRunnerSourceReconciliationRequiredError` replaces the plain
`Error` at the five `native_runner_source_*_reconciliation_required`
throw sites. `runnerRecoveryFailureClass` reads the authenticated
command-result classification or the typed error, never message text.
- Runtime (`native-session-runtime.ts`): after either class, recovery
never opens a replacement session, whatever the checkpoint policy.
- An attach refusal ends with the existing
`NativeProviderStateIncompatibleError`, which maps to the existing
operator path `native_provider_state_incompatible`.
- A pending side effect ends with the new
`NativeProviderSideEffectPendingError`.
- Server (`native-session-executor.ts`, `native-session-resume.ts`,
shared `EXECUTION_RECONCILIATION_CAUSES`):
`native_provider_side_effect_pending` is a permanent failure and an
execution-reconciliation cause, so the board gets a reconciliation
(record whether the action ran) instead of a generic Retry. The
reconciled continuation does not rebind the saved session, which still
refuses attachment. It starts a fresh provider session in the same
workspace. The saved session and workspace are unchanged.
- Archive (`archiveContinuityState`): a failed remote move now records
the bounded (512 characters), redacted exit status and output in
`runner_continuity_break_archive_failed: exit=… timedOut=… <output>`.
The remote move already ran before the local move on `master`, so a
failed remote move leaves the local state in place.
`restart-interrupted-turn.ts` accepts the detailed message for its
reattachment-loss retry.
- Docs: `doc/execution-semantics.md` and
`packages/paperclip-runner/docs/durable-recovery.md` describe the new
rule.

## Verification

Red before the fix, green after (each test first run against unchanged
source from `origin/master` 644ee66f5d):

- `native-session-runtime.test.ts`, test "never replaces a session after
a deterministic, non-destructive recovery refusal". Red: replacement was
attempted (`TypeError: Invalid value used as weak map key` from the stub
replacement). Green: typed error, and `openReplacementSession`,
`openSession` and `openRun` are not called.
- `harness-driver-backend.test.ts`, test "keeps the runner's closed
class of a failed recovery". Red: the typed error did not exist. Green:
both new classes are read from the command result and from the typed
error. The same message text without the type stays unclassified.
- `tests/codex_provider.rs` (real durable executor and fake Codex
app-server):
-
`attach_with_a_pending_tool_call_is_classified_as_a_pending_side_effect`.
Red: `left: None, right: Some("provider_side_effect_pending")`.
-
`saved_codex_thread_attaches_after_a_release_changes_its_feature_toggles`
(protected sandbox change). Red: `left: None, right:
Some("provider_attach_refused")`.
- `native-session-executor.test.ts`:
- Test "stops same-run retries for an operator while a saved
side-effecting call has no result". Red: `expected
'native_session_interrupted' to be
'native_provider_side_effect_pending'`.
- Test "retains the local controller checkpoint until remote continuity
archiving succeeds: failed/timeout". Red: the message had no detail.
Green: the message has the exit status and output, a token in the output
is redacted, and the local runner and control-plane files are intact.

Safety and regression checks:
- New test "still replaces a session after an unclassified resume
failure when the checkpoint allows it". It passes before and after: a
transport drop still reaches replacement.
- Existing test "types a runner-proven incompatible session when
same-session recovery is required" still passes.
- `native-session-resume.test.ts`: a source stopped with
`native_provider_side_effect_pending` is not rebound.
- `run-completion.test.ts` "replays saved user intent through normal
Retry …" passes with the detailed archive message.
- Runner Vitest: `native-session-runtime.test.ts` and
`harness-driver-backend.test.ts` (266 passed);
`runnerd-codex-transport-warm-attachment.test.ts` and
`codex-app-server-driver.recovery.test.ts` (74 passed).
- Rust: `--lib` (560 passed, plus one ACPX timing test that failed under
load and passed when rerun alone), `codex_provider` (118 passed),
`native_provider_backend` (45 passed), `durable_recovery` (3 passed).

Not run: a live staging recovery at this revision. The local server
typecheck could not resolve the unbuilt runner package, so required CI
provides typecheck.

## Risks

- A run that a replacement session would have continued after an attach
refusal now stops for an operator. The refusal repeats on every retry,
so the replacement was the only way past it. It also destroyed saved
state, which is what blocked the staging tasks. A board Retry still
continues the task in a fresh session in the same workspace. This is the
existing `native_provider_state_incompatible` behavior.
- A new failure code `native_provider_side_effect_pending` appears in
run records and recovery actions. It joins
`EXECUTION_RECONCILIATION_CAUSES`, so the UI shows the reconciliation
flow, not Retry.
- An attach refusal used to reach an automatic replacement; it now needs
one board Retry. This is intended: the replacement archived saved state
and failed part way on staging.
- The pending-side-effect class means "no result delivered to the
runner". The controller replays a recorded tool outcome only after
attach. A call whose outcome the server already recorded therefore also
stops for an operator. That is conservative; the reconciliation shows
the recorded outcome.
- Only a pending tool call classifies an attach blocker. Other blockers
(active turn, ambiguous turn start, active goal) keep their current
behavior.
- The archive error message is longer. The one exact-match consumer now
accepts the `code: detail` form.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 23:25:54 -06:00
DottaandPaperclip 1c91bacf97 fix(native): admit independent replies during contended publication (#19726)
## Thinking Path

> - Paperclip manages work done by AI agents.
> - A completed native run must deliver its saved final response.
> - The response worker also recovers retained replies that can wait on
publication locks.
> - A pass waited for its old publication groups even when other check
slots were free.
> - This could delay an independent final reply beyond a full page of
retained candidates.
> - This PR enables the existing shared coordinator option that admits
independent due work during an active pass.
> - Existing authority checks, resource keys and the four-check limit
still control publication.

## Linked Issues or Issue Description

**What happened?**
A native run completed after replacement recovery. Its final response
and accounting were saved, but its final issue reply was missing. The
response queue contained more than 100 retained chat replies and
repeated endpoint-lock failures. The existing worker did not admit
independent due entries until its current groups finished.

**Expected behavior**
A contended publication owner must not hold free worker slots. An
independent accepted final reply must publish once, including when its
candidate is on a later SQL page.

**Steps to reproduce**
Create 100 accepted, committed replies that share one publication owner.
Create an independent reply after them in candidate order. Hold the old
coordinator rows with a database transaction and start the real response
worker. Before this change, the independent reply stays unpublished
until the old group is released.

Related: #18977 changes response selection in standing conversations.
This PR changes only worker admission and preserves response selection.

## What Changed

- Enable `admitDueWhileActive` in the native response worker's existing
shared coordinator configuration.
- Add a real PostgreSQL regression with the full 100-row SQL page, the
default dispatch batch, real row-lock contention and a later-page
independent reply.
- Verify one comment per reply after settlement and no further SQL or
scheduled deadline across 60 seconds of idle time.
- Update one existing principal/rescope integration assertion to observe
the reconciled invalidation revision. Keep its actual per-run replies,
authorization, publication and idle checks.

## Verification

- Regression on the unchanged worker: the independent reply remained
unpublished while the first 100 replies were locked. The real PostgreSQL
test failed as expected.
- Final head `748fb9609104c016c33ff7caf282ba9e15e88495`: all six
publication regressions and both principal/rescope controls passed (8/8,
47.96 seconds). Checks cover later-page admission, exact per-run comment
counts, retry and rollback behavior, authorization, and 60 seconds with
no idle SQL or scheduled deadline.
- The tests used the canonical PostgreSQL wrapper and one owned
disposable PostgreSQL 18.1 fixture. All clients and test databases were
closed before fixture cleanup.
- The current-head [full CI
run](https://github.com/paperclipai/paperclip/actions/runs/42338953241)
supplies typecheck, build and broad test coverage. All current-head
checks passed; the merge gate is green.
- Local full typecheck was blocked by unbuilt dependencies; it is not
counted as a pass. The prior timer-state CI failure and the final
successful rescope-generation regression remain recorded.
- Greptile reviewed the final head at 5/5 with no unresolved threads.
`git diff --check` passed.
- Deployed acceptance remains separate. No serving instance was changed
by the PR tests.

## Risks

The worker can now use free slots during a slow pass. The shared
coordinator still limits checks to four and preserves task, endpoint and
principal exclusions. Authorization, result identity, retry limits,
response selection and transaction ownership are unchanged. The
regression checks idle quiet after all concrete obligations settle.

## Model Used

OpenAI GPT-6.1 (`gpt-6.1-sol`), with reasoning, code execution and tool
use. The runtime does not expose the context window size.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 22:47:30 -06:00
Nicky Leach b21884bdcf Release and resume retained plugin Runner sessions in Core (#19611) 2026-11-06 20:45:02 -08:00
DottaandPaperclip 9efee7ee16 fix(runtime): save dynamic skill bundles before native admission (#19729)
## Thinking Path

> - Paperclip manages agents, tasks, and their saved execution state.
> - A replacement server must restore each admitted skill from shared
storage.
> - Installed company skills already have immutable versions.
> - Connector skills add assigned resources and tool definitions for
each run.
> - Those dynamic bytes keep a null version ID and differ from the
static skill.
> - Native admission saved their input before it saved a durable package
receipt.
> - This change stages the exact packages first and publishes the input
and receipt together.
> - A new connector turn can then survive an empty server home.

## Linked Issues or Issue Description

Refs #19675 for the existing runtime asset preservation owner.

**What happened?**

A new native run with an assigned Slack connector creates a concrete
local skill bundle after the old runtime assets were migrated. Admission
saves its execution input without a preservation receipt. The new run
creates a local durability dependency again.

**Expected behavior**

With S3 configured, the exact dynamic bundle must be durable before the
execution input becomes visible. Its null version ID and admitted bytes
must remain unchanged. Local storage must keep its volume-backed
behavior.

**Steps to reproduce**

1. Configure S3 and a native agent with an assigned Slack resource.
2. Prepare a new native task through the normal runtime selection owner.
3. Inspect its runtime skill receipt or restore its skills with an empty
server home. The previous owner saved no receipt.

**Paperclip version or commit**

Reproduced on master `d1ee25d0f7`. The fix is tested on `4a3396d9b9`.

**Deployment mode**

S3-configured server. Local tests use PostgreSQL 18.1 and a private disk
transport with the S3 receipt contract.

## What Changed

- Extend the existing runtime asset owner with package staging and
transaction-bound receipt validation.
- Stage exact unversioned skill packages before native admission. Keep
storage I/O outside the run transaction.
- Publish the frozen input and receipt together under the existing run,
cancellation, and package collection locks.
- Reject changed input bindings and retired package metadata.
- Reuse valid same-run receipts without reading the previous host's
bundle path or requiring a new object read.
- Keep existing online preservation and local storage behavior.
- Add a bounded preparation span and document its work.

## Verification

- Genuine unchanged-production RED: the actual Slack overlay and real
admission transaction save no preservation receipt.
- Final rebased head: 68 tests pass in three suites in 25.36 seconds.
These include all runtime-selection, runtime-preservation, and connector
controls.
- The new positive checks that no execution input or coordinator is
visible during package readback. It restores exact skill and tool bytes,
modes, digests, and null version IDs in a cold home.
- Guards cover an uncertain storage failure, cancellation during upload,
a changed binding, retired package metadata, and local mode. An existing
receipt remains usable after removing the original bundle and during a
storage outage.
- The canonical PostgreSQL wrapper uses one owned disposable PostgreSQL
18.1 container. Each suite's database is cleaned up. Real package
archives, streams, and readback run locally; no external provider or
model is called.
- Strict server TypeScript, module boundaries, changed-line private-data
checks, and `git diff --check` pass.
- [Final-head
CI](https://github.com/paperclipai/paperclip/actions/runs/42337473062)
passes all required build, typecheck, test, and Runner gates. Fresh
Greptile is 5/5 with no open review threads.

## Risks

New S3-backed admission now requires its dynamic packages to be saved
before execution can proceed. A storage failure can therefore stop
preparation. Existing resource cleanup keeps its authority. Failed or
cancelled staging retains original files and the package owner's pending
receipts. Same-run recovery retains its original capture provenance and
cache behavior. The tests replace the external S3 transport; they do not
qualify live S3 permissions or provider execution. No schema, API,
workflow, company skill version, or provider protocol changes are
included.

## Model Used

OpenAI Codex (GPT-6 family; the 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
- [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 22:34:44 -06:00
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