Commit Graph
276 Commits
Author SHA1 Message Date
DottaandPaperclip 499712c68b fix(runner): restore cold state before inspecting completed handoffs (#19254)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native Runner saves provider sessions and controller state across
turns.
> - A completed warm handoff leaves a receipt in the controller journal.
> - A later cold continuation must restore external Runner state before
it can inspect that receipt.
> - The transport read the missing physical file before it called the
restore hook.
> - This pull request prepares admitted external state before that
inspection for completed handoffs.
> - Live Runner adoption and pending transitions retain their existing
ownership checks. The provider thread can continue after a cold restore.

## Linked Issues or Issue Description

Related: #19225 and #19157. This fix changes the shared transport read
order. It does not depend on the open source-reconciliation extension in
#19157.

**What happened?**

A cold continuation after a completed warm handoff could fail with
`runner_remote_state_unavailable`. The saved controller journal was
valid. The physical Runner state was absent because its restore hook had
not run yet. A later automatic reattachment could then fail an
allocation identity check.

**Expected behavior**

Restore admitted external state before inspecting a completed handoff.
Retain the original provider thread. Refuse a denied restore or foreign
session without starting provider work.

**Steps to reproduce**

1. Open a Codex session and complete a warm handoff to a second run.
2. Suspend the session and retain the controller journal and provider
backup.
3. Start a third run with no physical Runner state until the external
preparation hook restores it.
4. Read the retained thread. Before this fix, the read fails before
preparation.

## What Changed

- Prepare external Runner state before inspecting a completed
warm-handoff receipt.
- Keep live Runner adoption, pending attachments, and active warm
transitions on the existing exact-state path.
- Add a real Runner and controlled Codex regression for successful
restore, denied restoration, and a foreign restored session.
- Document the cold-continuation order in the execution contract.

## Verification

- Red: the new real-process regression fails on unpatched source because
resume reads the absent `runner-state.json` before preparation.
- Green: all three new cases pass. The restored thread keeps its
identity. The controlled provider starts one thread and resumes it once.
No turn is replayed.
- Adjacent safety suites: 60 cases passed across warm attachment,
interrupted attachment recovery, suspension, and cold restore. The new
foreign-session case also passes separately.
- Runner TypeScript and generated protocol checks passed.
- Full workspace typecheck and build passed. Runner TypeScript checks
passed again after the live-adoption guard. The full local test command
remains in progress; all required CI test jobs passed on the current
head.
- The first CI run found two live-adoption regressions: an already
completed staged handoff must not restore a backup while its Runner is
live. The guard now excludes live adoption. All six cases across cold
restore and staged handoff pass locally. The original CI failure remains
recorded in run 42047027346.
- Greptile reviewed the corrected head
`f33a041c8902c39ca52b995d53128fc23b0b5499` at 5/5. There are no review
threads or findings. All current-head CI gates passed.
- Live staging verification remains pending normal deployment. No pinned
serving image or bulk migration is part of this PR.

## Risks

The change calls an existing external preparation hook earlier for
completed handoffs. That hook must remain idempotent and must enforce
restore authority. Live adoption, pending attachments, and active
transitions do not gain this earlier call. The negative regressions
assert that a refused restore and a foreign session start no provider
work and do not change the authority journal.

## Model Used

OpenAI Codex, based on GPT-6, with reasoning, tool use, 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-05 09:14:10 -06:00
DottaandClaude Opus 5.5 e37ab23dbc fix: hold an agent's new work while its provider usage limit is active (#19246)
## Thinking Path

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

## Linked Issues or Issue Description

Refs #13148
Refs #13379

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

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

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

## What Changed

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

## Verification

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

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

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

## Risks

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

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

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code with tool use (shell, file edits, browser reads of staging run
records) and extended reasoning.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 08:57:55 -06:00
DottaandClaude Opus 5.5 d0afeacdd5 fix(remote-workspaces): keep retained runner state when replacing a retired root (#19251)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Remote workspaces save a task's files as snapshots. Runtime state
under `.paperclip-runtime` is never in a snapshot, because it belongs to
the sandbox
> - #19231 lets a parent task on a retained sandbox restore newer child
work over its own retired workspace root
> - That replace removed the whole retired root, including the parent's
retained runner session state under
`.paperclip-runtime/paperclip-runner/`
> - The parent's native continuation then reads that runner state on the
resumed sandbox, cannot find it, and fails with
`runner_remote_state_unavailable`
> - This pull request moves the retired root's runtime state into the
restored root before it removes the retired root
> - The benefit is that delegating parents on retained sandboxes
continue their session with the children's saved work

## Linked Issues or Issue Description

Refs #19231

**What happened?**

After #19231, a parent task on an agent with a retained sandbox
delegates two subtasks. Both subtasks finish. The parent's wake run now
restores the newer saved work successfully, but the run fails right
after with `runner_remote_state_unavailable: exit=1 timedOut=false`. The
run error code is `native_continuation_requires_reconciliation` and the
task becomes `blocked`. In one staging wave, 4 of 4 such parents failed.

Root cause: the native continuation on a resumed sandbox reads
`<remoteCwd>/.paperclip-runtime/paperclip-runner/sessions/<session>/runner/runner-state.json`.
Snapshots exclude `.paperclip-runtime`, so the restored root has only
the new restore receipt. #19231 parked the retired root and then removed
it, so the runner state was removed too.

**Expected behavior**

The restored root has the newest saved files and the sandbox's retained
runtime state. The parent's continuation reads its runner state and
continues.

**Steps to reproduce**

1. Prepare a remote workspace on sandbox A. Write files and runner state
under `.paperclip-runtime/paperclip-runner/`. Save.
2. Make a second run the writer on sandbox B. It restores, writes new
files, and saves.
3. Make a third run the writer on sandbox A again and prepare. The
restore replaces the retired root.
4. Read the runner state with `readRemoteRunnerState`. Before this
change, it fails with `runner_remote_state_unavailable: exit=1
timedOut=false`.

**Paperclip version or commit**

`master` at `26391707fb`, which includes #19231.

## What Changed

- `server/src/services/remote-workspace-transport.ts`: before it removes
the retired root, the restore script moves each entry of the retired
`.paperclip-runtime` into the restored root's `.paperclip-runtime`. It
does not move `workspace-ready.json`, `workspace-restored.json`, or
`workspace-snapshots`, and it does not replace an entry that already
exists. The move is a rename, so it keeps sockets and large session
state. The script also finishes an interrupted carry when the same
restore repeats. Only an authorized replace with that restore identity
creates the `<root>.retired-<identity>` path.
- `server/src/services/remote-workspaces.integration.test.ts`: the
retained-sandbox regression now writes parent runner state, restores
newer child work, and reads the state with the same
`readRemoteRunnerState` call that the continuation uses. It also covers
an interrupted carry on a repeated restore.
- `doc/execution-semantics.md`: document that runtime state moves with
the restored root.

## Verification

- Red before the fix: `npx vitest run
src/services/remote-workspaces.integration.test.ts -t "retired root"`
failed in the `own retired root`, `own restored root`, and `read-only
folder` cases with `Error: runner_remote_state_unavailable: exit=1
timedOut=false`. This is the same error as the staging failure.
- Green after the fix: all nine cases pass. The continuation read
returns the parent's runner state. The old `workspace-ready.json` is
gone and the new restore receipt is present. A repeated restore finishes
an interrupted carry and removes the retired root and its stale files.
- `npx vitest run src/services/remote-workspaces.integration.test.ts
src/services/remote-workspace-transport.test.ts
src/services/heartbeat/run-completion.test.ts`: 250 passed.
- Staging evidence (read-only): the failed parent wake run recorded a
succeeded `remote_workspace_restore` at the parent's own provider lease,
and then failed in the runner-state read.
- I did not run a live staging run. This PR does not deploy.

## Risks

- Runtime state from the retired root now persists into the restored
root. The state is the sandbox's own state for this workspace path,
which is the same state that a same-allocation reuse keeps.
- If the restored root already has an entry with the same name, the old
entry is not moved and is removed with the retired root.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic. It used tool use for
code search, shell, test runs, and read-only staging inspection in the
browser.

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 08:45:42 -06:00
DottaandClaude Opus 5.5 03abe1f1a1 fix(native): keep the transport cause on a closed notification stream failure (#19240)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs read provider events from a shared session notification
stream (Codex, OpenCode and ACPX drivers use it)
> - When that stream closes, the driver fails the session with
`notification_transport_failed`
> - The run error was only "Provider notification transport failed
closed." The real cause was only in a diagnostic event
> - In a staging soak, many runs on OpenRouter and Grok agents showed
this error. Operators read it as a provider fault. The diagnostic events
showed a host-side cause: a stopped sandbox plugin worker ended remote
runner supervision
> - This pull request keeps the redacted, bounded cause on the terminal
failure message, and does not name the provider
> - The benefit is that a failed run shows why it failed, so operators
do not chase the wrong provider

## Linked Issues or Issue Description

**What happened?**
Native runs failed with `native_provider_terminal_failed` and the error
"Provider notification transport failed closed." The run did not show a
cause. All failures in one cluster happened inside one minute, on
several agents and models (OpenRouter DeepSeek, Grok). Each run's
`harness.diagnostic` event had the real cause, for example
`runner_process_termination_unconfirmed: runnerd process supervision
failed: Cannot call "environmentExecute" — worker for "<plugin>" is
stopped`, or `native_adopted_runner_identity_unverifiable: runner
liveness could not be revalidated`. The sandbox duplex transport
reported a `provider_exit` loss about 30 seconds before (the read-only
process probe budget).

**Expected behavior**
The terminal run error keeps the redacted cause. It does not blame the
model provider when the cause is host-side runner supervision.

**Steps to reproduce**
1. Open a native Codex-protocol session and start a turn.
2. Make the notification stream throw (for example, a supervision
error).
3. Read the `session.failed` and `turn.failed` messages. Before this
change they are the bare generic text.

## What Changed

- `codex-session-notifications.ts`: the `notification_transport_failed`
terminal message is now `Notification transport failed closed: <cause>`.
The cause is redacted first, then whitespace is collapsed and it is
bounded to 1,000 characters. The `Error: ` prefix is removed. The code
and `recoverable: false` do not change.
- New regression in `codex-protocol-integrity.test.ts`: one
`session.failed`, the cause and code are present, the secret is
redacted, the length is bounded, and `turn.failed` has the same message.
- `doc/execution-semantics.md`: an additive note that this failure can
come from host-side runner supervision, and that the message keeps the
redacted cause.

## Verification

- Red: before the fix, the new test failed: `expected 'Provider
notification transport faile…' to contain
'runner_process_termination_unconfirmed'`.
- Green: `cd packages/paperclip-runner && npx vitest run
src/drivers/codex/ src/conformance/local-provider-fault-matrix.test.ts
src/native-session-runtime.test.ts` (565 passed).
- `npx tsc --noEmit -p packages/paperclip-runner/tsconfig.json`: no new
errors. The existing `src/eval/workflow-harness.ts` errors come from an
unbuilt workspace package.
- Staging evidence was read with read-only GETs only (run, events).

## Risks

- Low risk. Only the message text changes. The failure code,
`recoverable: false`, and the safe-replacement rules do not change. The
server matches on the code, not on the message.
- This PR does not add a retry in the same turn. Runner termination is
unconfirmed when supervision is lost. A retry or replacement could start
a second executor. The remote process probe already retries read-only
checks inside a 30-second budget. Recovery stays with the existing
ownership and recovery checks.
- The cause text can include a plugin id. It goes through the existing
provider-diagnostic redaction.

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

## Model Used

- Claude Opus 5.5 (Anthropic), model ID `claude-opus-5-5`, used in
Claude Code with tool use (shell, file edits, browser reads) and
extended reasoning.

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 07:51:39 -06:00
DottaandClaude Opus 5.5 e8163fc860 fix(remote-workspaces): restore newer saved work over a retained sandbox's own retired root (#19231)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Remote workspaces save a task's files as snapshots and restore them
on the sandbox that runs the next turn
> - A parent task can delegate subtasks. The subtasks inherit the
parent's workspace, run on their own sandboxes, and save newer snapshots
> - When the parent wakes on its retained (warm, reused) sandbox, its
earlier workspace root is still on that sandbox at the same published
path
> - The restore refused that root with `reason=occupied_root`, so the
parent run failed at setup and the task became blocked with a board
recovery action
> - This pull request lets the restore replace that root only when the
server proves it is the workspace's own earlier, fully saved allocation
on this sandbox
> - The benefit is that a delegating parent on a retained sandbox
continues with the children's saved work, and unknown occupants are
still refused

## Linked Issues or Issue Description

**What happened?**

A parent task on an agent with a retained sandbox delegates two subtasks
to other agents. Both subtasks finish. The parent's wake run fails
before the provider starts with `remote_workspace_restore_failed:
step=sandbox_restore; ... stage=inspect_root reason=occupied_root`. The
task becomes `blocked` with a stranded-work recovery action. Parents on
agents with ephemeral sandboxes complete in the same wave.

Root cause: the subtasks inherit the parent's execution workspace. Each
subtask becomes the writer at a new epoch on its own sandbox, restores
at the same published directory name, and saves a newer snapshot. The
workspace location then points at the last subtask's sandbox. The
parent's wake run gets a lease on its retained sandbox, so preparation
is not "same allocation" and must restore the newer snapshot. The target
path on the retained sandbox still holds the parent's epoch-1 root,
which has a preparation receipt (`workspace-ready.json`) and no restore
receipt. The restore script refuses any occupied root that is not the
same restore, so it fails with `occupied_root`. A warm reuse cannot just
keep the old root, because the snapshot is newer.

**Expected behavior**

The parent restores the newest saved work (its own files plus the
children's results) into its published root and continues. A foreign or
unknown occupant is still refused.

**Steps to reproduce**

1. Prepare a remote workspace on sandbox A, write files, and save (final
boundary).
2. Make a second run the writer on sandbox B. It restores, writes new
files, and saves.
3. Make a third run the writer on sandbox A again (same provider lease
id, new lease row) and prepare.
4. Before this change, step 3 fails with `stage=inspect_root
reason=occupied_root`.

**Paperclip version or commit**

`master` at `47027e7ba5`.

## What Changed

- `server/src/services/remote-workspaces.ts`: add
`isRetiredOwnAllocation`. It reads this workspace's snapshot and restore
operations recorded at the same environment, provider lease id, and root
path. It returns true only when every such use is from an earlier epoch
than the current writer, the last use ended in a succeeded `final` save
with quiesced save work (`unknown=false`, no active invocations), no
other save in that epoch is newer or unsettled, and the saved snapshot
is committed or later. Preparation passes the result to the restore as
`replaceRetiredRoot`, and only when there is no migration and the
authority is clean.
- `server/src/services/remote-workspace-transport.ts`: the restore
script accepts `replaceRetiredRoot`. In that mode it replaces an
occupied root only when the root carries a well-formed Paperclip receipt
(`workspace-ready.json` or `workspace-restored.json` with a 64-hex
identity). It parks the old root at `<root>.retired-<identity>`,
publishes the restored folder, rolls the old root back if the publish
rename fails, and then removes the parked copy. A repeated restore with
the same identity adopts the published folder and removes any parked
copy left by an interrupted attempt. Without a receipt, the script still
fails with `reason=occupied_root`. `replaceRetiredRoot` and
`allowEmptyRoot` cannot be combined.
- `doc/execution-semantics.md`: document the rule.
- `server/src/services/remote-workspaces.integration.test.ts`: add a
red/green regression and the safety cases.

## Verification

- Red before the fix: `npx vitest run
src/services/remote-workspaces.integration.test.ts -t "retired root"`
failed in the `own retired root` case with
`remote_workspace_restore_failed: step=sandbox_restore;
remote_workspace_operation_failed: operation=restore exit=1;
remote_workspace_restore_failed: stage=inspect_root
reason=occupied_root`. This is the same cause text as the staging
failure.
- Green after the fix: all six cases pass:
- `own retired root`: the parent restores on its retained sandbox. The
root has the parent's file and the child's file, the parked and staging
folders are removed, and the authority records the parent's lease. A
repeated preparation (lost reply) keeps the parent's new unsaved edits.
- `own restored root`: an occupant with a restore receipt from a
different preparation is also replaced.
- `receipt missing` and `forged receipt`: the occupant is refused with
`occupied_root`, and its files stay.
- `later allocation`: a later use of that root on that sandbox without a
final save is refused.
- `never saved here`: a different sandbox with an unknown folder at that
path is refused.
- `npx vitest run src/services/remote-workspaces.integration.test.ts
src/services/remote-workspace-transport.test.ts
src/services/heartbeat/run-completion.test.ts`: 247 passed. This
includes the existing `occupied root` cold restore case, which is still
refused.
- `tsc --noEmit` shows no errors in the changed files. The local tree
has unrelated errors from unbuilt workspace packages.
- I did not run a live staging run. Staging is read-only for this work,
and this PR does not deploy.

## Risks

- The replaced root is removed after the publish. The server proof
states that its content is in the saved lineage. If a process wrote to
that root after its final save and outside a Paperclip run, those writes
are lost. The same assumption applies when authority moves to another
allocation today.
- The proof uses operation records that have `exportCleanup` location
metadata. Older records without that metadata do not qualify, and those
cases still fail closed with `occupied_root`.
- A warm runner process that kept an open working directory in the old
root sees a removed folder. This is not verified live. The new turn is
given the published path, which holds the restored files.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic. It used tool use for
code search, shell, test runs, and read-only staging inspection in the
browser.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 07:07:13 -06:00
DottaandPaperclip 47027e7ba5 fix(routines): retain authenticated agent responsibility on creation (#19222)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Routines save work for later execution under a responsible person's
authority.
> - An agent can create a routine through its normal REST or native API
tools.
> - The create route kept the agent creator but dropped the
authenticated responsible user.
> - The service then selected a parent task's user or a company default,
even when the source run had a different user.
> - This pull request passes the authenticated responsible user
separately from creator attribution.
> - Future automatic runs then use the authenticated responsible user
saved in the routine revision.

## Linked Issues or Issue Description

**What happened?**

An authenticated native agent created a routine without a parent task.
The stored responsible user became a different company owner. An
unrelated parent task could also replace the authenticated user. The
defect changes the identity saved for future automatic work.

**Expected behavior**

Creation must use the server-resolved responsible user from the
authenticated run or API key. The agent remains the creator. A person
who clicks Run now still supplies that manual run's identity.

**Steps to reproduce**

1. Create an active company member and a different company owner.
2. Initialize a native agent run with the member's identity.
3. Use `call_api` to create a paused, self-assigned routine without a
parent.
4. Read the routine and its first revision. Before this fix, both use
the company owner.

**Paperclip version or commit**

Reproduced on master `56517dda6c5b32bc901b7dbf1605eaaa041c5fba`.

**Deployment mode**

Local source integration with real HTTP routes, agent authentication,
native tool authority, and embedded PostgreSQL. No provider turn is
needed for the reproduction.

Related work:
[#19128](https://github.com/paperclipai/paperclip/pull/19128) adds board
controls for saved responsibility.
[#15207](https://github.com/paperclipai/paperclip/pull/15207) repairs
synthetic identities from built-in installers. Neither fixes this
authenticated create path.

## What Changed

- Pass the authenticated agent's responsible user as a separate internal
actor field.
- Select the board user first, then the agent’s server-resolved
responsible user, before the existing parent and company fallback.
- Keep creator attribution, initial revision attribution, manual
dispatch identity, and existing records intact.
- Add real native API, HTTP, and PostgreSQL regressions for identity
selection, replay, client spoofing, stale claims, revoked membership,
cancellation, cross-company calls, API keys, and fallback behavior.
- Document the creation and manual dispatch identity rules in the
execution contract. Correct the obsolete responsibility sentence in the
agent access map.
- Make the existing live-retry route fixture wait for its committed
comment’s durable wake before draining. Keep all process and saved-owner
assertions.

## Verification

- Captured a failing native `call_api` test before changing production
code. The persisted responsible user was the unrelated company owner.
- The same real route and database boundary passes after the fix.
- Initial affected routine and identity checks: 131 passed. The existing
pending-AI-card fence also passed.
- Follow-up routine checks: 118 passed. The owning scheduled-retry suite
passed all 31 tests.
- Initial CI serialized shard 7/9 failed with a `23503` wake-to-agent
foreign key error during test cleanup. A controlled
real-route/PostgreSQL pause reproduced that exact error on unchanged
base `56517dda6c`. The drain returned before the route registered its
deferred comment wake. The same pause passed after the fixture waited
for the actual commentId’s durable wake receipt. The original failure is
preserved.
- Affected server typecheck after the fixture query change: passed.
- `pnpm -r typecheck`: passed.
- `pnpm build`: passed.
- `pnpm test:run`: still running as supporting coverage. The worktree
changed during the run: map cleanup, then the six-line fixture wait.
Production and routine-test files stayed unchanged. This is
mixed-working-tree coverage, not an immutable full-suite pass for a
commit. Exact next-head CI remains required.
- `node scripts/check-maps.mjs` and `git diff --check`: passed.
- Exact head `a12e3f6c42b8ad5826fd58faa3cd9553ea251e09`: all CI gates
passed. Greptile and the native substantive review both gave 5/5 with no
open findings. The native reviewer did not run tests or exercise
shadow-mode authorization.
- Live provider execution remains unverified. This change does not grant
access to another person's AI account.

## Risks

New agent-created routines now use their authenticated responsible user
before a parent or company default. This is the intended behavior
change. The service keeps its fallback when no bound identity exists.
Missing agent-key identity still fails authentication. Missing personal
AI configuration still blocks execution. Existing routines, revisions,
tasks, grants, and manual dispatch behavior do not change. The fixture
waits for a persisted wake record before draining; its original
live-process and saved-owner assertions remain.

## Model Used

OpenAI `gpt-6.1-sol` with reasoning, tool use, and code execution. The
host did not expose the context window size.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-05 06:15:25 -06:00
DottaandPaperclip 3a090b815b fix: retain native checkpoint ownership through warm close (#19225)
## Thinking Path

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

## Linked Issues or Issue Description

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

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

**Paperclip version or commit**

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

**Deployment mode**

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-05 05:23:19 -06:00
DottaandClaude Opus 5.5 b8e8ab4641 fix(recovery): continue saved input after a recovered sandbox stops, not after its destroy (#19224)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A board user can stop or interrupt a native run and send a message
for the next turn
> - When the stop leaves sandbox files unsaved, automatic recovery saves
them, re-stops the sandbox, and the saved message continues
> - For agents whose sandbox is destroyed after each turn (an ephemeral
lease), recovery then queues the destroy. That step removed the lease's
stop receipt
> - The continuation gate waits for a stop receipt, so the message
waited for the destroy and its cleanup retry, about 5 to 8 minutes
> - Agents with retained sandboxes continued within seconds, so the
delay looked agent specific
> - This pull request keeps the verified stop on the cleanup lease and
lets the continuation and the next remote writer accept it
> - The benefit is that a redirect, Retry or interrupt continues right
after its files are saved, for every lease policy

## Linked Issues or Issue Description

Refs #19206

**What happened?**
On hosted staging, a Stop or `interrupt: true` comment on a native run
left a follow-up turn that started 5 to 8 minutes later for some agents,
and 25 to 40 seconds later for others. The slow agents use ephemeral
sandbox leases. In each slow case, the stopped run's lease was released
by `pending_cleanup_retry` about 5 minutes after recovery, and the saved
message started one second later. The fast agents use retained leases,
which recovery releases at once.

**Expected behavior**
The saved message continues right after recovery saves the files and the
sandbox is stopped. The destroy of a stopped sandbox is resource cleanup
and must not hold the next turn.

**Steps to reproduce**
1. Use an agent whose sandbox lease is ephemeral (destroy after each
turn).
2. Interrupt or stop its native run so that recovery must save the
sandbox files.
3. Post a message. See the follow-up start only after the sandbox
destroy finishes.

**Paperclip version or commit**
Hosted, at `56517dda`.

## What Changed

- `server/src/services/workspace-recovery.ts`:
`scheduleRecoveredWorkspaceCleanup` keeps the verified stop receipt as
`workspaceRecoveryCleanup.stopReceipt` when it queues the destroy.
- `server/src/services/remote-execution-termination.ts`: new
`hasRecoveredCleanupStopReceipt(lease)`. It is true only when the lease
has recovery cleanup for its own run, was released, and the carried
receipt matches the lease, run, provider and provider lease id.
- `server/src/services/explicit-native-continuation.ts`: the
`remote_cleanup` gate accepts that receipt.
- `server/src/services/execution-blocker.ts`: `hasPendingRunCleanup`
ignores a lease with that valid receipt, so a saved native interrupt
send (and Retry or fresh-workspace checks) does not wait for the
destroy. Other cleanup holds are unchanged.
- `server/src/services/remote-workspaces.ts`: the next remote writer
accepts that receipt for the ended writer's lease.
- `server/src/services/execution-blocker.ts` (from #19206 review, not in
the merged head): pre-turn evidence uses the exact startup lifecycle
event list, so turn-scoped events such as `workspace.diff.recorded`
count as provider work.
- `doc/execution-semantics.md`: describes the rule.

## Verification

- Red before the fix, green after: `cd server && npx vitest run
src/services/workspace-recovery.test.ts -t "admits the interrupt
redirect"`. An interrupted native run with recovered files and a queued
destroy blocked the user message with `remote_cleanup` before the fix.
After the fix the message is admitted. A lease without the stop receipt,
or with a receipt for another run, still blocks with `remote_cleanup`.
The test runs the same input as a plain message and as a saved native
interrupt send (`queuedCommentInterruptId`); `hasPendingRunCleanup` was
true before the fix and blocked the interrupt send with
`cleanup_pending`.
- `cd server && npx vitest run
src/services/remote-workspace-reservation.test.ts -t "ended writer"`:
the next writer reserves the workspace from an ended writer whose
recovered lease awaits destroy; a missing or foreign receipt still
blocks. Removing the new branch fails the positive case.
- Updated `saves the authoritative remote workspace after native failed
...`: Retry now continues before the destroy, and the cleanup lease
carries the stop receipt for its run and lease.
- `workspace-recovery.test.ts -t "provider work cannot be excluded"`
includes `workspace.diff.recorded` as provider work.
- Related suites pass: `workspace-recovery.test.ts`,
`explicit-native-continuation.test.ts`,
`remote-workspace-reservation.test.ts`,
`remote-workspaces.integration.test.ts`,
`remote-workspace-lifecycle.test.ts`, `task-retry*.test.ts`,
`native-cancelled-workspace-preservation.test.ts`,
`workspace-recovery-source.test.ts`. `cd server && npx tsc --noEmit`
passes.
- Hosted evidence (read only): slow cases have ephemeral leases released
by `pending_cleanup_retry` about 5 minutes after
`issue.workspace_recovered`, with settlement one second later. Fast
cases have retained leases released by recovery.

## Risks

- Low to medium. The next turn can start while the old stopped sandbox
is still being destroyed. The receipt proves that the sandbox was
stopped after the files were saved, and an ephemeral successor uses a
new sandbox. A destroy failure stays with the existing cleanup retry.
- Leases without the carried receipt behave 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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 05:08:03 -06:00
DottaandClaude Opus 5.5 beb73381ca fix(native): let a Stop before the provider session opens continue once (#19206)
## Thinking Path

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

## Linked Issues or Issue Description

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

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

**Paperclip version or commit**

Base: `87dfba6818`.

**Deployment mode**

Legacy runtime with sandbox (Daytona) remote workspace storage.

## What Changed

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

## Verification

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

## Risks

- A retry keeps the same restore path. If the cause is deterministic but
unnamed, the run uses its two retries before the board action. Named
refusals skip the retries.
- The board action is recorded only for statuses that stranded-work
recovery does not scan, and only when no other action is active. This
prevents a second recovery identity for one failure.
- Script stderr grammar is new. Old sandboxes run the script that the
server sends, so no runner update is necessary.
- No migration, deployment, or credential change.

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

## Model Used

Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, through Claude Code with
tool use, code execution, and browser tools for read-only staging
evidence. Context window and reasoning mode are host-managed.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 03:13:44 -06:00
DottaandClaude Opus 5.5 f5891564ec fix(recovery): continue from a retired sandbox's backup and hand spent export repairs to the board (#19208)
## Thinking Path

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

## Linked Issues or Issue Description

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

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

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

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

## What Changed

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

## Verification

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

## Risks

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

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

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic. Agentic coding with
tool use (shell, file edits, read-only browser inspection of staging).
Default context window.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 02:40:40 -06:00
DottaandClaude Opus 5.5 2ce5f03cd8 fix(workspaces): name remote restore failures and leave a board recovery action (#19207)
## Thinking Path

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

## Linked Issues or Issue Description

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

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

**Paperclip version or commit**

Base: `87dfba6818`.

**Deployment mode**

Legacy runtime with sandbox (Daytona) remote workspace storage.

## What Changed

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

## Verification

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

## Risks

- A retry keeps the same restore path. If the cause is deterministic but
unnamed, the run uses its two retries before the board action. Named
refusals skip the retries.
- The board action is recorded only for statuses that stranded-work
recovery does not scan, and only when no other action is active. This
prevents a second recovery identity for one failure.
- Script stderr grammar is new. Old sandboxes run the script that the
server sends, so no runner update is necessary.
- No migration, deployment, or credential change.

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

## Model Used

Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, through Claude Code with
tool use, code execution, and browser tools for read-only staging
evidence. Context window and reasoning mode are host-managed.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 02:40:01 -06:00
ea42a2e417 fix(recovery): coalesce duplicate legacy no-replay holds (#17055)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Recovery records keep uncertain execution outcomes from being
replayed.
> - A settled record can still block replay while it waits for a
verified decision.
> - Repeated scans can create more settled records for the same failed
execution.
> - Resolving only one duplicate leaves the task behind another hold.
> - This pull request retains the settled hold and applies one verified
decision to its exact duplicates.
> - The task can continue through the existing delivery path without
creating another delivery owner for each duplicate.

## Linked Issues or Issue Description

Refs #14600. This change addresses duplicate settled holds and their
same-source resolution. The visibility and other deferred-wake paths
remain outside this change.

Related: #10011 handles unchanged active incidents. This patch handles
settled no-replay records. #15285 covers missing reconciliation input.
#17031 covers a handoff to a new task owner. External-adapter
continuation is covered separately by #15982.

## What Changed

- Retain a resolved legacy no-replay decision when a later scan finds
the same company, task, cause, fingerprint, and source run.
- After a verified reconciliation, resolve duplicate records that also
match the selected action's kind. Keep the selected action as the
delivery owner for this group. Record the merged action IDs in the
activity log.
- Preserve independent workspace and system recovery records, existing
reconciliations, and records for other sources.
- Add PostgreSQL regression tests for repeated scans, historical
duplicates, delivery count, repeated resolution, scope boundaries,
simultaneous resolutions, and rejected reconciliation rollback.
- Update the execution semantics and recovery maps. This patch has no
schema migration or adapter-specific code.

## Verification

Current head: `3c84bc89325d40bfdda5d28f24ed2fb67d48756c`. It merges
`master` at `043f2514e67c50320433a0013fb228a75a51325c` without rewriting
the existing commits. The only conflict was in
`markExecutionReconciliation`. The resolution retains the upstream
transaction and all three queue notifications. Source, sibling, and
audit writes use the same transaction.

The final merged tree passed:

```sh
pnpm exec vitest run server/src/__tests__/issue-recovery-actions.test.ts server/src/services/legacy-execution-recovery.test.ts server/src/__tests__/heartbeat-process-recovery.test.ts server/src/__tests__/execution-control-work.test.ts --maxWorkers=1
# 549 passed; zero skipped
pnpm --filter @paperclipai/server exec tsc --noEmit
# exit 0
node scripts/check-maps.mjs
# passed: 35 areas, 191 capability IDs
git diff origin/master --check
# passed
```

Three new cases add real PostgreSQL and HTTP-route evidence. Two
resolution requests target different duplicate holds while both wait on
database locks. They leave one continuation owner, one coalescing audit
record, and one queued successor. A live-provider rejection leaves all
records unchanged. A checkout rejection after reconciliation writes
rolls back the source, siblings, and audit row. The fixture occupies the
dispatch slot, so no external model provider runs.

The new contention case was also run with only
`execution-recovery-resolution.ts` replaced by its parent revision at
`65f59e339a9fb6e0635bcbd8c36791bd610326a6`. It failed with two
continuation owners instead of one. The source was restored
byte-for-byte, and all three new cases passed again before the merge.
The final four-suite run above validates the merge with the upstream
notification changes.

The original positive regressions also failed on that parent revision:
three scan-and-settle cycles left four records instead of one, and
resolving one historical hold left its two duplicates unreconciled. The
scope matrix covers company, task, kind, cause, fingerprint, source run,
replay state, protected recovery records, and prior reconciliation.

Fresh-checkout collection initially failed because plugin SDK build
outputs were absent. After building them, PostgreSQL startup exposed
native library symlinks missing from an installation with scripts
disabled. The package's symlink setup resolved that failure. The
standard server typecheck attempt started a Rust vendor prerequisite
build and was stopped; direct TypeScript checking passed. These failed
or incomplete attempts are not counted as passes.

Full repository build, recursive typecheck, and CI were successful on
the earlier `08e5c0285` revision ([CI
run](https://github.com/paperclipai/paperclip/actions/runs/39541949702)).
They are historical evidence, not results for this merge. The current
local proof is the four suites and checks listed above. Full upstream CI
for the merge is pending. Greptile reviewed
`3c84bc89325d40bfdda5d28f24ed2fb67d48756c` with 5/5 confidence and no
actionable findings; that source review is separate from test execution.

## Risks

- A match that is too broad could clear an independent hold. Tests cover
company, task, kind, cause, fingerprint, source run, replay state,
workspace recovery, system recovery, and prior reconciliation
boundaries.
- The existing issue-lock transaction covers the selected action and its
duplicates. The patch does not create a new lock or an automatic retry
path.
- The patch does not infer that a network or provider failure means no
action occurred. Existing reconciliation validation still checks the
source, owner, stopped processes, leases, and observed action outcome.
- Previously reconciled records and their delivery state remain
unchanged. This patch prevents new duplicate delivery owners when it
resolves unreconciled duplicates. It does not migrate every historical
delivery record.

## Model Used

OpenAI Codex, based on GPT-6, assisted with the implementation, tests,
and review. It used repository tools and code execution. The exact
runtime model ID, context window size, and reasoning setting are not
exposed in this session. An independent AGY review also ran; its
provider model ID was not exposed.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with the available 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 linked the existing issue with `Refs #14600`
- [x] I have not referenced internal/instance-local Paperclip issues or
links
- [x] My branch name describes the change and contains no internal task
IDs or instance details
- [x] I have run the focused tests locally and they pass; full upstream
CI is pending for the final merge
- [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 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: Rockiez <7697772+Rockiez@users.noreply.github.com>
Co-authored-by: Devin Foley <devin@paperclip.ing>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 23:54:51 -08:00
DottaandClaude Opus 5.5 87dfba6818 fix(native): retry an unpublished accepted task reply durably instead of dropping it (#19200)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A native run commits its status decision and its final reply comment
in separate steps
> - Background presentation recovery publishes an accepted reply when
the live path does not record one
> - That worker dropped the run after one refused attempt and only threw
on an error, with no record on the run
> - A task could then show done while the user never saw the answer,
until an unrelated wake reloaded the queue
> - This pull request makes the publication of an accepted ordinary task
reply durable, bounded, and recorded
> - The benefit is that a task cannot close silently without its answer

## Linked Issues or Issue Description

Refs #19130 (verify requested publication at finalization, for
documents), #19183 (deliver messages saved while a native run
finalizes), #19186 (stale owner done). This change does not duplicate
them. It covers the final reply comment.

**What happened?**
A native run accepted a result with disposition `done`. The status
decision set the task to done. The accepted final reply was not
published as a task comment. The run had no publication record (no
`run.presentation.resolved` event and no `presentationDecision`). The
reply was published by background recovery 58 minutes later, after an
unrelated wake reloaded the presentation queue.

**Expected behavior**
The accepted reply is published promptly. A refused or failed attempt is
retried with backoff and never posts twice. If publication fails
permanently, the run and the task show a named failure with an owner.

**Steps to reproduce**
1. Commit an accepted native task result with disposition `done`, with
the final reply not yet visible to presentation recovery.
2. Run the native response presentation worker check once. The attempt
is refused.
3. On `master`, the worker returns `null` and drops the run. Nothing
records the attempt. Nothing retries it until another notification for
that scope.

**Paperclip version or commit**
`master` at 3b17b5ce15.

**Deployment mode**
Cloud staging, native `paperclip_runner` agent.

Root cause notes: the staging evidence shows that the live completion
path did not reach its presentation step for the affected run (no
`lifecycle run succeeded` event). Staging server logs for that window
are not available, so the exact first refusal is not proven. The worker
defect is proven: one refusal makes `check()` return `null`, which
deletes the scheduled entry. One thrown error escapes with only
in-memory backoff and no record. The fix closes that gap for every
refusal or error cause.

## What Changed

- `native-response-presentation-work.ts`: when an accepted ordinary task
reply (`done` or `needs_review`, not an Agent Chat conversation) is
still unpublished after a due attempt, the worker records the attempt
and keeps the run scheduled. A thrown error for this class is recorded
instead of escaping. When the reply is published, an open retry receipt
is set to `resolved`.
- New `native-response-publication-receipt.ts`: stores the receipt in
`heartbeat_runs.result_json.responsePublication` (attempts, last reason,
last error, next attempt). Backoff is 30 s doubling to at most 5 min,
for 6 attempts. Each retry appends a `run.response_publication.retrying`
run event. After the last attempt it records
`native_response_publication_failed` with the run's agent as owner, a
`run.response_publication.failed` run event, and an
`issue.response_publication_failed` task activity.
- `native-response-presentation-query.ts`: the candidate query exposes
the saved retry deadline, so a restarted worker reconstructs the
schedule. It excludes a run whose publication is recorded as failed, so
retries stay bounded.
- An open retry receipt is settled to `resolved` in the same write that
records the reply's `commentId` (background recovery and the live
`recordRunPresentationDecision`). A candidate that disappears for
another reason leaves the receipt unchanged, so the bound cannot reset.
- When the reply comment already exists but its decision was not
recorded, recovery now records it as `reuse` with that `commentId`. It
does not post again and does not count a refusal.
- Under the run lock, an attempt is not counted again when another
worker already saved a future deadline. The worker also honors that
saved deadline after recovery.
- Idempotency is unchanged: the comment is written only while the
presentation decision has no `commentId`, under the existing coordinator
lock. The receipt write never overwrites a run that already has a
`commentId`.
- `doc/execution-semantics.md`: documents the contract.

## Verification

- `cd server && npx vitest run
src/services/native-runtime/native-response-publication-retry.test.ts`
- Red on `master` (3 failed): after one refusal `check()` returned
`null` (dropped); one injected transaction error escaped with no record;
the refusal loop never stopped (more than 10 attempts, no failure
recorded).
- Green with this change (5 passed, including an already posted reply
and two concurrent workers): one refusal keeps the run scheduled and
recorded, a reloaded worker keeps the same deadline, the reply posts
exactly once after it becomes visible, and repeated checks do not
double-post; one injected error is recorded and retried; permanent
refusal stops after bounded attempts with the named, owned failure on
the run event and task activity.
- Related suites pass: `agent-chat-response-recovery.test.ts`,
`presentation-ownership-read.test.ts`,
`idle-native-finalization.test.ts`, `chat-monitor-continuation.test.ts`,
`native-preserved-monitor-wait.test.ts`,
`paperclip-control-plane-port.test.ts`,
`chat-channels.integration.test.ts`,
`photon/channel.integration.test.ts` (2047 of 2049 passed in one
combined run). One local failure needs a bundled Slack skill file that
is missing in this worktree. The other ("materializes direct
external-chat finals once") failed once under load and then passed three
times in isolation and in the combined set with this change.
- Not run: a live staging run. The change has not been deployed.

## Risks

- Behavior change: an ordinary task reply that recovery keeps refusing
now gets a recorded failure and a task activity after about 12 minutes.
Before, it was dropped silently. A persistent integrity decline (for
example a digest mismatch) now becomes visible this way. This is
intended.
- The status decision still commits before the reply. This PR does not
hold `done` on the comment. It makes a missing reply visible with an
owner.
- Agent Chat conversations and yielded chat responses keep their
existing behavior and their completion outbox owner.
- No schema migration. The receipt lives in the existing `result_json`
column.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 01:19:04 -06:00
DottaandClaude Opus 5.5 037c58bb22 fix(daytona): keep the provider auto-stop strictly after the warm runner idle window (#19199)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A warm remote agent keeps its sandbox running after a turn. The host
keeps a warm runner for an idle window (`runnerIdleTimeoutMs`, or the
agent's `idleTimeoutMs`)
> - The Daytona provider also stops an idle sandbox after
`autoStopInterval` minutes. The default is 15 minutes
> - On staging, the warm idle window was 900,000 ms (15 minutes). The
two windows were equal
> - A follow-up turn at the end of the warm window resumed a sandbox
that Daytona was stopping. Daytona refused the resume in about 4
seconds, and the run failed at setup
> - #19195 added a bounded retry for that refusal. It did not remove the
cause
> - This pull request makes the two windows non-overlapping by
construction. Daytona derives its auto-stop for a warm reusable sandbox
from the same warm idle value that the host uses
> - The benefit is that a follow-up inside the warm window always finds
a running sandbox, and idle sandboxes still stop

## Linked Issues or Issue Description

Refs #19195 (bounded retry for a refused resume; this change removes the
common cause).

**What happened?**

On a staging soak, three follow-up runs failed at setup with a refused
sandbox resume. Each resume began 15 minutes 8 to 25 seconds after the
previous turn released the sandbox. The warm idle timeout was 900,000
ms. Daytona's default `autoStopInterval` is 15 minutes. Nothing in the
code kept these two values apart.

**Expected behavior**

A reusable sandbox that keeps a warm runner must not auto-stop inside
the warm idle window or at its edge. The provider backstop must fire
after the host's own idle handling, with a margin. Idle sandboxes must
still stop.

**Steps to reproduce**

1. Configure a Daytona sandbox environment with `reuseLease` and a warm
runner with `runnerIdleTimeoutMs: 900000` (or an agent with
`lifecycleMode: warm`, `idleTimeoutMs: 900000`).
2. Run a turn. Do not set `autoStopInterval`.
3. Before this change: the sandbox is created with `autoStopInterval:
15`, equal to the warm window. A follow-up near minute 15 races the
provider stop.

**Paperclip version or commit**

`c656b95b6b` (current `master`).

## What Changed

- Daytona plugin (`plugin.ts`): add `effectiveAutoStopIntervalMinutes`.
For a reusable lease that keeps a warm runner, the effective auto-stop
is at least `ceil(warm idle / 1 min) + 5` minutes. The plugin applies
the floor only when the host forwards `warmRunnerIdleTimeoutMs`, or when
the environment itself selects `runnerLifecycleMode: warm`. An explicit
`0` (disabled), a longer configured value, `inherit` with a per-turn
agent, per-turn, and non-reusable sandboxes keep the configured value.
- Daytona plugin: use the effective value at sandbox creation. On
resume, raise a shorter auto-stop on an existing sandbox. This update is
best effort and bounded to 5 seconds; the #19195 retry still covers a
refusal. The stale-handle refresh threshold keeps the configured
interval, so a cached handle is not trusted for longer before the
provider confirms the new interval. The warm window is not written to
the saved normalized config.
- Host (`runner-environment-lifecycle.ts`): add
`resolveWarmRunnerIdleTimeoutMsForRun`. It returns the run's effective
warm idle window (environment `runnerIdleTimeoutMs`, else the agent
`idleTimeoutMs`) for a warm `paperclip_runner` sandbox run. It shares
the lifecycle-mode resolution with `resolveRunnerEnvironmentForRun`.
- Host (`environment-run-orchestrator.ts`, `environment-runtime.ts`):
pass that value as `warmRunnerIdleTimeoutMs` to the sandbox-provider
driver on acquisition and on remote recovery resume. The driver adds it
to the provider worker config only. Lease identity, reuse matching, and
lease metadata are unchanged.
- Daytona manifest: describe the warm floor on `autoStopInterval`.
- `doc/execution-semantics.md`: document the contract.

## Verification

- Red (before the fix, same tests, with the floor disabled): 5 failures
in `plugin.test.ts` "warm idle window versus Daytona auto-stop". The
staging case failed with `expected 900000 to be greater than or equal to
1200000`. The resume case failed because `setAutostopInterval` was not
called.
- Red (host): the new `environment-runtime.test.ts` case first failed
because the provider worker config had no `runnerIdleTimeoutMs`. The
service wrapper dropped the field. The fix forwards it.
- Green: `npx vitest run --config vitest.config.ts` in
`packages/plugins/sandbox-providers/daytona`: 563 passed. This includes
a setter that never settles (resume completes after the 5-second bound)
and `inherit` with no forwarded window (explicit `autoStopInterval: 2`
stays 2).
- Green: `npx vitest run
src/services/runner-environment-lifecycle.test.ts
src/__tests__/environment-runtime.test.ts
src/__tests__/environment-run-orchestrator.test.ts` in `server/`: 242
passed. This includes recovery resume forwarding the admitted warm
window.
- The regression tests assert the relationship `autoStop >= warm idle +
margin` for several warm windows (125 s, 300 s default, 900 s, 1,800 s)
and for an explicit `autoStopInterval: 15`. They are not constant
checks.
- `node scripts/typecheck-low-memory.mjs` passes. `pnpm typecheck` in
the Daytona package passes.

## Risks

- A warm reusable Daytona sandbox now stays running up to the warm
window plus 5 minutes after its last activity, instead of 15 minutes.
With a 15-minute window this is 20 minutes. Cost is bounded and idle
sandboxes still stop.
- The resume-time `setAutostopInterval` call is best effort. If it
fails, the old interval stays and the #19195 retry still applies.
- Other providers are unchanged. They receive one extra host-owned
config key (`warmRunnerIdleTimeoutMs`) only for warm `paperclip_runner`
runs, and they ignore unknown keys.
- No lease identity change, so existing retained sandboxes are still
reused.

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 01:10:19 -06:00
DottaandClaude Opus 5.5 c656b95b6b fix(environments): retry a refused sandbox resume with the bounded bootstrap budget (#19195)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A warm remote agent keeps its sandbox after a turn, so a follow-up
turn resumes the same sandbox and its files
> - The sandbox provider auto-stops an idle sandbox (Daytona: 15 minutes
by default). The warm native idle window is also 15 minutes
> - A follow-up that reaches the sandbox during that stop gets a
provider refusal. The host keeps the sandbox ("the lease was preserved
and no replacement was created"), which is correct
> - But setup then records a generic `setup_failed`. No retry is
scheduled, no cause is recorded, and the reply that woke the agent is
effectively dropped
> - This pull request gives that refusal its own failure code and routes
it through the existing bounded bootstrap retry. The retry resumes the
same recorded sandbox
> - The benefit is that a follow-up continues a few seconds later on the
same saved sandbox, instead of stopping the task with no next action

## Linked Issues or Issue Description

Refs #18852 (the bounded bootstrap retry that this change reuses).
Related: #19152 and #19187 (copy-back causes), #19178 (Daytona
errored-sandbox recovery before stop-only cleanup), #19182 and #19180
(recovery holds). This change does not overlap with them: they cover
cleanup, copy-back, and recovery after a run. This change covers a
resume refusal before provider work starts.

**What happened?**

On a staging soak, two follow-up turns failed at setup after a
successful first turn:

- A board reply woke the agent. The run failed after 4.9 seconds with
`setup_failed`: `Reusable sandbox lease "…" could not be resumed; the
lease was preserved and no replacement was created.` The issue stayed in
review with no recovery action. The reply was not delivered.
- An earlier run on another task failed with the same message.

Both resume attempts started 15 minutes 8–25 seconds after the previous
turn released the sandbox. The first turns used `keep_running` with a
warm idle timeout of 900,000 ms. The Daytona default `autoStopInterval`
is also 15 minutes. So the resume reached the sandbox while the provider
was stopping it. The provider refused within seconds; this was not a
long resume timeout. The provider cause was not recorded, so this timing
is the strongest evidence. The exact provider state remains an
inference.

**Expected behavior**

A refused resume before provider dispatch is a temporary infrastructure
condition. The host retries it with a small, bounded budget. Each retry
resumes the same recorded sandbox. A refusal does not prove that the
sandbox is lost, so the host never destroys it or creates a replacement.
Only an explicit provider not-found or a workspace identity mismatch
with a verified backup permits replacement (unchanged). The run records
why the provider refused, without provider text.

**Steps to reproduce**

1. Run a warm remote turn that keeps its sandbox.
2. Make the provider's `environmentResumeLease` throw a non-transient
error (for example, a 400/409 while the sandbox stops).
3. Wake the agent with a reply.
4. Before this change: the run fails with `setup_failed`, no retry is
scheduled, and the error has no cause.

**Paperclip version or commit**

Seen on staging at `5b99bdae`. The touched code is unchanged on current
`master` (`f7a4c5678f`).

## What Changed

- Move `ReusableSandboxResumeError` to
`server/src/services/reusable-sandbox-resume-error.ts` (re-exported from
`environment-runtime.ts`). Give it the code
`reusable_sandbox_resume_failed`.
- Add the provider refusal to the error message by closed labels only:
`(cause=<label> status=<http> code=<errno>)`. The labels come from the
existing transport classifier, now exported as
`sandboxProviderFailureLabel`. Provider text, paths, and IDs are never
copied.
- Carry the provider status across the plugin worker RPC. A JSON-RPC
error has only a message and a numeric RPC code. The worker host now
attaches a validated `paperclip/environment-resume-error/v1` envelope
(HTTP status and an allowlisted errno only) to `environmentResumeLease`
failures. The host re-validates it and adds it to the recorded cause.
- In setup-failure settlement, find this error through the orchestrator
wrapper. Record `reusable_sandbox_resume_failed` only when provider
dispatch did not start and the run has no native session or runner
identity (the same guard as `remote_workspace_prepare_timeout`).
- Add the code to `isTransientWorkspacePreparationCode`. The existing
scheduler then gives two retries (30 s, 60 s) with its issue gate, Stop,
pause, and dependency checks.
- Exclude the code from generic continuation recovery and from legacy
reconciliation (no unknown provider actions; bootstrap evidence proves
none started), like the template.
- Add a specific stranded-recovery notice: "Saved sandbox could not be
resumed".
- Document the contract in `doc/execution-semantics.md`.

## Verification

- Red (before the fix, same tests): 6 failures. The `fresh` case
recorded `setup_failed` and did not schedule a retry. The error had no
cause. Legacy reconciliation held the run. Generic recovery granted
another budget.
- Green: `npx vitest run src/services/heartbeat/run-completion.test.ts
src/services/legacy-execution-recovery.test.ts src/services/recovery/
src/__tests__/heartbeat-retry-scheduling.test.ts
src/__tests__/environment-runtime.test.ts
src/services/remote-workspace-transport.test.ts
src/services/heartbeat/retries.test.ts` (in `server/`): 619 tests pass.
Also `src/services/recovery/stranded-notice` and
`src/services/remote-workspace*`: 411 pass.
- Safety regressions:
- `run-completion.test.ts`: only a pre-dispatch legacy failure with no
native identity gets the code and a retry. `started`, `retained`
(native), and an untyped copy of the message stay `setup_failed` with no
retry. The recorded error contains the closed cause and not the provider
path.
- `heartbeat-retry-scheduling.test.ts` (real PostgreSQL): two concurrent
schedulers across a restart create exactly one successor per attempt.
The budget ends after two attempts. Stop, pause, and a new dependency
cancel the scheduled retry. Generic stranded recovery creates no extra
run after exhaustion.
- `packages/plugins/sdk/tests/environment-resume-error.test.ts`: through
the real worker RPC, the `JsonRpcCallError` has no status, and the
envelope gives 409 and the errno. Provider text does not cross. Other
methods get no envelope. Malformed envelopes are rejected.
- `environment-runtime.test.ts`: a throwing resume still preserves the
exact lease. The runtime does not call destroy or acquire. The resume
throws a `JsonRpcCallError` whose message has no status. The thrown
message names `cause=provider_error status=409` from the envelope, and
not the provider text.
- `node scripts/typecheck-low-memory.mjs` passes. `git diff --check`
passes.
- Not verified: a live staging run at this revision. The original tasks
were not retried. This PR is not deployed.

## Risks

- Low. The change affects only runs that fail with this typed error
before provider dispatch. Each retry resumes the same lease and inherits
the source wake context, so a reply is delivered once and no second
executor starts.
- If the provider cannot start the sandbox for longer than about 90
seconds, the two retries end and the task gets the specific recovery
notice. The sandbox and its saved files are kept.
- The error message now has a parenthetical cause before the semicolon.
No code parses this message.
- An issue in review does not get the immediate stranded notice
(unchanged existing behavior). The retry, not the notice, delivers the
reply in that case.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with
tool use (shell, file edits, browser reads of staging diagnostics).
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-05 00:20:21 -06:00
DottaandPaperclip 245b4d3b79 Preserve cancelled workspaces after controllerless Stop (#18820)
Admit cancelled workspace preservation after an audited Stop acknowledgement when no live controller received an interrupt. Retain the strict dispatched receipt for provider termination and operator handoff. Existing source, lease, coordinator and result ownership checks remain enforced.

Validated with complete Stop/preservation suites (134 tests), real-file recovery, workspace typecheck and build, and green hosted CI. Greptile and Paperclip both reviewed the exact head at 5/5 with no unresolved findings.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-11-04 22:17:50 -08:00
DottaandClaude Opus 5.5 3b17b5ce15 fix(native): keep a reassigned-away run from closing the new owner's task (#19186)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A native agent run reports "done" in an accepted result. A finalizer
then decides the task status.
> - The board can reassign a task while the run is in flight. The old
run is cancelled, but its accepted result still goes to the finalizer.
> - The finalizer did not check who owns the task now. The old owner's
"done" closed the task of the new owner.
> - The new owner never did the work, and the task shows done with no
reply from the current owner.
> - This pull request makes a done report apply only when the reporting
run's agent still owns the task.
> - The benefit is that reassignment keeps its meaning: the new owner
keeps the open task, and the old result stays as history.

## Linked Issues or Issue Description

**What happened?**

On a hosted staging instance, agent A started a native run on a task.
Agent A finished the work and submitted a "done" result. Then the board
reassigned the task to agent B, and Paperclip cancelled agent A's run
with `issue_reassigned`. About 45 seconds later, the native finalizer
committed agent A's done decision with reason
`completion_claim_policy_accepted`. The task became done. Agent B's runs
were then cancelled because the task was terminal. Agent B never worked
on the task.

**Expected behavior**

A done report from a run that no longer owns the task must not close the
task. The accepted result stays as history. The task status stays as it
is for the current owner.

**Steps to reproduce**

1. Assign a low-risk native task to agent A and start a run.
2. Submit a done result for the run.
3. Reassign the task to agent B before finalization.
4. Finalize the run. Before this change, the task becomes done.

**Paperclip version or commit**

Observed on staging at `b9fbbf8d30`. Reproduced on `master` at
`06826915d7`.

Related: #19130 adds a publication check for requested documents at
finalization. It does not check task ownership.

## What Changed

- `status-arbiter.ts`: add `ownsNativeTaskCompletion` and
`supersedeStaleOwnerCompletion`. A done decision from an agent that is
not the current assignee (another agent, a user, or nobody) becomes
`preserve` with reason `completion_owner_superseded` and no effects.
Other decisions do not change. This is the same ownership rule that the
provider-failure path already uses (`ownsProviderFailureDecision`).
- `native-run-finalizer.ts`: apply the rule after arbitration and before
the deliverable check. Conversation tasks keep their existing path.
- `native-status-reassessment.ts`: apply the same rule, so new evidence
from a reassigned-away run cannot close the task later.
- The status committer does not change. A reassignment during
finalization already increments `statusVersion` (migration 0358). The
committer then replans, and the retry applies the new rule.
- `doc/execution-semantics.md`: describe the rule next to the existing
"a late finalizer cannot change a reassigned task" statement.

## Verification

New file
`server/src/services/native-runtime/native-stale-owner-completion.test.ts`.
It uses embedded PostgreSQL, the real coordinator store and the real
finalizer.

- Control: the owning run's done result closes the task with
`completion_claim_policy_accepted`.
- Reassigned before finalization: the task stays `in_progress` with
agent B. The decision is `completion_owner_superseded`. The accepted
result row is unchanged.
- Reassigned after arbitration and before the status commit: the commit
replans and the task stays `in_progress`.
- Reassessment control: later approved evidence closes the owner's task
with `decision_superseded_by_new_evidence`.
- Reassessment after reassignment: later approved evidence from the old
run does not close the task. The decision is
`completion_owner_superseded`. This case fails when the reassessment
rule is disabled.
- Pure policy cases: another agent, a board user and no assignee all
preserve status. A non-done decision passes through.

Red before the fix (same tests, fix not applied):

```sh
cd server && npx vitest run src/services/native-runtime/native-stale-owner-completion.test.ts
# Tests 2 failed | 1 passed — both reassignment cases: expected "in_progress", received "done"
```

Green after the fix:

```sh
cd server && npx vitest run src/services/native-runtime/native-stale-owner-completion.test.ts
# Tests 10 passed

cd server && npx vitest run src/services/native-runtime/paperclip-control-plane-port.test.ts \
  src/services/native-runtime/native-completion-feedback.test.ts src/services/native-runtime/native-deliverable-feedback.test.ts \
  src/services/native-runtime/native-skill-test-status.test.ts src/services/native-runtime/native-routine-status.test.ts \
  src/__tests__/native-status-arbiter-corpus.test.ts src/__tests__/native-status-reassessment-work.test.ts \
  src/services/native-runtime/agent-chat-response-recovery.test.ts src/__tests__/native-run-projection-work.test.ts \
  src/services/native-runtime/task-reassignment.test.ts src/services/native-runtime/native-provider-failure.integration.test.ts
# 11 files, 614 tests passed
```

I did not run the full `pnpm test:run`, `pnpm -r typecheck` or `pnpm
build` locally. CI runs them.

## Risks

- A native run that reassigns its own task to a user or to another
agent, and then reports done, no longer closes the task through
arbitration. The status that the agent set through the API stays. This
matches the existing provider-failure ownership rule.
- A native run on a task with no assignee cannot close it through
arbitration. Native task runs start from an assignment, so this is not
expected in normal use.
- Review runs and conversation tasks keep their existing paths. Review
outcomes already preserve status before this rule.
- No schema change and no migration.

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

## Model Used

- Provider: Anthropic. Model: Claude Opus 5.5 (`claude-opus-5-5`), in
the Claude Code agent harness.
- Capabilities used: tool use, shell and code execution, repository
search, browser reads of staging, and extended reasoning.
- The model wrote the change, the tests and this description. A human
requested the fix.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 00:02:06 -06:00
DottaandClaude Opus 5.5 f1516e8009 fix(native): stop a session.open model rejection instead of a doomed bootstrap retry (#19190)
## Thinking Path

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

## Linked Issues or Issue Description

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

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

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

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

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

## What Changed

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

## Verification

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

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

Green after the fix:

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

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

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

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

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

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

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

## Risks

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

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

## Model Used

- Claude Opus 5.5 (Anthropic), model ID `claude-opus-5-5`, used in
Claude Code with tool use (shell, file edits, browser reads of the
staging API). It did the investigation, code change, tests and PR text.

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 23:24:34 -06:00
DottaandClaude Opus 5.5 46a50a3bdd fix(recovery): release a recovered workspace hold for the new assignee after reassignment (#19184)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - When a board user reassigns a task in the middle of a run, Paperclip
stops the old run and saves its sandbox files (copy-back).
> - If copy-back fails at the stop, Paperclip records a
file-preservation hold for the stopped run. The hold blocks all
execution on the task until the files are recovered.
> - The new assignee's wakes were already queued. The final dispatch
gate saw the hold and cancelled them.
> - Automatic recovery then saved the files. But the hold stayed
effective for the new assignee: explicit continuation accepts only the
stopped run's own agent, so no message, Retry or stranded-work sweep
could start the new assignee.
> - The task stayed `in_progress` with no run, no wake and no active
recovery.
> - This pull request releases a recovered file-preservation hold for a
different current owner.
> - The benefit is that a reassigned task continues with its new owner
after its files are safe, with no manual action.

## Linked Issues or Issue Description

**What happened?**
A board user reassigned a running native task from agent A to agent B
and posted a comment. A's run ended `cancelled` / `issue_reassigned`.
Copy-back was still in progress, so B's first run was cancelled
`workspace_busy`. Paperclip then recorded a resolved no-replay hold
(`native_continuation_requires_reconciliation`, fingerprint
`native-workspace:<A run>`). The final dispatch gate cancelled B's two
queued runs with `execution_reconciliation_required` ("Recovering saved
work…"). Automatic recovery then saved the files
(`workspaceRecovery.status = recovered`, `workspaceRepairRequired:
false`). The blocker still said "Saved work recovered. Send a message to
continue." with `canContinue: false`. The task stayed `in_progress` with
no run for hours. Two hosted reproductions had the same shape. A sibling
reassignment without a copy-back failure recovered normally.

**Expected behavior**
After the saved-file receipt verifies, the new assignee continues. The
hold must still block every executor while the files are not recovered.
The stopped run's own agent keeps its no-replay hold.

**Steps to reproduce**
1. Start a native run on a task with a retained sandbox.
2. Reassign the task to another agent while the run executes, so that
copy-back records a `workspaceRestoreRecovery` hold.
3. Let automatic workspace recovery finish.
4. Before this change, `getExecutionBlocker` still returns the hold for
the new assignee, and the stranded-work sweep queues nothing.

**Paperclip version or commit**
`b9fbbf8d30` (also seen on `818d2bb6`).

**Deployment mode**
Hosted, PostgreSQL. The test uses embedded PostgreSQL.

Related open pull requests (checked; none covers this path): #19177
(board reconciliation of sibling holds), #19169 (disposition repairs
behind a cancelled blocker), #19143 (park a workspace-busy wait), #19149
(interrupt stop identity).

## What Changed

- New `restoreHoldReleasedForCurrentOwnerPredicate`
(`server/src/services/execution-blocker.ts`) is a SQL predicate. It
matches a file-preservation hold (`workspaceRestoreRecovery`, not
`restore_unsafe_archive`) when all of these are true:
  - the stopped run is terminal and finished;
- the task has an agent assignee, and that agent is not the stopped
run's agent;
  - the stopped run's agent is not the current review participant;
- `completedWorkspaceRecoveryCondition` is true for the hold: completed
system recovery, `workspaceRecovery.status = recovered`, succeeded
preservation operations bound to this hold, and the `workspace_finalize`
receipt;
- every saved operation is a copyback into the task workspace
(`workspace_recovery`). A snapshot-only save keeps the hold, because the
new owner's ordinary turn does not attach snapshots.
- The predicate is wrapped in `coalesce(..., false)`. Its negation
therefore cannot hide an unrelated hold through NULL JSON reads.
- The proof is the durable completed-recovery receipt. It does not read
live workspace authority (epoch, writer, snapshot), so reuse of the
workspace by the new owner cannot re-arm the hold.
- `getExecutionBlocker` excludes matching holds from both the
file-preservation query and the general blocker query. There is no scan
limit and no extra query per hold.
- `admitExplicitNativeContinuation` excludes the same holds from its
source checks. A released hold therefore cannot reject a new owner's
message with "owner changed".
- `completedWorkspaceRecoveryCondition` also accepts a SQL expression
for the run id. Its behavior is unchanged.
- The hold row, its `automaticRecovery.replay = "blocked"` evidence and
its status are not changed. If the task returns to the stopped run's
agent, the hold applies again.
- `doc/execution-semantics.md` describes the rule next to the existing
"Reassignment does not discard the stopped run's files" sentence.

## Verification

New test in `server/src/services/workspace-recovery.test.ts`: `hands a
reassigned task to its new assignee once the previous owner's files are
recovered`. It uses the real retained-workspace recovery, embedded
PostgreSQL and the real stranded-work sweep. It builds the incident
shape: a cancelled `issue_reassigned` source run, a resolved no-replay
hold, a new assignee, and that assignee's gate-cancelled run.

- Before recovery, the blocker holds the new owner
(`workspaceRepairRequired: true`).
- After recovery, the blocker is `null`. The sweep creates exactly one
pending turn for the new owner. That turn is not a retry of the stopped
run. A second sweep adds no turn.
- The hold row keeps `status: resolved`, `outcome: blocked` and `replay:
blocked`.
- Live repair evidence on the stopped run is then changed, so
`hasVerifiedWorkspaceRepair` returns false (as after a remote reuse).
The blocker stays `null`.
- After a reassignment back to the stopped run's agent, the hold applies
again.
- An unrelated reconciliation hold for the same stopped run, without
file evidence, still blocks the new owner.
- New test `keeps a snapshot-only recovery held for a new assignee`: the
snapshot-only save does not release the hold. This test fails against
the first revision of this pull request.

Red before the fix (test kept, `execution-blocker.ts` restored from
`master`):

```
cd server && npx vitest run src/services/workspace-recovery.test.ts -t "hands a reassigned task"
AssertionError: expected { …(13) } to be null   (blocker: "Saved work recovered. Send a message to continue.", canContinue: false)
```

With only the blocker assertion removed, the sweep also failed:
`expected [] to have a length of 1 but got +0`.

Green after the fix: 1 passed.

Related suites, all pass (991 tests): `workspace-recovery`,
`execution-blocker-read`, `legacy-explicit-continuation-recovery`,
`workspace-restore-reconciliation`, `native-failed-workspace-recovery`,
`workspace-restore-repair-route`, `execution-projection-recovery`,
`heartbeat-undispatched-execution-wait`,
`issue-update-comment-wakeup-routes`,
`heartbeat-observed-workspace-preservation`,
`native-workspace-reconstruction`, `explicit-native-continuation`.

Not run: a live run on a deployed instance. The hosted incident tasks
are not recovered by this pull request until it is deployed.

## Risks

- A new owner can start after the files are recovered, while the stopped
run's action outcomes stay unknown. This matches the existing rule for
holds without a workspace: Paperclip does not create those holds for a
reassigned task. The new owner gets a fresh turn, not a replay.
- The hold still blocks every executor until the exact receipt verifies,
so there is no second executor on the workspace during copy-back.
- Other execution holds (non-workspace holds recorded before a
reassignment) keep their current effect. This change does not alter
them.
- A reassignment after a snapshot-only save still needs a board action,
as before. That path is not changed here.
- The release is one correlated `exists` in the existing blocker
queries. Its subqueries use primary keys and the run and issue indexes.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 23:23:21 -06:00
Nicky Leach 8c062fa481 fix(native): deliver plugin VM Runner usage to Core (#19162) 2026-11-04 21:02:22 -08:00
DottaandClaude Opus 5.5 512fbea0bb fix(native): deliver messages saved while a native run finalizes (#19183)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A user can reply on a task while the agent's run is still finishing
> - A remote native run first commits its result and its status decision
(for example `done`). Then its sandbox environment settles (harness
backup, warm retention).
> - A message that arrives after the final turn took its input is saved
as a deferred receipt behind that run
> - The saved-message recovery path holds the receipt while the source
run still owns its environment lease or its workspace copy-back owner
> - Nothing notified that path when the lease or the owner settled, so
the messages waited for an unrelated company event
> - This pull request notifies the existing deferred-receipt work when a
run's environment lease settles or its workspace finalization owner is
released
> - The benefit is that messages sent while a run finalizes are
delivered once, soon after the run settles, and a task closed in that
window is reopened for them

## Linked Issues or Issue Description

**What happened?**
A user posted three plain comments on a task while the agent's remote
native run was finalizing. The final turn had already called its finish
tool. The comments were saved in one deferred receipt. The run then
committed `done` and its status became `succeeded`. No follow-up run
started. The task stayed `done` with three unanswered requests. About 20
minutes later, an unrelated event in the same company woke the
deferred-receipt worker, and the messages were delivered.

**Expected behavior**
A message that arrives after the last turn took its input, but before
the run releases, gets one follow-up turn as soon as the run settles. If
finalization marked the task `done` in that window, the task is reopened
for the message. Messages that the finished turn already consumed are
not replayed.

**Steps to reproduce**
1. Start a native run with a remote (sandbox) environment.
2. After the agent calls its finish tool, post a user comment on the
task.
3. Let finalization commit `done` and project the run as `succeeded`.
4. Observe that the comment's deferred receipt stays
`deferred_issue_execution` after the environment lease settles.

**Root cause**
A remote native run is finished by native finalization, which projects
the terminal status. The in-process run completion
(`releaseIssueExecutionAndPromote`) does not run for it. Its saved
messages depend on the stranded-queue sweep in
`resumeDeferredExecutionReceipts`, which calls the release with
`resumeStrandedQueue`. That release correctly waits while
`hasPendingRunCleanup` is true (an unsettled environment lease or a
workspace finalization owner). Finalization notifies the
deferred-receipt worker when it projects the run, but the cleanup is
still pending at that time. The worker then drops the entry. Lease
settlement and owner release did not notify the worker again.

Refs #19122 (saves a redirect sent while a stopped run settles; this
change reuses the same deferred-receipt delivery path for normally
completing runs).

## What Changed

- `environmentService.releaseLease` notifies deferred-execution work for
the company when a run's lease leaves `active` or `pending_cleanup`
(released, retained, or failed).
- `environmentService.releaseLeasesForRun` notifies deferred-execution
work in the same way.
- `withNativeWorkspaceFinalizationOwnership` notifies deferred-execution
work when it releases its owner.
- Delivery uses the existing path: `resumeDeferredExecutionReceipts` →
stranded-queue release → deferred wake promotion (with the existing
reopen rule for user messages). No new delivery mechanism.
- New regression in `deferred-execution-receipt-work.test.ts` with the
real deferred-receipt worker, the real release adapter, and the real
lease service.
- `doc/execution-semantics.md`: one additive paragraph for messages sent
while a run finalizes.

## Verification

- Staging evidence (read-only): the task's saved queue held the three
comments as one `deferred_issue_execution` receipt after the run
succeeded. The run had no `run succeeded` lifecycle event (it was
finished by native finalization, not run completion). The receipt was
promoted about 20 minutes later with `reopenedFrom: done` by the same
recovery path.
- Red before the fix: the new test failed with `expected null to be
truthy` (the receipt stayed deferred with no run after the lease
settled).
- Green after the fix: `cd server && npx vitest run
src/__tests__/deferred-execution-receipt-work.test.ts -t "finishing
native run"`.
- Safety cases in the new test:
- The receipt stays deferred while the environment lease is active
(cleanup holds still apply).
- After settlement, exactly one run carries both messages in order, and
the done task is reopened once.
  - A second settlement signal does not deliver the messages again.
- Related suites: `deferred-execution-receipt-work`,
`environment-service`, `environment-cleanup-work`,
`native-environment-settlement-work`, `idle-sleep-safety`.
- Not done: a live staging reproduction at the fixed revision.
Deployment is out of scope.

## Risks

- The deferred-receipt worker reloads the company's deferred receipts
after each run lease settlement. Each reload is bounded and runs only
when deferred receipts exist. The notification is transactional, so a
rolled-back release does not wake the worker.
- Low risk: no schema change, no API change, and no change to admission
or reopen rules.

## Model Used

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

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 22:26:42 -06:00
DottaandClaude Opus 5.5 06826915d7 fix(dispatch): park a workspace-busy wait instead of recreating runs (#19143)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Child tasks can reuse their parent's execution workspace. Only one
run at a time may write in a shared sandbox workspace.
> - A run that finds the workspace held by another live run waits with a
`workspace_busy` scheduled retry.
> - That wait cancelled the run and created a new retry run every 60–120
seconds, until the holder ended. Each check made one cancelled run
record.
> - Three sibling tasks that waited for a parent made 15 cancelled runs
in 13 minutes. They also got the workspace in random order, not in
arrival order.
> - This pull request keeps the waiting run as one parked retry while
its holder is live, and wakes it when the holder ends.
> - The benefit is a clean run history, a prompt handoff, and fair
arrival-order access to a shared workspace.

## Linked Issues or Issue Description

Refs #18691 (the workspace wait for a live holder). Refs #19075 (sends a
stopped writer with a pending save into the same wait). Refs #12587 (an
earlier proposal about fairness for this retry).

**What happened?**
A parent task delegated three subtasks that inherit its execution
workspace. Each subtask run was created, then cancelled with
`workspace_busy` ("Shared execution workspace is busy: run … is still
running"), then created again about every 1.5–2 minutes. One subtask got
8 cancelled runs, one got 5, and one got 2. Every cancellation named a
holder that was really running: first the parent run, then the sibling
that got the workspace. So serialization was correct. The cost was the
churn of run records, and the subtask that waited first ran last.

**Expected behavior**
A run that needs a busy shared workspace waits as one durable record. It
continues when the holder finishes. Siblings that wait for one workspace
get it one at a time, in arrival order. Two writers never share the
workspace, and the user can still stop a waiting run.

**Steps to reproduce**
1. Bind two or more issues to one shared execution workspace in a
sandbox environment.
2. Keep a run live on one issue (the holder).
3. Wake the other issues. Before this change, each due check promotes
the retry, cancels it with `workspace_busy`, and creates a new retry
run. After this change, the retry stays parked until the holder ends.

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

## What Changed

- `finalizeWorkspaceBusyDeferral` records
`contextSnapshot.workspaceBusyWait` = `{ key, holderRunId, waitingSince
}`. The key is `execution:<id>` or `project:<id>`. `waitingSince`
carries over when the run defers again for the same workspace.
- New `server/src/modules/run-dispatch/adapters/workspace-busy-wait.ts`:
- `parkWorkspaceBusyRetryInTx` runs in the scheduled-retry promotion
transaction, after the existing gate allows the retry. If the recorded
holder is still running and is not stale, or an earlier waiter for the
same workspace is still scheduled, queued, or running, the retry stays
`scheduled_retry`. Only its deadline moves. It does not create a run, an
event, or a wake. The fallback deadline is 5 minutes, and never later
than the holder's one-hour stale point.
- `wakeWorkspaceBusyWaitersInTx` runs in the transaction that ends a
running run (`writeRunStatus`). It makes due the retries that wait for
that run, and the retries that wait for the same workspace as that run.
It registers the scheduled-retry dispatch notification before the write.
- `WORKSPACE_BUSY_HOLDER_STALE_AFTER_MS` moves to the run-dispatch
domain, so the deferral gate and the parked check use one value.
`heartbeat/retries.ts` exports it again.
- A retry without a recorded wait (for example, a retry from before this
change) keeps the old behavior.
- `doc/execution-semantics.md` describes the parked wait.

## Verification

- Red before the fix, green after
(`server/src/__tests__/heartbeat-workspace-busy.test.ts`, "reused shared
execution workspace in a sandbox"):
- "waits on a live holder in one parked run, then runs once when the
holder ends": before, the retry was promoted while the holder was live.
After, it stays parked through due checks at +1 s, +10 min, and +30 min.
Ending the holder makes it due at once. It runs exactly once, and there
is only one `workspace_busy` cancellation.
- "hands the workspace to three waiting siblings in arrival order, one
writer at a time": before, nothing woke the waiters when the holder
ended. After, exactly one waiter is admitted per handoff, in arrival
order. Each runs once, and there are 3 cancellations in total (one first
deferral for each sibling).
- "still lets the user stop a parked waiter": before, the waiter was
promoted while the holder was live. After, Stop cancels the parked
retry, and nothing brings it back.
- Safety case (green before and after): "lets a stale holder reach the
existing quiet-holder failure instead of parking forever".
- `npx vitest run src/__tests__/heartbeat-workspace-busy.test.ts`: 35
passed.
- `heartbeat-remote-workspace-busy`, `heartbeat-retry-scheduling`,
`src/modules/run-dispatch`, `heartbeat/retries.test.ts`,
`execution-projection.test.ts`: 266 of 267 passed. The one failure,
"records pre-provider quota rejection…", is a timing flake on a loaded
machine and passes when it runs alone. It does not use this path.
- `tsc --noEmit` reports no errors in the changed files.
- Staging evidence was read only. This change is not deployed.

## Risks

- One extra indexed `SELECT … FOR UPDATE` on `scheduled_retry` rows when
a running run ends. The update and the notification happen only when
waiters exist.
- If an earlier waiter stays queued for a long time (for example, its
agent is paused), later waiters for the same workspace wait behind it.
Each check is a deadline update, not a run. The fallback check runs
every 5 minutes.
- A remote writer that has ended but has not finished its save (#19075)
is not parked, because its run is no longer `running`. Such a wait can
still make a small number of deferrals until the save settles.
- Low migration risk: no schema change. Old retries without the new
context key keep the old behavior.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 22:01:37 -06:00
Devin FoleyandPaperclip 5fdfd3f17a fix(recovery): bound repeated saved-record response bodies (#19168)
## Thinking Path

> - Paperclip manages agent tasks and their saved execution history.
> - A continuation carries completed API receipts to prevent duplicate
work.
> - Repeated saves can copy large record bodies into each new provider
prompt.
> - The resulting command can exceed the durable PRP frame limit before
useful work starts.
> - This change projects large successful saved-record responses as
compact references.
> - The original receipts and authenticated user direction remain
available.

## Linked Issues or Issue Description

**What happened?**

A task with many saved document and comment responses can fail with
`command_payload_too_large`. Same-run recovery can then show a runner
identity failure that hides this first cause. Completed response bodies
repeat content already stored in task records.

**Expected behavior**

Continue from saved records with completed-action evidence that fits the
runner command. Keep user messages, answers and approvals intact. Keep
the full original receipts.

**Steps to reproduce**

1. Save 39 successful document, comment and task-write responses in a
run's API receipt ledger.
2. Give each response a large record body.
3. Build and render a continuation, then queue it through the real
durable PRP journal.
4. Before this fix, the regression throws `command_payload_too_large`
with a 4.2 MB prompt.

Related work: #16980 pages oversized assigned tool results. This change
addresses earlier response bodies repeated in a later continuation. It
does not change tool-result paging or protocol limits.

## What Changed

- Project response data over 4 KiB for three known successful
record-write operations. Keep record, revision and status metadata, HTTP
outcome, receipt identity and a data digest.
- Keep small, failed, unknown, mismatched and foreign-company results in
their existing form.
- Explain the reference format in shared prompt guidance and the
execution contract.
- Add database and real-journal regression coverage. Verify the original
receipt ledger remains unchanged.

The continuation service owns the projection. The shared adapter helper
only explains the reference to native and legacy prompt consumers. There
are no new dependencies, provider calls, timers or background scans.

## Verification

- The unfixed database regression fails at the real PRP command limit
with `command_payload_too_large`.
- Continuation builder, native execution input and native continuation
suites: 118 tests pass.
- Shared prompt consumers: 142 tests pass.
- Module boundaries and diff whitespace checks pass.
- Greptile: 5/5 on `8109e7361c5b5e38f9aad35af9bf4ad7bdc5f338`, with no
unresolved review threads. All CI checks pass.
- Full workspace `pnpm -r typecheck` and `pnpm build`: passed.
- Hosted verification: pending deployment. The source fix does not
itself resume the affected hosted task.

Local setup failures remain accounted for: the fresh checkout lacked an
ignored root SDK link and embedded PostgreSQL library symlinks after an
offline install that skipped lifecycle scripts. The maintained
workspace-link helper, SDK build and platform hydration script repaired
those prerequisites. A later test expectation assumed JSON insertion
order across PostgreSQL JSONB; it now checks the digest against the
unchanged stored receipt. These failures did not change runtime limits
or recovery guards.

## Risks

Large responses from these three successful operations now use explicit
saved-record references in continuation context. Agents must read the
record or source receipt when full content is needed. This is not a
universal prompt-size limit: user history, unknown operations and other
inputs retain their existing representation. No migration, permission,
credential, provider-stop or replay policy changes.

## Model Used

OpenAI GPT-6.1 Sol (`gpt-6.1-sol`), xhigh reasoning, through Codex in T3
Code. Used for investigation, implementation and local verification. No
subagents.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either linked existing issues or described the issue in-PR
following the issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links
- [x] My branch name describes the change and contains no internal task
id
- [x] I have run the targeted tests locally and they pass
- [x] I have added tests and updated relevant documentation
- [x] I have considered and documented risks
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open findings
- [x] I will address review comments before requesting merge

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 19:01:08 -08:00
Nicky Leach 737db73275 fix(native): measure resumed run usage from the prior run's total (#19161) 2026-11-04 18:55:14 -08:00
Devin FoleyandPaperclip 94fccc3180 fix(workspaces): preserve snapshots beside runtime sockets (#19159)
## Thinking Path

> - A workspace can contain saved source files alongside transient
runtime IPC endpoints.
> - The portable archive supports files, directories and confined
symlinks; treating a Unix socket as a fatal entry prevents every file
beside it from being saved.
> - Keep the existing archive owner and validation. Record socket paths
as exclusions, leaving the source endpoint and adjacent work intact.

## Linked Issues or Issue Description

**What happened?**

Retained workspace recovery repeatedly fails with
`remote_workspace_archive_unsupported_entry` when the source tree
contains a Unix socket, preventing adjacent source files from being
saved. A real socket reproduces the error in local capture and the
serialized sandbox helper.

**Expected behavior**

Save regular workspace files, record runtime socket paths as exclusions,
and leave the source endpoints and new file edits intact.

**Steps to reproduce**

1. Create a temporary workspace with an uncommitted text file.
2. Open a Unix domain socket in that directory with a Node server.
3. Capture the workspace through the local archive function or generated
sandbox helper. Before this fix both captures fail; afterward the saved
file restores and the socket exclusion is recorded.

**Paperclip version or commit**

Reproduced on `6fa54d55779295d90c455389cebdd3da96607d20` before this
change. The socket fixture establishes this failure class; it does not
establish the entry type behind every historical unsupported-entry
occurrence.

## What Changed

- Exclude Unix domain socket endpoints from portable workspace captures
and include their paths in the existing exclusion manifest.
- Keep source sockets in place. Preserve normal files and the existing
rejection of named pipes, device entries and unsafe links.
- Exercise local and serialized capture, an unchanged source endpoint,
restoration of adjacent uncommitted work, FIFO rejection, and a socket
becoming a real file during capture. A changed source still fails safely
and retains that new file.
- Document the portable snapshot behavior.

## Verification

- Red regression: both local and serialized captures failed with
`remote_workspace_archive_unsupported_entry` before the change.
- `pnpm exec vitest run
server/src/services/remote-workspace-snapshot-archive.test.ts`: **43
passed** using real filesystem sockets, pipes, Git repositories and
archives.
- `pnpm -r typecheck` and `pnpm build`: passed.
- Module boundaries and `git diff --check`: passed. Added source and PR
text were scanned for credentials, private incident identifiers and
personal data before push.
- Snapshot storage and save-concurrency consumer suites pass **23
tests**. Their first local runs exposed missing workspace SDK build
links and unhydrated macOS embedded-Postgres library symlinks in the new
checkout. Repairing those prerequisites made the unchanged consumer
suites pass; the earlier failures remain recorded and no assertion was
weakened.
- Greptile reviewed `e313565a5088abce448f11b29982462aa338a692` at
**5/5** with no unresolved threads. All current-head CI checks are green
and the branch is conflict-free. Hosted recovery needs a qualified
deployment and original-task verification.

## Risks

Socket endpoints represent runtime IPC and are not recreated by archive
restore. Their paths are visible in the existing exclusion manifest.
This adds no archive entry type, subprocess, recurring work, provider
call or dependency. It does not alter source authorization, stop
receipts, ordinary file validation, restore safety or recovery budgets.
Unix socket/FIFO fixtures are skipped on Windows; existing portable
archive coverage still runs there.

## Model Used

OpenAI GPT-6.1 Sol (`gpt-6.1-sol`) through Codex, xhigh reasoning. Used
repository tools, code execution and real local filesystem/process
fixtures.

## Checklist

- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] Added focused behavioral and preservation regression coverage
- [x] Updated the execution contract and documented limits
- [x] Typecheck and build pass locally
- [x] Scanned the diff and description before push
- [x] Current-head CI is fully green
- [x] Greptile is 5/5 with no unresolved comments

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 18:06:59 -08:00
c2cc69d1e4 fix(recovery): resume and settle preserved native results (#19034)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native execution can save an accepted result before workspace export
fails.
> - Recover must preserve the files and settle that same result without
another agent turn.
> - The export failure can own the task's Blocked transition and its
recovery notice.
> - Recovery must prove preservation, stop, source identity and current
authority before settlement.
> - Recover can also transfer repair ownership to the system, which must
keep explicit board export repair available.
> - This change restores those paths while preserving later task state,
live owners and unknown action outcomes.

## Linked Issues or Issue Description

Related #17916 protects unfinished exports. Related #19004 preserves
deliberate Retry authority. This change repairs accepted-result
finalization and its recovery notice.

**What happened?**

A native run accepted its result, then exhausted workspace export.
Normal Recover could save the files but leave the result and old
recovery notice unsettled. A saved Retry could also use a different
generation from preservation. Separately, Recover changed the action
owner from board to system. Explicit export Retry then rejected that
same source as stale, even when its accepted result and stopped
allocation still matched.

**Expected behavior**

Preserve files, finish the original accepted result and close only its
obsolete notice after exact proof. Keep explicit board export repair
available after system adoption. Preserve current authority, Stop, later
task changes, newer execution and every physical ownership fence.

**Steps to reproduce**

1. Accept a native result and exhaust its workspace export.
2. Request ordinary Recover, optionally with an existing saved Retry.
3. Let file preservation complete. Inspect the task and source recovery
notice.
4. To reproduce the explicit-repair defect, call export Retry after
Recover adopts the action. Before this repair it rejects the valid
system-owned source before any provider call.

## What Changed

- Record the exact accepted result, contract, task status version and
prior decision when export exhaustion creates Blocked.
- Bind passive finalization to complete, source-bound preservation and
the current chat/review authority. Recheck the proof under status commit
locks.
- Keep the saved Retry identity separate from its preservation
generation. Retain coalescing and reject malformed or mismatched
history.
- Preserve an existing published GitHub assessment for the
no-publication status-unproven wait. Keep all semantic, result,
authority and publication checks.
- Close an old export notice only after its own accepted result, export
or preservation receipts, effects, projection and old Runner ownership
settle. Keep newer work and files unchanged.
- Permit explicit board export Retry after Recover transfers the exact
source to the system. Require valid system and workspace recovery
records for that same run, plus every existing result, stop, allocation,
task and shared physical-lock check. Reject missing proof, another
source and agent ownership before provider access.
- Merge current master while retaining the stronger Stop, source and
verified preservation checks.
- Update the execution contract. No schema, endpoint, Runner protocol,
model, trust or provider artifact change.

## Verification

- Current head: `9be5a73489801095c9f39c9bfcb285a22b055912`.
- The explicit-repair regression fails before the new guard change:
ordinary Recover adopts the real database action, then export Retry
rejects it as stale. It passes after the repair.
- On the merged source, three owning PostgreSQL/file suites pass **220
tests**: `preserved-native-response-wait.integration.test.ts`,
`native-workspace-export-retry.test.ts` and
`idle-completed-history.test.ts`. The export suite includes four
unrelated-owner/source rejection controls and both board-owned and
system-adopted active-exporter controls. Results and run counts prove
that repair creates no provider turn.
- Server layered typecheck, module boundaries, `git diff --check`, and
the added-line secret/private-incident scan pass.
- Full workspace typecheck (`pnpm -r typecheck`) and build (`pnpm
build`) pass. The complete local `pnpm test:run` is still running; its
completed serialized chat partitions pass. Current-head CI is green (53
successful checks, 3 expected skips). Greptile reviewed
`9be5a73489801095c9f39c9bfcb285a22b055912` at 2026-11-05 00:58 UTC with
confidence **5/5**; all review threads are resolved.
- Original qualification remains supporting evidence: the
accepted-result fixture exercises HTTP Recover, real files, the
finalizer, saved Retry coalescing, eight commit races, current
principal/resource/head changes, Stop, later task changes, notice
closure and repeated delivery. No accepted response, provider turn or
publication is replayed.
- Earlier failures remain recorded. Original red controls exposed
wrong-action preservation, unrelated finalization barriers, generation
mismatch and a lost GitHub assessment; the original repair added
controls for them. The prior CI run
[41908572248](https://github.com/paperclipai/paperclip/actions/runs/41908572248)
failed an unchanged completed-history runtime fixture because its
loopback service refused connections through the original ten-second
readiness window. That owning suite passes unchanged in the 220-case run
above. The earlier readiness failure's cause remains unproven; no wait
or assertion was weakened.
- Local and CI tests do not establish hosted task recovery. The serving
revision and original-task outcomes require separate verification after
rollout.

## Risks

- Historical failures without the exact export-owned Blocked receipt
keep their current task status. A saved result does not grant execution
or publication authority.
- Explicit export repair still resumes only the same retained allocation
under the shared physical lock. It cannot take over an active exporter,
create another agent turn, choose a replacement sandbox or discard saved
files.
- Retiring a source notice does not release physical resources or modify
a newer task owner, later Done state or saved snapshot history.
- The proof adds bounded reads only for concrete recovery obligations.
It adds no recurring worker or polling timer.

## Model Used

OpenAI GPT-6 through Codex for the original repair. OpenAI GPT-6.1 Sol
(`gpt-6.1-sol`), xhigh reasoning, for the merge, integration
verification and system-adopted export repair. Both used repository
tools, code execution and PostgreSQL tests. 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
- [x] I have checked ROADMAP.md and searched for duplicate or related
work
- [x] I have described the issue and linked related public PRs
- [x] I have included no internal task identifiers or private incident
links
- [x] I have added behavioral and safety regression tests
- [x] I have documented verification limits and risks
- [x] I have updated the execution contract
- [x] All Paperclip CI gates are green on the current head
- [x] Greptile is 5/5 on the current head with no unresolved comments
- [x] I will address review comments before requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Devin Foley <devin@paperclip.ing>
2026-11-04 17:16:17 -08:00
DottaandClaude Opus 5.5 11f2a5293d fix(native): name the proof that holds a recovered per-turn owner (#19061)
## Thinking Path

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

## Linked Issues or Issue Description

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

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

**Paperclip version or commit**

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with tool use,
shell execution, and read-only browser access to a test deployment. The
harness does not expose the context window size.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 18:50:01 -06:00
DottaandClaude Opus 5.5 5723fd82ca fix(native): release a fresh claim when task preparation fails (#19065)
## Thinking Path

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

## Linked Issues or Issue Description

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

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

## What Changed

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

## Verification

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

## Risks

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

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

## Model Used

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

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 18:25:36 -06:00
DottaandPaperclip b7ef307a65 fix(workspaces): wait for an ended writer's save instead of failing setup (#19074)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agent runs can use a remote task workspace in a sandbox. One writer
run at a time holds that workspace.
> - When a writer run ends, its workspace save and sandbox release
finish a few minutes later. The writer hold stays until then.
> - A new run that started in that interval failed with `setup_failed`:
"The workspace still has an unfinished writer or save operation."
> - A live writer already makes the new run wait (`workspace_busy`,
added in #18691). An ended writer that is still settling did not.
> - This pull request reports the ended writer as the holder too, so the
new run waits instead of failing.
> - The benefit is that work continues automatically after a short
settle interval. Users do not see a failed run and a blocked task.

## Linked Issues or Issue Description

Refs #18691

**What happened?**
A task run failed during workspace setup with `setup_failed` and the
error "The workspace still has an unfinished writer or save operation."
The previous run on the same task had ended a few seconds before. Its
sandbox lease was released 4 to 17 minutes later. A staging soak
recorded 31 of these failures over 14 tasks in about 29 hours.

**What did you expect to happen?**
The new run waits for the workspace, as it does when the previous writer
is still running. It continues when the save and release settle.

**Steps to reproduce**
1. Bind a task to a remote workspace whose authority has `dirty: true`
and a `writerRunId` for a run with status `failed` that ended now.
2. Wake the task's agent.
3. The run fails with `setup_failed`. No retry is scheduled.

## What Changed

- `reserveRemoteWorkspace` now throws `RemoteWorkspaceWriterActiveError`
for an ended writer whose save or sandbox release has not settled.
Before, it threw a plain 409 conflict. The error keeps the same message,
status and `remote_workspace_writer_pending` code. It uses the writer's
`finishedAt` as its last activity and sets `ended: true`.
- Workspace preparation already converts that error to a
`workspace_busy` deferral with the one-hour holder cutoff. It now passes
`holderEnded` to the deferral.
- `WorkspaceBusyDeferral` says "ended, but its workspace save has not
settled" for an ended holder, instead of "is still running".
- A writer that is missing or has no task still gets the plain conflict,
as before.
- `doc/execution-semantics.md` describes the wait and its cutoff.

## Verification

- Red before the fix: the new test "waits for a writer that just ended
while its save or sandbox release settles" failed. The run had `status:
failed`, `errorCode: setup_failed`, `executionRecovery.kind: bootstrap`.
This is the same result as the staging failures.
- Green after the fix:
`cd server && npx vitest run
src/__tests__/heartbeat-remote-workspace-busy.test.ts
src/services/remote-workspace-reservation.test.ts`
  (16 tests pass.)
- Safety cases in the new tests:
- The waiting run does not change the workspace authority. The unsaved
writer's files and hold stay unchanged.
- A writer that ended more than one hour ago still causes the
unfinished-writer setup failure, with no scheduled retry. The wait is
bounded.
- Existing cases still pass: live writer wait with Stop and
reassignment, the concurrent reservation race
(`remote_workspace_writer_pending`), and the unused-reservation
takeover.
- Also run: `remote-workspaces.integration.test.ts`,
`heartbeat-workspace-busy.test.ts`,
`remote-workspace-save-concurrency.test.ts`,
`workspace-recovery.test.ts`. All 6 suites pass (442 tests).
- Not done: a live run on staging at this revision. The fix is not
deployed.

## Risks

- A task whose ended writer never settles now makes up to about one hour
of short `workspace_busy` waits (one scheduled retry every 1 to 2
minutes) before it fails. Before, it failed at once. The end state after
the cutoff is the same.
- Some of the staging failures have other causes that this change does
not fix:
- A previous run that stays `running` with no output for more than one
hour still causes this failure, as intended.
- A dirty workspace whose writer ended without a saved result (for
example a workspace finalization retry that never runs) still blocks the
task after the cutoff. These need separate fixes.
- No schema, API or UI changes.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with
tool use (shell, browser reads of staging run records) and extended
reasoning.

## Checklist

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

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native Runner sessions keep one provider conversation across task
turns, so Paperclip accounts usage per run.
> - Codex can report more usage for a turn after its run's charge was
posted.
> - The Runner then marked the session `reconciliation_required`. If
another run was attached, it also failed that run. Core refused the
delivery with `native_usage_after_finalization_requires_reconciliation`.
> - That usage has already happened. Blocking the session cannot undo
it, and the debt waited for an operator while the run totals stayed
wrong.
> - Codex totals are cumulative, and the Runner already knows the owning
run in most cases. So the correct amount can be posted automatically.
> - This pull request reports the exact increment with its owner, and
posts it to the owning run as a separate cost event.
> - The benefit is that run accounting becomes correct without stopping
work, and an old run's usage can no longer fail a newer run.

## Linked Issues or Issue Description

No issue exists. I searched open and closed pull requests and issues for
"late usage", `native_usage_after_finalization_requires_reconciliation`
and `codex_usage_accounting_integrity`. I found no duplicate.

**What happened?**

Code review found this while diagnosing session recovery failures. When
Codex reported usage above the known total for a turn that was not
active, the Runner blocked the session. If that turn belonged to a
previous run, the Runner also failed the attached run with
`codex_usage_accounting_integrity`. Core kept the receipt as debt, and
could correct it automatically only if the token counts were unchanged.

**Expected behavior**

Late usage does not stop any run or session. The charge goes to the run
that used the tokens, and it is auditable.

**Steps to reproduce**

1. Finish a Codex turn on a native Runner session and attach the next
run.
2. Have Codex send `thread/tokenUsage/updated` with a higher total for
the finished turn.
3. On the unfixed code, the attached run fails and later attaches are
refused.
`late_codex_usage_requires_durable_reconciliation_before_or_after_handoff`
encoded this behavior.

**Paperclip version or commit**

`master` at `04a24f4e06`.

**Deployment mode**

Any deployment that uses a native Runner with Codex.

## What Changed

- **Runner:** late usage no longer sets `reconciliation_required` or
fails a run. For a turn that belongs to a previous run, the Runner emits
`usage.reported` with the increment in `runDelta` and the owner in
`reconciliation.priorOrigin`. `runDeltaAvailable` stays `false`, so an
older Core does not bill it to the attached run.
- **`CodexRunUsage::absorb_late_usage`:** treats only usage above the
known thread total as late, and keeps the attached run's delta
unchanged. A report at or below that total adds nothing, so the sum
across runs always equals the thread total and a replay cannot charge
twice.
- **Delivery:** `captureLateUsageForOwner` stores the increment as a
late receipt on the owning run. The owner must be a finished run in the
same company and native session. If the Runner cannot name the owner,
Core uses the session's last finished run and records the attribution as
`inferred`.
- **Local Runner:** late usage stays with the run that is active when it
arrives, or the next run. Usage reported between two runs stays in the
next run's delta, so the total stays exact.
- **Accounting:** `reconcileLateRunUsageReceiptInTransaction` posts a
token increase as a new cost event with the key
`heartbeat:<run>:late:<receipt>`. It updates the run totals so the
accounting integrity check still agrees, records
`cost.late_usage_posted` in the activity log, and queues budget
evaluation.
- **No more refusal:** `persistUsageReceipt` no longer throws after it
saves late evidence, so late usage cannot wedge a Runner outbox.
- **Covered receipts:** an older same-source receipt at or below the
accepted totals, with matching billing, is cleared because a newer
cumulative receipt already covers it. Conflicting billing stays as debt.
- **Older Runners:** a zero-filled invalidation stays on the delivering
run.
- **Unchanged:** a newer lower total, a changed model or billing type,
and per-model receipts stay as visible accounting debt for an operator.
- **Unchanged:** an unexpected provider message during the warm
attachment drain still fails closed. A separate change addresses that
gate.
- **Docs:** added "Late provider usage" to `doc/execution-semantics.md`.

## Verification

- `cd packages/paperclip-runner/runner && cargo test --locked -p
paperclip-runner-core --test codex_provider`: 99 passed, 2 ignored.
- `cargo test --locked -p paperclip-runner-core --lib codex_usage`: 7
passed. This includes new tests for the out-of-order and repeated report
cases.
- `cd server && pnpm exec vitest run
src/services/run-cost-accounting.test.ts
src/services/native-runtime/native-usage-delivery.integration.test.ts
src/services/native-runtime/native-recovery-usage-delivery.test.ts
src/__tests__/cost-data-quality.test.ts`: passed against embedded
PostgreSQL before the last review fixes, and the accounting and delivery
files were re-run after them.
- `npx tsc --noEmit -p .` in `server` reports no errors.
- New tests cover exact and inferred owners, repeated delivery, late
usage that arrives before the owner's charge posts, another session's
run, an older Runner's report with no increment, a same-run increase,
and a lower total. They also run the accounting integrity check after
posting.
- **Review:** Greptile Apex scored 1/5, 4/5, 3/5, 2/5 and 4/5 while
findings were fixed, then 5/5 at the current head.
- **Local Runner:** driver tests cover usage reported between two runs,
including across a restore. No end-to-end local run has exercised it.
- **Not run locally:** the full test suite. CI provides that coverage.
- **Not verified live:** a read-only check of a test deployment found no
late receipts in its history, so this path has only run against
fixtures.

## Risks

- **Run totals can rise after a run finishes.** This is deliberate. The
correction is a separate cost event and an activity log entry.
- **Budgets are corrected after the fact.** A late charge can put an
agent over budget after later work has started. Budget evaluation runs
after each correction, but it cannot prevent that work.
- **Local sessions accept imperfect attribution by design.** Their late
usage can land on the active or next run, because local sessions have no
durable path for routing it. This also applies after a Stop or a pause.
The total stays exact.
- **Out-of-order reports can leave tokens on the wrong run in the
session.** If a prior turn's report arrives after the attached run has
already counted past that total, those tokens stay on the attached run.
Moving them would need a lower correction to the attached run. The total
stays exact.
- **Inferred attribution can name the wrong run in the session.** The
usage still belongs to that session and the total stays correct. The
attribution is recorded as `inferred`.
- **A session with no finished run charges the delivering run,** as an
inferred late charge after its own charge posts.
- **Rollout order:** deploy Core before the Runner. An older Core would
keep this event on the attached run as incomplete usage, and would still
refuse a same-run late receipt.
- **Migrations:** this change has no migration.

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 15:30:55 -08:00
7d1e9b3477 fix(recovery): retain exact warm native export sources (#17916)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native task recovery must save unfinished workspace exports before
continuation.
> - A prepared export can still refer to a sandbox with a warm reuse
policy.
> - The strict recovery inspector rejects that policy and exhausts
source verification.
> - Ordinary reuse must not give the same unfinished allocation to
another run.
> - An existing user Retry can authorize bounded file maintenance after
exact ownership checks.
> - This change retains that exact source and preserves the original
failure record.
> - Recovery completes only when files, stop evidence and policy
settlement agree.

## Linked Issues or Issue Description

**What happened?**

A failed native run has a prepared `keep_running` workspace export. Its
exact sandbox lease still has `reuse_by_environment` policy. Native
export cleanup can preserve that policy while it stops the sandbox.
Generic workspace recovery then rejects the source with
`lease_policy_not_retained`. A saved user Retry remains blocked before
file export.

**Expected behavior**

Keep unfinished exports out of ordinary warm reuse. With current user
authority and an unchanged saved Retry, retain the exact source for one
bounded maintenance generation. Preserve files and return continuation
to normal admission. Keep all ambiguous sources held.

**Steps to reproduce**

1. Prepare a native workspace export with a warm sandbox and
`keep_running` disposition.
2. Save a normal board Retry for the failed source run.
3. Persist its first exhausted source-verification failure before file
export.
4. Run the normal maintenance worker. Before this change, source policy
rejection leaves the saved files unrestored.

Refs #17773 for source diagnostics. Related #17839 records a missing
preservation obligation during blocked restart. This change handles an
existing prepared warm export whose verification already failed.

## What Changed

- Exclude unfinished physical allocations from reuse and obsolete-lease
cleanup across every historical lease row. Reuse a clean candidate or
acquire a fresh sandbox through normal selection. Preserve exact
same-run reattachment, workspace-less execution and ordinary task
admission checks.
- Preserve the newer prepared remote-workspace writer proof for native
and legacy runs. Share its binding check between single-source recovery
and one batched allocation read.
- Keep persisted recovery-action IDs when constructing continuation
source checks. Synthetic sources cannot grant snapshot receipt
authority.
- Permit one audited maintenance adoption for the first exhausted
source-verification attempt. Require an unadmitted canonical saved board
Retry, current user authority, unchanged assignment and latest source
run.
- Keep all company, source, allocation, workspace writer, controller and
physical finalization ownership checks. The public read-only inspector
keeps its strict policy check.
- Pin the exact source for file maintenance. Preserve the previous
generation and reuse the existing bounded recovery worker. Create no
provider turn or new user request.
- Restore the warm policy in the same transaction as verified native
copyback completion. A failed write or changed settlement keeps a
recovery obligation.
- When local merge metadata is missing, settle the exact verified
snapshot while keeping its unfinished allocation retained. Deliver the
saved Retry through normal fresh isolation and attach the verified
files. Keep the accepted result as history.
- Require native copyback proof before reopening an accepted result's
original workspace finalizer.
- Validate snapshot receipts through one shared boundary for warm,
retained and ephemeral sources. Bind the receipt to the current
generation's operation IDs, company, issue, run, recovery action, lease
and provider allocation.
- Retire fixture input before removing recovery proof, and use the same
configured heartbeat for HTTP and worker dispatch. Keep the exact
catalog refusal and no-provider-work assertions.
- Document the recovery contract in technical documentation. Leave
capability maps unchanged for this bug fix.

## Verification

- Candidate-selection RED: all six clean-candidate and fresh-allocation
controls fail on unchanged `f54e7baf538` because a pending export aborts
the whole acquisition. GREEN: the focused suite passes **15/15 cases**
after candidate filtering. Both provider paths preserve the protected
lease and never resume or destroy its allocation. Obsolete fingerprints
cannot send that allocation to cleanup. Historical-row fencing, modern
native/legacy writers, exact same-run reuse, workspace-less execution
and the single-read allocation-history check remain covered.
- The existing reusable-runtime selection cases passed **40/40**.
Driver/capability contract suites passed **5/5**. Reusable service
controls passed **2/2**. After merging current master, the combined
lease/provider suites pass 61 cases and the recovery integration
selection passes 36 cases, including the upstream inherited-session and
resolved-interruption assertions. This gives 97 passing targeted cases
on the final source. A duplicate test-app declaration from the automatic
merge was removed while retaining the upstream assertions.
- The source-history repair has meaningful red/green evidence: three
valid retired-handoff cases could not complete under the old blanket
history-row rejection. Settlement now uses the same full source
inspector as adoption. It accepts exact retired handoffs and still
rejects a predecessor that becomes active after export. The 26 focused
history, rollback, snapshot and ambiguity cases passed before this last
merge.
- Server typecheck passes on the final merged source. Module boundaries
and `git diff --check` pass. Full local repository tests and a
successful current-head full build are not claimed. Current-head CI is
complete on `0ef974d76483e11fb0b0304bcb05bbd3b56e2ef9`: both required
gates pass, with 54 successful checks, three expected skips, one neutral
Paperclip review check and a successful security status. [Full CI
evidence](https://github.com/paperclipai/paperclip/actions/runs/41904339370).
Greptile reviewed that exact head at **5/5** with no actionable
findings. Its check completed successfully. [Ponytail also reviewed the
exact head at
5/5](https://github.com/paperclipai/paperclip/pull/17916#issuecomment-6640083408).
No review threads remain unresolved, and the branch is mergeable.
- Earlier local broad runs exposed a dropped recovery-action ID in the
merged source projection and deferred test input being revisited after
its records were removed. Both were repaired. Later local disk
exhaustion caused manifest and saved-file reserve failures; generated
caches were removed only from these checkouts. The affected saved-file
control passed unchanged after cleanup. The original logs are retained.
No disk reserve or timeout was lowered.
- On the prior head, the first [full CI
attempt](https://github.com/paperclipai/paperclip/actions/runs/41899038530)
received simultaneous runner shutdowns. Four jobs were cancelled, and
two late jobs have lost-runner annotations with unavailable logs. The
Runner job had five earlier ACPX failures whose full assertion report
was lost on interruption. Their cause remains unproven. Identical Runner
source passed the [#19004
control](https://github.com/paperclipai/paperclip/actions/runs/41898305521/job/126814429725),
including all seven enabled tests in that file.
- That attempt's [browser
shard](https://github.com/paperclipai/paperclip/actions/runs/41899038530/job/126817404595)
returned to the exact saved task URL, but its conversation was still
loading at the unchanged heading deadline. The
[artifact](https://github.com/paperclipai/paperclip/actions/runs/41899038530/artifacts/13358840931)
is retained. All five Cloud-auth cases passed locally on the unchanged
head and in the [same-source CI
control](https://github.com/paperclipai/paperclip/actions/runs/41898305521/job/126814429966).
The delay remains unexplained. One rerun of failed/interrupted jobs was
submitted after the workflow completed. This new source revision
requires fresh CI.
- The earlier [original-head
workflow](https://github.com/paperclipai/paperclip/actions/runs/40552228175)
passed all 48 jobs, and prior head `f54e7baf538` received Greptile 5/5.
These results do not establish readiness for the new head. The earlier
Superagent scan was neutral after a pinned-base checkout failure; its
external analysis remains unverified.
- These tests use real PostgreSQL and controlled provider boundaries.
Hosted rollout and original-task acceptance remain unverified.


## Risks

- Maintenance can resume and stop the exact sandbox for file export. It
cannot admit a provider turn, choose another allocation or bypass normal
continuation gates.
- A saved Retry alone is insufficient. A changed source, newer run,
active writer, competing lease, lost user authority or previous adoption
keeps the hold.
- Policy restoration can fail. Completion and restoration now commit
together, so normal recovery remains responsible for a retry.
- Snapshot preservation cannot repair missing local merge metadata. Its
old allocation remains explicitly retained. A snapshot receipt grants no
permission to reuse or destroy that allocation, or to replay the
accepted result.
- The allocation check uses one projected join per candidate over the
existing provider lease index. It adds no polling timer or recurring
scan. A fresh fallback can create another sandbox while the unfinished
source remains retained; it grants no authority to execute a provider
turn.
- This change does not establish the cause of a prior provider failure
or prove that any specific hosted sandbox still contains its original
files.

## Model Used

OpenAI Codex, GPT-6 family, for the original change. OpenAI GPT-6.1 Sol
(`gpt-6.1-sol`), xhigh reasoning, for the merge and integration repairs.
Both used repository tools and PostgreSQL verification.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Devin Foley <devin@paperclip.ing>
2026-11-04 14:47:42 -08:00
Nicky LeachandClaude Opus 5.5 0173110ee3 fix(native): point failed run attachments at the docs (#19052)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - When a later run cannot attach to a native session, the run fails,
and a later run may start a new provider thread.
> - The first failure is easy to miss. In 5 of 11 cases on a test
deployment, the run ended with a different error before it reported the
attach failure.
> - People and agents who diagnose a failed run look at its log and
events, so that is where an explanation is useful.
> - This pull request adds a pointer there at the first failure, with
the real cause and a link to the docs.
> - The benefit is that a failed attach explains itself, and readers can
find the open follow-up in #19047.

## Linked Issues or Issue Description

Refs #19047. I searched open and closed pull requests and issues for
`prior_attach_failed` and related terms, and found no duplicate.

## What Changed

- **Log:** `executeHeartbeatRuntime` writes one stderr line to the run
log when a same-run retry follows a failed `run.attach`.
- **Events:** `heartbeat.ts` adds a `lifecycle` event with the same
message and the code `native_run_attach_failed`, using the stored
redacted failure.
- **Helper:** `describeFailedAttachment` extracts the real cause and
returns nothing for other failures.
- **Docs:** "Session settlement after a run" in
`doc/execution-semantics.md` now explains this and links #19047.
- **Link target:** the pointer links to the repo docs and not the issue.
Agents read these logs, and anyone can comment on a public issue.
- **Unchanged:** error codes and retry behavior.

## Verification

- `cd server && pnpm exec vitest run
src/services/heartbeat/runtime-execution.test.ts
src/services/native-runtime/remote-warm-session-recovery.test.ts`: 57
passed.
- `npx tsc --noEmit -p .` in `server` reports no errors.
- New tests cover the log line at the throw site, and the helper's
output for attach failures and other failures.
- **Not covered:** the new event in `heartbeat.ts` has no test of its
own. It uses the tested helper.
- **Not verified:** I have not confirmed that agents read this log when
they diagnose a failure.
- **Not run locally:** the full test suite. CI provides that coverage.

## Risks

- **Failure text in the log:** when the failure has no structured
message, the log line includes the whole failure string. It passes
through the run log's identity redaction. The event uses the stored
failure that the executor already redacted.
- **Link stability:** the link depends on the docs heading. Renaming
that heading would break the anchor but not the page link.
- **Migrations:** this change has no migration.

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 14:36:17 -08:00
DottaandClaude Opus 5.5 86afc2048b fix(native): admit follow-ups after an owner finished in this process (#19071)
## Thinking Path

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

## Linked Issues or Issue Description

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

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

**Paperclip version or commit**

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 16:29:25 -06:00
DottaandPaperclip 4740d251b7 fix(onboarding): retain plan approval across follow-up decisions (#19043)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Onboarding asks a person to approve a saved plan before execution
tasks can start.
> - A person can then approve a separate proposal for the first
execution step.
> - The creation gate checks only that latest card for the plan target.
It loses the earlier approval of the unchanged plan.
> - Decomposition and native context also omit checkbox plan
confirmations.
> - This pull request validates the latest execution decision and the
latest plan decision together.
> - The benefit is that approved work can start while human, revision,
and proposal gates remain effective.

## Linked Issues or Issue Description

Refs #17914, #18160, and #18283. The related-PR search found no
duplicate repair.

**What happened?**

After a person accepted a checkbox plan card and a later execution
confirmation, native task creation and REST child creation returned 403.
Accepted-plan decomposition returned 422 because its lookup recognized
only standard confirmations. Native task context returned no accepted
plan for the checkbox decision.

**Expected behavior**

The latest execution proposal can use an independently approved,
unchanged plan. A pending or rejected latest proposal must still stop
execution. The current plan must retain valid human approval of its
exact revision.

**Steps to reproduce**

1. Save a plan on a server-created onboarding task.
2. Create a revision-bound checkbox confirmation and accept it as a
human.
3. Create and accept a separate, untargeted confirmation for the first
execution task.
4. Create the child through the native tool or REST API.
5. Separately, try accepted-plan decomposition or read native task
context after step 2.

**Paperclip version or commit**

The workspace's registered hosted release is Core
`09c05c55050fede448196637cbaa543a154da76e`. The three regressions also
failed against local source `3cdb367369f29692b81cf19b514a2abdba90734f`.

**Deployment mode**

Hosted native Runner with ACPX Claude. Local regression proof uses
actual PostgreSQL, authenticated HTTP routes, native tool authority, and
synthetic task data on macOS arm64. No customer data or credentials are
in the fixtures.

## What Changed

- Validate the newest ordinary proposal and the newest plan proposal
separately. Keep the existing human-card and conversational provenance
checks for each.
- Require the accepted plan target to match the current document and
revision. A newer pending, rejected, invalid, or foreign plan decision
cannot fall back to an older approval.
- Share persisted confirmation parsing across admission, decomposition,
and native context. Validate checkbox selections against the saved
options and limits.
- Return `acceptedPlan.approvedOptions` with the user's saved selected
IDs and option text on later wakes, including revision-pinned context.
Keep unchecked defaults out of scope.
- Add behavioral and safety regressions. Document the approval rule.

## Verification

- Three behavioral regressions failed before the repair: child creation
returned 403, decomposition returned 422, and native context returned
`acceptedPlan: null`. All three now pass.
- Two scope-retention regressions failed before the review fix: native
context omitted the selected option after a later approval, with and
without a pinned plan revision. Both now pass. Unchecked defaults stay
excluded.
- Current admission, native-tool, and accepted-confirmation replay
suites: 131 tests passed. Existing first-task completion safety checks:
17 passed, including the atomic decision recheck.
- Safety cases cover missing and rejected plan approval, agent-only
decisions, foreign source runs, changed revisions, invalid checkbox
selections, newer pending or rejected proposals, and task-creation
replay.
- Full repository `pnpm -r typecheck` and `pnpm build` passed locally on
final head `7c0e8ab784`.
- [Full final-head
CI](https://github.com/paperclipai/paperclip/actions/runs/41911580531)
passed: all Vitest shards, Runner checks, eight browser E2E shards,
build, release registry, and canary dry run. The required `verify` and
`e2e` aggregates passed. Storybook checks were skipped for this backend
change; the file-selection job was skipped because full CI ran.
- Earlier local typecheck attempts exposed Copilot verification during
build preparation, stale declarations across the rebase, and two
test-fixture typing errors. The assets were verified, declarations
rebuilt, and fixtures corrected before the complete final-head pass.
- Full local Vitest attempts were interrupted for the rebase and review
repair. The duplicate final local run was stopped after full Linux CI
passed. No complete macOS Vitest-suite pass is claimed.
- A fresh Claude-provider acceptance run remains unverified: the local
Claude account is signed out and no Anthropic API credential is
available. The actual native tool and HTTP paths are verified with real
PostgreSQL persistence. This PR does not deploy or recover the reported
production task.
- Greptile: 5/5 on `7c0e8ab784`, with the review thread resolved.

## Risks

A later acceptance must not revive rejected or changed scope. Admission
validates both current decisions and retains source, company, human
provenance, and document locks. The extra plan-proposal lookup is
bounded and limited to accepted, untargeted first-task proposals.
Ordinary delegation adds no new proof lookup. The native context adds an
optional `approvedOptions` projection. Revision-pinned context now
performs the existing scoped approval read to recover that selection. No
database schema, request, provider, or background worker changes are
included.

## Model Used

OpenAI Codex, exact model `gpt-6.1-sol`, max reasoning effort, with
repository, shell, browser, and GitHub tools. The current session
metadata confirms the model and effort. The context window size is not
exposed.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 15:24:58 -06:00
DottaandPaperclip 8a0e7ab511 fix(runner): preserve ordinary append for unscoped governed terminals (#19044)
## Thinking Path

> - Paperclip manages work done by AI agents.
> - A native turn can park on a saved human question or approval.
> - Its terminal event can return a committed receipt before observers
finish.
> - That option requires an explicit turn ID at the event port.
> - The protocol also permits events without a turn ID.
> - The runner requested the option for those events, which the port
rejected.
> - This change keeps those events on the ordinary append path.

## Linked Issues or Issue Description

Refs: #19025.

**What happened?**

The native governed-wait observation can accept a schema-valid
`item.completed` without a turn ID. If the later terminal also has no
turn ID, the runner compares two absent IDs and requests
`deferPostCommitObservers`. The event port rejects that option. The run
then fails with `governed_wait_provider_terminal_unproven`.

**Expected behavior**

Only a terminal with an explicit turn ID can request the early receipt.
An unscoped terminal must keep the ordinary append behavior and await
its observers. The event port must retain its turn identity check.

**Steps to reproduce**

1. Create a pending human-only interaction from the run.
2. Pass a schema-valid completed tool event without a turn ID through
the real governed-wait observation.
3. Supply an interrupted terminal without a turn ID during the
cancellation drain.
4. Run the actual native runtime with the PostgreSQL event port. The
parent code rejects the deferral request and reports a failed terminal.

**Paperclip version or commit**

Reproduced on `4ea53f9eb5a24de10db3dfda18456132987c4af8`. The follow-up
was identified by a review published after #19025 merged. No observed
provider emission or original-task recovery is claimed. I checked
current master and related PRs before this correction.

**Deployment mode**

Built from source. The regression uses real PostgreSQL and a
deterministic provider boundary; it makes no model call.

## What Changed

- Require a truthy terminal turn ID before the runner requests deferred
observers.
- Keep ordinary append behavior, the port identity check and all
timeouts unchanged.
- Exercise the production governed-wait observation with a persisted
pending interaction and the real event port.
- Retain exact-turn observer and checkpoint controls. Add a direct
rejection control for unscoped deferral.
- Document that unscoped terminals await their observers.

## Verification

- First meaningful red: the actual observation, native runtime and
PostgreSQL port fail on the unmodified parent runtime. The new case
reports `NativeProviderTerminalFailure` in 143ms; this is not a setup or
deadline failure.
- Focused regression and authority controls: 12 passed, 69 filtered.
Full runtime suite: 203 passed. Full PostgreSQL port and timing suites:
84 passed.
- The tests leave the human-only interaction pending with continuation
policy `none`, accept one result, and start only one turn. Existing
scope, invalid evidence, checkpoint, abort, duplicate and restart
controls remain.
- Exact candidate: `8840ea2c9c308e5a60423e67fb3612d24029ba84`.
Repository-wide `pnpm -r typecheck`, `pnpm build` and `pnpm
check:module-boundaries` passed on this exact head. `pnpm test:run` is
still in progress and is not reported as passing. Current-head CI
passed: 55 success, 3 skipped and 1 neutral, including required `ci /
verify` and `ci / e2e`. Greptile and Ponytail both reviewed this exact
head at 5/5 with no blocking findings. The native review was
source-only. No unresolved review threads remain.
- No UI, database schema, agent permission or mapped user entry point
changes. No deployment, live retry or provider replay was performed.

## Risks

Unscoped terminals retain the existing observer wait and can still fail
if it exceeds the existing drain deadline. They do not gain an early
receipt. The change does not supply a missing turn ID or weaken source,
company, run, session, cancellation or transaction checks. A PRP
terminal is not physical-stop or accounting-completion proof.

## Model Used

OpenAI `gpt-6.1-sol`, with code and shell tools. The host does not
expose the reasoning mode or context-window size.

## Checklist

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

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 15:07:06 -06:00
DottaandPaperclip 22735cfcea fix(native): preserve governed terminal receipts during observer settlement (#19025)
## Thinking Path

> - Paperclip manages work done by AI agents.
> - A native turn can yield to a saved human question or approval.
> - The provider must stop, and its terminal event must be saved.
> - The old drain waits for both the event transaction and its later
observers.
> - A slow observer can make a saved terminal look unproved.
> - This change separates the committed receipt from owned observer
work.
> - Checkpoint and result acceptance still wait for that work to settle.

## Linked Issues or Issue Description

**What happened?**

A native governed wait can fail with
`governed_wait_provider_terminal_unproven` after its exact interrupted
terminal is already in PostgreSQL. The event port waits for postcommit
accounting, output and projection callbacks before it returns the
receipt. Those callbacks can cross the provider drain deadline.

**What did you expect to happen?**

A committed terminal proves the event transaction. Its observers remain
separate work. A checkpoint or result must not be accepted until those
observers settle. A pending human-only question must keep its audience
and continuation policy.

**Steps to reproduce**

Run the real PostgreSQL control-plane regression. Commit a same-turn
interrupted terminal. Hold its postcommit observer beyond the existing
provider drain. The parent code reports an unproved provider terminal.
The fixed code retains the committed receipt and joins the observer
before checkpoint or result acceptance.

No overlapping repair was found in the current master or open-PR search.
Related [#18977](https://github.com/paperclipai/paperclip/pull/18977)
covers final conversation-response selection. It does not change this
receipt boundary. The source reproduction is consistent with the
observed failure. The exact hosted provider code is unavailable, so it
does not prove that every observed failure used this branch.

## What Changed

- Add an explicit terminal-only option to the internal event port.
Ordinary appends keep their existing behavior.
- Return that receipt only after the normal transaction and company,
agent, run, session, source and revocation checks.
- Retain the full observer promise. Join it before checkpoint and result
writes. Keep its original rejection and observe it after an aborted
join.
- Reuse the existing provider drain and checkpoint deadlines. Preserve
duplicate and restart replay without another model turn.
- Document the receipt and duplicate-observer replay boundaries. Keep
human-only and no-continuation cards unchanged.
- Assert the actual Codex facade requests deferral only for its
interrupted terminal. Isolate empty skill inventory in monitor-only
process fixtures while retaining their real scheduler, lifecycle,
selection snapshot and three-second deadline.

## Verification

- Meaningful parent red: actual native runtime and real PostgreSQL event
port. The terminal row commits, but a held observer causes
`NativeProviderTerminalFailure` with
`governed_wait_provider_terminal_unproven`.
- Current candidate `0db24ae74ee42fdf92035d8407fc095e9f73cc6a`: full
runtime suite, 203 passed. Real PostgreSQL control-plane, timing and
monitor suites, 103 passed. Runner TypeScript and full layered server
typechecks passed on the unchanged candidate source tree. The controls
cover uncommitted and foreign evidence, explicit false and failed
terminals, held and rejecting observers, abort and controller
revocation, duplicate receipts, and new-port recovery. The real pending
human-only card stays unanswered with continuation policy `none`.
Recovery does not call `startTurn` again.
- `pnpm -r typecheck`, the production server build (including the real
Runner release binary), module boundaries and vendor dependency
verification passed on `6d75`; the follow-up changes only tests and
documentation. Diff checks pass on the current candidate. The prior
`399ad3dc2b` repository-wide `test:run` was stopped as obsolete during
the general-server phase after preserving its progress (exit 130,
incomplete); its repository build was not reached. Its six completed
chat shards passed 1,904 cases, but this is prior-head evidence and not
a full-suite pass. Full current-head CI is required before merge.
- CI attempt 1 on `6d75` failed one real three-second monitor-settlement
assertion; the aggregate verify gate consequently failed. Other jobs
passed. The actual CI merge tree was
`3037bf6db162da3b0a9172046360401f8449ebc5` (`6d75` into `89703c3`). An
unchanged full-file replay reproduced three timeouts. Original-deadline
inspection found the legacy process still preparing; local phase timing
measured cold skill inventory at 6.629 seconds. The original CI did not
retain that phase detail, so its exact subcause remains unproved. The
fixture correction supplies empty skill entries for these skill-free
process tests and still invokes the selection-snapshot callback. It
preserves all assertions and the three-second deadline, with no new
drain wait. All 21 cases pass; deliberately restoring the old
blocked-status rejection still fails the real blocked-check case with
409. No CI retry was issued.
- The prior current-head native review was 4/5 with no production
correctness finding. Its requested direct facade-options assertion and
duplicate-observer wording correction are included. Current-head CI is
green (55 success, 3 skipped), including required verify and E2E;
current-head Greptile is 5/5 with no actionable findings. The old native
advisory remains tied to `6d75`, and its requested findings are
addressed. No new native assessment is claimed or required by
protection.
- The first draft broke three existing transaction-start timing
controls. The no-debt path now keeps the original synchronous start, and
all three pass. Two draft type errors were corrected. A further real-PG
red found an extra default-append assertion after commit. Removing it
restores the original committed receipt after a late abort; the same
aborted signal still cannot write a checkpoint or result. Original setup
and assertion failures remain in private verification receipts.
- Normal merge of current master
`3a2249dd5768313f04b2aa3860dfd363492eda16` has no conflicts. The
affected production source blobs did not change in that baseline merge.
- Local proof uses Node 24.22.2 on Darwin arm64, a real PostgreSQL
instance and deterministic provider-boundary fixtures. It makes no model
call. No deployed replay or original-task recovery is claimed.

## Risks

Deferred observers must remain owned. A missed join could accept
incomplete accounting or output. A rejected or stalled observer must
prevent acceptance, and an abort must prevent late checkpoint/result
writes. The tests exercise these boundaries. The port keeps the existing
best-effort trace-publication handling. This change does not turn a PRP
terminal into physical-stop or accounting-completion proof.

## Model Used

OpenAI `gpt-6.1-sol`, with code and shell tools. The host does not
expose the reasoning mode or context-window size.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 14:11:03 -06:00
Nicky LeachandClaude Opus 5.5 e107a75755 fix(native): decide session settlement from live state (#18992)
## Thinking Path

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

## Linked Issues or Issue Description

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

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

**Paperclip version or commit**

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

**Deployment mode**

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

## What Changed

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

## Verification

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

## Risks

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

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 12:04:29 -08:00
DottaandPaperclip d180ad8c77 fix(heartbeat): retry chat execution authorization contention (#19017)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A task Retry must use the current task and chat authority.
> - The request and queue paths already retain a Retry through brief
lock contention.
> - Execution checks that authority twice, including after native input
preparation.
> - A busy task row or chat identity could fail the admitted run at
either check.
> - This pull request retries only the rolled-back authorization
transaction.
> - The same run can proceed after the lock clears. The final dispatch
gate still rejects changed ownership.

## Linked Issues or Issue Description

**What happened?**

A failed chat task can accept a normal Retry and then fail during
execution setup when another PostgreSQL transaction holds the task row
or chat identity lock. The late check can fail after native input and
workspace preparation. The exception can then enter the workspace
preservation path.

**Expected behavior**

Keep the same admitted run and its frozen input. Retry a recognized,
rolled-back authorization conflict within the existing bound. Recheck
current authority before each attempt. Reject Stop, paused agents,
revoked chat access, or a changed task owner before the execution
callback.

**Steps to reproduce**

1. Admit a normal failed-chat task Retry with the existing PostgreSQL
fixture.
2. On a second connection, hold the task row or the chat identity
advisory lock when execution authorization starts.
3. Release the lock after the real authorizer reports contention.
4. Observe that the original code fails the run before the authorized
callback. Repeat at the late native check after input preparation.

**Paperclip version or commit**

Reproduced on master `039e0dad2c51c80251e1302a19c5566e039c357a`. The
branch is rebased on `8f06bd4e1`.

**Deployment mode**

Source checkout with real PostgreSQL and the local HTTP Retry route.
Provider and chat transport responses use controlled test fixtures.

Refs: #18895 and #18896 cover request and queue admission. This change
covers execution authorization. #18898 and #18989 cover predecessor
cleanup, a separate boundary. The roadmap and public PR search show no
duplicate execution-authorization repair.

## What Changed

- Recognize the existing typed chat-presentation contention error in the
bounded admission retry helper. Keep its existing PostgreSQL
lock-unavailable handling.
- Use that helper at both execution authorization checks. Each attempt
checks cancellation and the applicable legacy controller lease, then
opens a fresh transaction. The loop contains no provider or workspace
callback.
- Include failed-chat Retry in the existing final dispatch ownership
gate. This preserves current task ownership and native recovery owner
checks after a delay.
- Add real PostgreSQL contention, late prepared-input, Stop, pause,
revocation, owner replacement, native completion, and ordinary legacy
completion controls. Document the retry and dispatch boundaries.

## Verification

- The unchanged base failed real task-row and chat-identity contention
cases. A separate late native case failed after frozen input
preparation. A safety control then proved that a replaced task owner
could still reach the backend; the final dispatch gate fixes that path.
- All 11 PostgreSQL cases and all four helper controls pass. The tests
keep exact frozen model and trust input, reject changed authority, and
do not replay a provider callback.
- A real local HTTP Retry reaches the native executor, one controlled
provider turn, one accepted result, a committed finalizer, and one
published chat reply. Reprocessing the canonical request does not start
another turn.
- A separate real HTTP ordinary legacy Retry runs one controlled adapter
callback, completes its task through the normal service, and does not
replay the canonical request.
- Independent review found no material issue in the two production
files. It checked transaction scope, lock order, current ownership,
native lease expiry, and import cycles.
- The full workspace typecheck and full `pnpm build` pass on this head.
Full `pnpm test:run` passed all 1,916 chat integration cases. Its
broader server group reported the existing workspace-runtime case
`returns a bounded structured conflict and exposes only same-company
workspace references` as failed after 46 ms. The unchanged focused case
and the complete 179-case file both pass on rerun. The active broad run
was then stopped for this finite handoff; its child exited 130 and the
command exited 1. The detailed assertion was buffered and was not
retained, so its exact cause remains unknown. This is a partial local
run, not a full local pass; later local groups did not run. All three CI
chat shards pass. [Greptile reviewed this exact head at 5/5 with no
findings](https://github.com/paperclipai/paperclip/pull/19017#issuecomment-6638521119).
- Retained setup and fixture failures are not counted as product
regressions or passes. An incomplete copied Copilot runtime failed the
first typecheck; the supported installer repaired that isolated
dependency, then typecheck passed. Early acceptance fixtures used a
wrong publication field or did not complete their task; the final
controls use the real field and completion service.
- The first CI run failed [workspace shard
1](https://github.com/paperclipai/paperclip/actions/runs/41885475363/job/126769239992)
on child-server startup and a five-second dense ZIP test deadline.
[Server shard
2](https://github.com/paperclipai/paperclip/actions/runs/41885475363/job/126769239908)
failed an existing local runtime readiness probe. All 18 cases in the
two CLI files and the runtime case then passed unchanged locally on this
head. The CLI files also match an earlier independently verified head
byte for byte. One failed-job rerun was requested. Its [server
job](https://github.com/paperclipai/paperclip/actions/runs/41885475363/job/126777070120)
was cancelled after the runner received a shutdown signal. It produced
no test verdict. After that workflow ended, only the interrupted server
job and its required dependents were rescheduled once. The [complete
workspace rerun
passed](https://github.com/paperclipai/paperclip/actions/runs/41885475363/job/126777070125).
The [rescheduled server
job](https://github.com/paperclipai/paperclip/actions/runs/41885475363/job/126781960353)
and its aggregate passed. The [complete current-head workflow is
green](https://github.com/paperclipai/paperclip/actions/runs/41885475363).
All required current-head CI and security gates pass, including the
Ready-triggered scans; three conditional checks are skipped. The
complete unchanged idle-history file also passes all 75 cases locally.
No timeout or assertion changed; all attempts remain part of this
result.
- These local controls do not claim hosted-provider or Linux sandbox
acceptance. The original task-row failure did not retain a SQLSTATE; the
PostgreSQL reproduction proves the local gap without assigning that
original cause.

## Risks

- The existing bound allows 50 backoffs of 100 ms. It limits backoff
time, not total database or lease wall time.
- Native ownership is checked at the final dispatch gate. The
per-attempt controller lease assertion applies to legacy execution.
- A persistent lock or changed authority still fails closed. Unknown
prior provider effects and accepted work retain their existing guards.
- No migration or model, account, trust, credential, or permission
policy change is included.

## Model Used

OpenAI GPT-6 through Codex, with reasoning, tool use, code editing, and
PostgreSQL verification. 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 within the stated scope:
relevant regression and acceptance controls, and the complete chat,
idle-history, and workspace-runtime files. The broad local run remains
partial as recorded above.
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 13:07:50 -06:00
DottaandPaperclip 3a2249dd57 fix(runner): retry proven ingress bootstrap failures (#18981)
## Thinking Path

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

## Linked Issues or Issue Description

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

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

**Paperclip version or commit**

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

## What Changed

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

## Verification

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

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

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

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

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

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

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

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

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

## Risks

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

## Model Used

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

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 12:16:51 -06:00
Devin FoleyandPaperclip edb9d29942 fix(recovery): preserve Retry authority and reopen cross-task cases (#18997)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Tasks keep execution identity and saved workspace files separate
from control-plane updates.
> - A scanner can have permission to reopen a case while its run cannot
restore that case's files.
> - Retry can also follow a preparation failure that has no
recovery-action record.
> - Both paths can accept valid work but leave the target unable to run.
> - This change restores files under the target executor and saves
admitted Retry authority on the canonical receipt.
> - Existing workspace, user, source and successor checks still apply.

## Linked Issues or Issue Description

**What happened?**

An authorized agent updates another closed task. The route tries to
restore the target workspace under the caller's run identity and returns
`403 Task workspace run identity is unavailable`.

A separate failure occurs when a normal board Retry follows a
pre-provider preparation failure. Admission has no recovery-action row
on which to save continuation authority. Dispatch then fails with
`continuation_user_authorization_missing`.

**Expected behavior**

Accept the authorized task update. Restore its terminal workspace when
the target's admitted executor starts. An admitted Retry must retain its
exact dispatch authority even when the predecessor has no
recovery-action record.

**Steps to reproduce**

1. Complete a task with an isolated workspace. From a different running
task, use an authorized comment or PATCH with `resume: true` on that
closed task.
2. Separately, fail native preparation before provider work starts, with
no recovery-action row. Submit board Retry, admit its successor, and
build the execution continuation.
3. Before this change, the first path returns 403 and the second rejects
its admitted authority.

**Paperclip version or commit**

Reproduced against `e968131efa7e63d321f3bcca8d47a62513c2efb4`. Rebased
onto `afbc5a885b5`.

**Deployment mode**

Server-managed native task execution. Tests use real PostgreSQL and
temporary workspace files.

Related PRs: #16927 covers terminal workspace races. #17916 retains
exact native export sources. Neither covers these route and Retry
receipt failures. A GitHub search found no duplicate. ROADMAP.md was
checked; this is a recovery bug fix.

## What Changed

- Defer workspace restoration only for authorized cross-task comments
and updates from a live run. Checkout keeps its existing workspace
identity checks.
- Restore the target's selected terminal workspace under its own
admitted run. Preserve its saved files and existing source checks. Leave
manually archived workspaces unchanged.
- Store server-created continuation authority on the canonical admitted
Retry receipt when no source recovery-action record exists. Existing
action records remain authoritative. Read the fallback only after
matching the company, task, agent, user, source, generation and
successor.
- Add route and PostgreSQL regression tests. Document both paths.

## Verification

- Red: both cross-task route tests returned the observed 403 before the
route fix.
- Red: the target executor could not reuse its archived terminal
workspace before the restoration fix.
- Red: an admitted preparation Retry with no recovery-action row failed
with `continuation_user_authorization_missing` before the receipt fix.
- Green: 419 recovery and continuation tests pass, plus 183 focused
route, environment and Retry tests. The assembled heartbeat test starts
the agent in the same terminal worktree and verifies both its unpushed
commit and uncommitted file bytes. It fails before the restoration fix.
- Negative cases reject wrong Retry runs, users, source runs,
generations, unadmitted receipts and missing authority. The original
failed run stays unchanged. Workspace tests verify saved file bytes.
- Full `pnpm build`, `pnpm -r typecheck`, `pnpm check:module-boundaries`
and `git diff --check` pass.
- The full suite found an existing assembled chat authority regression:
clearing a recovery-action record could be bypassed by the new fallback.
The fix now writes the fallback only when that source has no action
record. That assembled regression passes with the original test
unchanged.
- Full `pnpm test:run` is running again. Its first attempt also hit an
unrelated ancestor skill-root lookup for a missing Slack skill. The
rerun uses a temporary untracked link to the repository's own bundled
skills. This link is excluded from the PR. Full-suite completion and
hosted CI are pending.
- Hosted CI has multiple runner interruptions. E2e shard 1/8, server
shards 3/12, 8/12 and 12/12, and workspaces-a shard 1/2 show runner
shutdown signals. E2e shard 5/8 and server shards 1/12 and 2/12 have
GitHub annotations reporting lost runner communication; their logs
return 404. The `ci / verify` assertion failed because the general-test
matrix was cancelled. GitHub rejected job and failed-workflow reruns
while this workflow is still running. Rerun the interrupted checks after
it settles.
- Greptile reviewed exact head
`a1e3929aedb1ee3bfcd001f4ebb1510106bf4070` at 2026-11-04 17:37 UTC: 5/5,
with no new blocking findings and no unresolved review threads.

## Risks

- A deferred workspace restore can still fail at target admission. The
run keeps the existing workspace failure path; this change does not
bypass file recovery or provider stop checks.
- Retry receipts now store server-created authority. Existing action
receipts remain valid. No schema migration is required.
- Local regression tests establish the software behavior. The new
revision has not been deployed to a hosted instance.

## Model Used

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

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [ ] 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-04 11:55:16 -06:00
DottaandPaperclip 6364fa7327 fix(heartbeat): wait for retry predecessor cleanup (#18989)
## Thinking Path

> - Paperclip manages agent work and preserves its task ownership.
> - Heartbeat admission controls when a queued run can start.
> - A failed run can still own workspace saving and environment cleanup.
> - Automatic recovery can transfer the task execution lock to its
queued retry before that cleanup finishes.
> - The old queue check then sees the retry itself and misses the
predecessor's remaining work.
> - This pull request checks the exact retry predecessor before claiming
the queued run.
> - Existing cleanup notifications can then resume that same run through
normal admission.

## Linked Issues or Issue Description

Refs #18898 and #18691. The first repair covers canonical connected-task
retries with a native source. The second covers live remote workspace
writers. This change covers ordinary automatic continuation when the
predecessor failed before native runtime selection. The search found no
open repair for that path.

**What happened?**

A preparation failure saved a terminal legacy run row while its remote
workspace save and environment cleanup were still active. Generic
continuation created one queued successor and moved the task execution
lock to it. The queue checked that current lock but did not check the
successor's predecessor. The new run started and failed at the existing
unfinished-writer guard.

**Expected behavior**

Keep that same successor queued until its exact predecessor finishes
execution and cleanup. Check company, agent and task identity before
using retry provenance as a wait condition. Preserve Stop, reassignment,
failed cleanup and independent recovery gates.

**Steps to reproduce**

1. Save a failed legacy preparation run with an active environment
lease.
2. Queue its ordinary automatic continuation and bind the task execution
lock to the successor.
3. Claim the queued run before the predecessor finishes cleanup.
4. The old code starts the successor. The fixed code retains it in the
queue.
5. Commit successful cleanup through the existing cleanup owner. The
ordinary dispatcher starts that same run once.

**Paperclip version or commit**

The causal regression fails on master
`86814c6550f52e99ede150a883bdc773cbb540e3`.

**Deployment mode**

Source checkout on macOS arm64 with disposable PostgreSQL, the assembled
heartbeat service and the production work coordinator. The adapter
boundary is controlled. No hosted provider replay or deployment is
claimed.

## What Changed

- Check the current task execution owner and the retry's exact
predecessor under the existing task claim lock.
- Use the saved native task binding, ordinary task binding and
historical context fallback in that order. Ignore unrelated retry
provenance.
- Reuse the existing live-executor and pending-cleanup checks. Reuse
committed cleanup notifications and the existing dispatcher.
- Add real PostgreSQL claim controls and assembled cleanup-event
acceptance. Keep the original source failure, saved files and current
task ownership intact.
- Document the queued predecessor-cleanup rule.

## Verification

- RED: the unchanged queue claimed the successor and wrote a new
controller lease while its legacy predecessor retained an active remote
environment lease. The regression expected the successor to remain
queued.
- GREEN: the same regression passes after the change. The complete queue
suite passes all 71 tests.
- The assembled heartbeat suite passes all six tests. The added cases
cover one continuation after committed cleanup, saved-file reuse, final
task lock release, board Stop, reassignment and failed cleanup.
- Scope controls cover a deleted source after scan, another company,
another task, another agent, canonical native task binding and the
existing current-owner hold. A failed cleanup and a still-live local
executor retain the wait.
- Node 24.19.0 and pnpm 9.15.4. Module boundaries and `git diff --check`
pass.
- Initial fixture attempts failed on database constraints and observer
timing. The final fixtures use valid immutable bindings, the board Stop
attribution and a completed real dispatcher call after reading the exact
committed lease state. Production ownership guards were retained
throughout.
- Full low-memory repository typecheck passed at `5cddb49322`. The sync
to master `04a24f4e06` adds only the independent retained-allocation
test fixture from #18984; production code is unchanged.
- Full `pnpm build` passes on current head
`c7c2bcf08ffd05be5ce714683ae1dcfe6f5b49f1`.
- The post-sync run executes all 80 behavior assertions successfully
across the queue, assembled cleanup and updated upstream retention
suites. Its queue `afterAll` then exceeds the existing 30-second
database shutdown hook. That failed suite result is retained. The
controlled post-build queue rerun passes all 71 tests with normal
teardown. The earlier shutdown cause remains undetermined. Its owned
queue-test PostgreSQL process has exited. The separate local default
`pnpm test:run` was intentionally stopped after full unfiltered CI
passed on the exact head. Four local chat partitions completed before
that stop; this is not a local full-suite pass. The verified owned
process group and its PostgreSQL fixture have exited. Logs and retained
fixture data remain available. The timeout has not been increased.
- Full unfiltered CI passes on current head
`c7c2bcf08ffd05be5ce714683ae1dcfe6f5b49f1`: all 12 server shards, all
six internal chat partitions, all nine serialized server shards, the
workspace groups, native Runner lanes, typecheck, build and browser E2E.
Both required [ci /
verify](https://github.com/paperclipai/paperclip/actions/runs/41866478617/job/126711606613)
and [ci /
e2e](https://github.com/paperclipai/paperclip/actions/runs/41866478617/job/126711263146)
pass. The file selector is skipped, so this is the full CI test matrix.
The head has 55 successful checks and three expected skipped checks. The
ready-for-review contributor trust and security scan also pass.
- Current-head Greptile is 5/5, its check is successful, and no inline
review threads remain. The ordinary review check also passes. No merge,
deployment or original task retry was performed.

## Risks

- A predecessor with unfinished cleanup keeps its successor queued. Its
existing cleanup owner remains responsible for settlement.
- A task claim reads at most one additional predecessor row. A matching
source also uses the existing ownership and cleanup queries. The change
adds no timer, schema, credential, provider or model policy.
- A committed cleanup signal only requests normal admission. Current
ownership and independent execution gates still apply.

## Model Used

OpenAI GPT-6 through Codex, with reasoning, code editing, tool use and
PostgreSQL verification. The session does not expose a more specific
serving model ID or context-window limit.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 11:16:54 -06:00
DottaandPaperclip aaea05e158 fix(native): preserve backups when completed warm sessions retire (#18986)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native Runner keeps provider history when a warm session ends.
> - Object storage must preserve that history before the sandbox is
released.
> - A completed turn releases its live controller lease.
> - The backup owner lookup rejects that released lease and stops the
next task preparation.
> - This pull request uses the existing completed-run checkpoint fence
for backup storage.
> - The next turn can prepare after the previous provider state is
saved.

## Linked Issues or Issue Description

Refs: #18932, #18011, #18984.

**What happened?**

A warm Codex turn completed and kept its provider session alive. The
next turn stopped during preparation with
`native_harness_storage_owner_missing`. The backup lookup required a
live lease after normal completion had released it.

**Expected behavior**

Save the completed session under its retained controller generation and
committed result. Keep the existing owner and successor checks. Do not
start another provider turn during the save.

**Steps to reproduce**

1. Run a warm native session with object-backed provider backups.
2. Commit the first turn and release its controller lease.
3. Start the next task preparation and retire the previous session.
4. Observe the backup owner error before provider work starts.

**Paperclip version or commit**

The regression is present at `86814c6550f52e99ede150a883bdc773cbb540e3`.

**Deployment mode**

A hosted server with a remote native Runner and S3 storage.

## What Changed

- Accept a released backup owner only when its exact controller record
has a committed result.
- Reuse the checkpoint owner policy for same-controller preparation and
native successors.
- Recheck the controller generation, committed result, and released
state after upload.
- Preserve live-owner rejection and the rule that a late upload cannot
replace a newer conversation backup.
- Add real PostgreSQL storage and restore regressions. Document the
completed-run backup rule.

## Verification

- On 2026-11-04, two new regressions failed on the unchanged source with
`native_harness_storage_owner_missing`. These covered an idle completed
session and a same-controller preparing successor.
- After the fix, 54 selected tests passed: 30 durable backup tests and
24 existing early-retirement checkpoint tests. After the fixture-only
upstream sync, all 58 selected storage, checkpoint, process, and
affected fixture cases passed again.
- The storage cases use real PostgreSQL, encrypted archives, object
transfer, and restoration. They cover invalid completion evidence,
foreign or expired successors, active predecessors, changed generations,
changes during upload, and newer backup publication.
- Command: `pnpm exec vitest run
server/src/services/native-runtime/native-harness-backup-storage.integration.test.ts
server/src/services/native-runtime/native-runner-restart-recovery.integration.test.ts
-t 'released|backup|early retirement|durably retires an accepted warm
Runner'`.
- An independent source review found no outstanding code issue.
- Full repository typecheck passed with `node
scripts/typecheck-low-memory.mjs` before the upstream fixture-only sync.
Production source is unchanged across that sync.
- An assembled process probe passed with the Runner binary built from
this head and a fake Codex protocol peer. It started real processes,
committed a result, released the source lease, claimed the next
controller, and completed the source stop, drain, and suspend commands.
It recorded one provider turn start and kept the task bound to its
successor.
- Command: `pnpm exec vitest run
server/src/services/native-runtime/native-runner-restart-recovery.integration.test.ts
-t 'durably retires an accepted warm Runner'`.
- CI exposed the old allocation-readiness test setup in three cases. The
branch now includes the merged #18984 fixture correction. The same three
cases passed locally after that sync.
- An earlier CI test shard was cancelled after its runner received a
shutdown signal. Its log was retained before one rerun. Another old-head
shard reported failure with no retrievable job log or annotations; its
cause remains unknown. Current-head CI must pass independently.
- Local `pnpm build` passed at this head. All 1,904 chat cases passed
across the supported local shards; the earlier shards used
byte-identical source before the fixture-only sync. The interrupted
default command is retained and is not reported as a pass.
- The local bulk server run had separate enrollment acknowledgment,
environment-run status, monitor-settle, idle-history teardown, and
repository-rehome failures. In isolation, idle-history passed all 75
tests and the unchanged repository cases passed with their original
limits (4.9s large files, 6.4s refs). The remaining four local failures
reproduced on unchanged master `04a24f4e06` under the same environment
and limits. Their causes remain unproven. Logs and the baseline
comparison were retained; the local full suite is not claimed as passed.
- Current-head CI first hit the unchanged repository-rehome large-file
case's 15s timeout. The same isolated case passed in 4.9s. After
source/call-path comparison, one bounded failed-shard retry passed all
115 executed suites; no assertion or timeout was changed. The original
timeout remains recorded.
- Local verification uses macOS 26.5.2 arm64, Node 26.4.0, pnpm 9.15.4,
PostgreSQL 18.1, and Cargo 1.97.1. The current tested head is
`622f69f76ff68c71eacdcbcbc6ab4d4c6eb83bf9`.
- The full unfiltered [PR
workflow](https://github.com/paperclipai/paperclip/actions/runs/41868587974)
passed at `622f69f76ff68c71eacdcbcbc6ab4d4c6eb83bf9`: all 12 general
server, 3 chat, 9 serialized, workspace, native Runner, build/typecheck,
E2E, and final verify lanes. The file selector was skipped, so this was
the complete fallback matrix.
- Greptile reports 5/5 on this exact head with no open review threads.
Disposable local test processes were cleaned up and the source tree is
clean.
- The real hosted Codex and S3 workflow has not been rerun because the
local verification environment has no remote provider credentials. The
original task remains unchanged.

## Risks

The main risk is a stale controller publishing provider history after a
new owner starts. Both storage transactions keep the company, run, boot,
and generation fence. A released owner must keep its committed result.
Existing successor checks and the newer-backup publication guard remain
in force. The live checkpoint writer still requires a non-null lease. No
migration or API change is required.

## Model Used

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

## Checklist

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

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 11:15:58 -06:00
DottaandPaperclip afbc5a885b fix(workspaces): save partial and shallow Git checkouts offline (#18990)
## Thinking Path

> - Paperclip manages AI agents and their workspaces.
> - A completed remote run must save its files before finalization can
release its source.
> - A shallow Git mirror uses upload-pack and can omit objects that
exist only in the index.
> - A filtered source can also fail when that mirror requests blobs the
source never downloaded.
> - Fetching missing history is not required to preserve an existing
partial checkout.
> - This change saves the available object stores and their existing
shallow, sparse, and promisor state.
> - The benefit is an offline save that preserves accepted work and
remains readable by the existing archive reader.

## Linked Issues or Issue Description

Refs #16727, #18705, and #18935. These changes cover managed clones,
streaming capture, and safe diagnostics. This change repairs Git
snapshot contents.

**What happened?**

Remote workspace capture invokes a mirror clone before it saves the
archive. A shallow filtered sparse checkout with a missing promised blob
fails that clone and surfaces `remote_workspace_operation_failed`. An
ordinary shallow checkout can also lose an index-only object during the
mirror transfer.

**Expected behavior**

Capture preserves the objects already available in the checkout,
including staged work. It preserves partial, shallow, and sparse
semantics without remote hydration. Corrupt or unsupported state must
keep its source and receive no successful save receipt.

**Steps to reproduce**

1. Create a real Git repository with two paths and commit history.
2. Clone it with `--filter=blob:none --depth=1 --sparse`, then select
one path.
3. Stage a change, edit it again, and add an untracked file.
4. Remove access to the original repository and capture the workspace.
5. The base capture fails before transfer. The changed capture restores
the available work after the original source is removed.

**Paperclip version or commit**

Reproduced on `86814c655`. The same transport/archive sources are
present in `76e43852a`.

## What Changed

- Preserve existing object files for shallow and partial repositories,
including their original promisor pack markers.
- Preserve shallow boundaries and the selected sparse settings and
patterns.
- Validate available object contents with full Git fsck. Reject unknown
corruption instead of creating new promises.
- Deny Git network protocols during capture. Keep the ordinary mirror's
explicit local file transport.
- Exclude ambient Git location, index, object-store, config, and
template overrides. Disable templates for init and clone.
- Keep the existing archive format, source fences, path guards,
credential allowlist, and failure retention.
- Add real command and reader regressions for full, shallow, filtered,
sparse, corrupt, and unsupported repositories.

## Verification

- Red: the compiled Node 24 transport completed helper installation,
then failed capture with the same generic operation error and no
transfer. Dirty source bytes remained intact.
- Red: the new full/shallow/partial transport-to-reader cases exposed
both filtered upload-pack failure and a missing staged-only object.
Independent review then reproduced accepted corrupt object bytes and
ambient template config/hook copying.
- Green: all 110 transport/archive cases pass after those repairs. They
cover staged, dirty, and untracked work; ordinary and forged-promisor
corruption; malformed filters; alternate stores; unsafe links;
credential redaction; and template isolation. An initial linked-worktree
config regression was fixed before this complete passing run.
- A compiled new capture restored through both the unchanged earlier
reader and the new reader after source removal. HEAD, status, work
bytes, sparse/shallow state, and the same missing promised object were
preserved. Full Git fsck passed. No remote protocol was permitted.
- Full repository `pnpm -r typecheck` and `pnpm build` pass at
`91ca00a8327216def91303e19940e92ffe07c08f`. The build stamp names that
exact head.
- The [complete current-head CI
run](https://github.com/paperclipai/paperclip/actions/runs/41865961403/attempts/2)
is green: all 58 checks are terminal with no failure or pending check.
Three expected skips and one neutral review remain those states.
- Preserve the [first CI
attempt](https://github.com/paperclipai/paperclip/actions/runs/41865961403/attempts/1):
one browser case reached its 5-second heading deadline while the
conversation pane was loading; a chat worker reported shutdown; another
browser worker lost communication and its log blob was absent. A
failed-job-only retry passed at the same source head. No test deadline,
assertion, or source change was made for that retry.
- The complete local `pnpm test:run` passed all 1,904 chat cases across
six partitions. Its later general-server phase timed out two unchanged
tool-access cases at 15 seconds. That known-red command was stopped
before completion. Both cases then passed unchanged in a fresh isolated
process. The original timeout cause remains unknown; this is not a full
local-suite pass.
- [Greptile reports
5/5](https://github.com/paperclipai/paperclip/pull/18990#issuecomment-6635931211)
at the current head with no actionable finding. The product review's
loose-object cost advisory was [answered with local measurements and
resolved](https://github.com/paperclipai/paperclip/pull/18990#discussion_r4470261833).
There are no unresolved review threads.
- Original service acceptance remains unverified. Recorded checkout text
is mechanism-compatible narrative evidence, not proof of its physical
Git state or original failure cause. This PR includes no deployment or
additional live recovery.

## Risks

- Full object validation and copying available metadata use CPU and
temporary space proportional to the local object store.
- The offline path rejects alternate object stores and unsupported
partial filters. Such sources remain protected.
- Promised objects remain missing. Later work still needs ordinary
repository access to request them.
- There is no archive format or schema change. Existing readers restore
standard Git files, and existing full snapshots remain covered.

## Model Used

OpenAI Codex, GPT-6 family, with reasoning, repository tools, shell
execution, and local verification. The host does not expose a more
specific 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-04 10:52:39 -06:00
DottaandPaperclip 86814c6550 fix(recovery): ready retained sandboxes before preparation (#18934)
## Thinking Path

> - Paperclip manages work performed by AI agents.
> - Restart recovery must preserve each run's workspace and execution
owner.
> - A retained remote lease can refer to an archived sandbox.
> - The reattachment path returned that lease without making it ready.
> - A short project-authentication command then had to start the
sandbox.
> - This PR resumes and verifies the exact retained allocation before
preparation.
> - Recovery keeps the recorded workspace and preserves saved work if
resume fails.

## Linked Issues or Issue Description

**What happened?**

A native run resumed after a controller restart. Its saved sandbox had
been archived. Reattachment returned its lease without a provider
lifecycle resume. The next project-authentication probe tried to start
that sandbox with its 15-second command budget. The start failed before
the probe could inspect project settings. The heartbeat reported a
configuration conflict and stopped work.

**What did you expect to happen?**

Make the exact retained sandbox ready through the existing bounded
lifecycle operation. Verify its allocation before running short
preparation commands. Keep the validated lease and recorded working
directory. Never acquire a replacement in this path.

**Steps to reproduce**

Hold the provider's exact-lease resume pending and call remote
reattachment for an active ephemeral lease. Before this change,
acquisition returns immediately. The added tests hold acquisition until
resume completes and reject missing, changed or expired allocation
evidence. Provider compatibility cases retain the recorded workspace
when resume omits directory metadata or returns the allocation root.

**Deployment mode**

Native Runner in a sandbox with a retained workspace and managed AI
connection. The observed provider was Daytona.

Refs #18910, which restores selection of retries using current remote
workspace storage. This change repairs the next preparation boundary.
Related #17421 and #18559 change project-file inspection; they do not
make retained sandbox leases ready.

## What Changed

- Resume the exact validated sandbox lease before returning it to
preparation.
- Require the receipt to confirm the original provider allocation
without reporting expiration.
- Keep the original database lease and recorded directory. Resume
metadata can describe the allocation root.
- Pass the existing typed resume-directory input through the runtime
facade.
- Preserve retained E2B sandboxes when resume timeout setup or workspace
inspection fails. Keep new-allocation cleanup and explicit
release/destroy behavior.
- Preserve plugin-task reattachment and lease identity checks.
- Update the Stop-accounting fixture to confirm exact-allocation resume
before cleanup, with its budget, ownership and no-paid-work assertions
intact.
- Document the ordering and add readiness, receipt, failure and
ownership regressions.

## Verification

- Red: seven initial retained-lease cases failed before the readiness
change. Two receipt-compatibility cases and four E2B preservation cases
failed before their fixes.
- Green: all four runtime, workspace-preparation, heartbeat-recovery and
orchestrator suites pass: **442 tests**.
- The Stop-accounting CI failure reproduced locally. All **five**
integration cases pass with the exact-allocation resume fixture.
- All **22 E2B tests**, the standalone plugin typecheck and its build
pass.
- **13 Daytona safety cases** pass, including recorded directories,
allocation sentinel rejection, saved-file reads and no replacement
creation. The SDK boundary is simulated; the saved-file case uses real
files and a POSIX shell.
- These focused checks cover **482 passing cases**.
- Full recursive typecheck, final server typecheck and full workspace
build pass on macOS arm64 with Node 24.22.2 and PostgreSQL 18.1.
- Greptile is **5/5** on head
`32f915a475275680bcf2a2aa43ae0c72f4e4b1db`. Both original review threads
are resolved.
- All current-head CI gates pass. The check rollup has 54 successful
checks, three skipped checks and one neutral check.
- The separate Paperclip Review check remains queued. Its public
response says work is paused and the request is saved. The review task
is waiting for saved-file recovery, so no Paperclip assessment of this
head is claimed. The branch-required gate has passed.
- A read-only deployed trace showed an archived sandbox start fail at
the short probe's 15-second deadline. Actual provider reattachment with
this revision has not been exercised. Live recovery remains unverified
until rollout.

## Risks

Recovery now makes one bounded lifecycle call before short preparation
commands. A missing resume capability or a missing, changed or expired
allocation receipt holds recovery. The existing runtime and provider
retain allocation and sentinel verification. The path does not acquire,
release, replace or reseed the lease. Warm providers can return their
existing allocation. E2B retains the existing sandbox if resume setup
fails; explicit lifecycle cleanup still owns its removal. No background
polling, credential-policy change, migration, workflow change or
lockfile change is added.

## Model Used

OpenAI GPT-6 through Codex, with reasoning, source inspection, tool use
and code execution. The exact backend snapshot and context window 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
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 09:04:13 -06:00
DottaandPaperclip 84e9c8f364 fix(retry): wait for admitted source cleanup before dispatch (#18898)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Connected-task Retry keeps the original request and its current
authority.
> - One durable Retry can already own a queued successor.
> - The old native environment can still have pending cleanup before
that successor starts.
> - The task execution lock points to the successor, so the old cleanup
can escape the queue claim check.
> - Dispatch then refuses the source state and fails the new run before
provider work.
> - This pull request keeps that same run queued and wakes dispatch when
cleanup changes.

## Linked Issues or Issue Description

Refs #18845, #18853, #18860, #18877, #18882 and #18932. These repairs
cover preservation binding, interrupted preparation, old native
ownership, manual native holds or other setup guards. This change covers
an already admitted connected-task Retry whose native predecessor still
owes cleanup. It uses the shared cleanup predicate so the manual-hold
repair remains with its existing owner. Search found no duplicate
queued-source cleanup repair.

**What happened?**

A board Retry saved its exact chat request and admitted one queued
successor. The source lease was still pending cleanup when that
successor reached dispatch. A retained stop receipt did not settle the
full cleanup contract. The queue checked the task's current execution
owner, which was already the successor. The native authorization guard
then rejected the predecessor state and failed the new run before
provider dispatch.

**Expected behavior**

Keep the same canonical Retry and queued run while the exact source
cleanup is pending. After authoritative settlement, submit that run
through normal dispatch once. Do not infer safe execution from a
stop-and-retain receipt alone.

**Steps to reproduce**

1. Retry an authorized connected request from a failed native run.
2. Admit one successor while capacity keeps it queued.
3. Leave or restore the source lease to pending cleanup with a matching
retained stop receipt.
4. Release capacity and resume the ordinary queue.
5. The old claim path starts the successor and the native source guard
refuses it.

**Paperclip version or commit**

Reproduced against master at `f77499283270d2e966aabd58493f9d35c6bbdf5f`.

**Deployment mode**

The regression uses the real HTTP route, chat authorization, queue
claims, disposable PostgreSQL and the production work coordinator on
macOS arm64. A controlled adapter supplies the final result. No hosted
provider replay or deployment is claimed.

## What Changed

- Validate the exact canonical Retry, chat action, company, task, agent,
source and successor before testing source cleanup.
- Keep the successor queued under the existing issue claim lock while
the shared cleanup predicate remains pending.
- Wake only the scoped queued-dispatch worker after cleanup or exact
workspace-owner release commits. Add no timer or background scan.
- Verify pending cleanup at intake, later cleanup invalidation, one
final execution, idempotency, Stop, changed input, unknown stop
receipts, a different live controller and signed sandbox absence
followed by owner release.
- Document the queued-wait contract. No schema, endpoint, credential,
model or provider policy changes are required.
- Align five native preparation acceptance fixtures with the existing
workspace publication contract. Use the real reservation and publication
owner. Retain a raw-root refusal case with no provider launch and
preserved files.

## Verification

- Latest master sync at `e26cad76b8f20ba823d702ed60e7b7294f8f5a7c`
includes master `396b7fc62a0a4b1d8aadbda8221c05879d54177c`. The shared
test helper preserves #18944's canonical retained-directory default, the
real publisher for the five fresh-workspace cases, and the explicit
raw-root refusal. Independent remerge review found no dropped upstream
behavior, guard or assertion.
- The latest head passes the focused six cases, all 580 complete
workspace/continuation tests, all 38 selected HTTP Retry cases, fresh
full recursive Node 24.22.2 types and full build. No affected tests
skipped. An initial cached test-layer attempt read old budget
declarations; regenerating them resolved all seven errors without
test/mock or production-export changes.
- All current-head CI is green at
`e26cad76b8f20ba823d702ed60e7b7294f8f5a7c`, including required [ci /
verify](https://github.com/paperclipai/paperclip/actions/runs/41846304017/job/126636751314)
and [ci /
e2e](https://github.com/paperclipai/paperclip/actions/runs/41846304017/job/126636638086).
Greptile and Paperclip Review both rate this exact head 5/5, with no
findings or open review threads.
- Master sync at `3e4f266c2da172a84afd0b9cb4a91ea3b95f3c0d` merges
master `b891e69bd23d6dab8ce536569e15bc4a7a31c35c`. The only conflicts
were adjacent queue imports and two HTTP Retry test matrices. Both
imports and both complete matrices remain unchanged. Independent review
found no relaxed guard or assertion.
- Node 24.22.2 and pnpm 9.15.4: all 38 selected real HTTP Retry cases,
194 queue/cleanup/finalizer cases, module boundaries, recursive types
and build pass at `3e4f266c2da172a84afd0b9cb4a91ea3b95f3c0d`.
- That sync head's [server
check](https://github.com/paperclipai/paperclip/actions/runs/41838741852/job/126604742761)
failed five native acceptance cases in CI and locally with
`remote_workspace_published_path_invalid`. The test fixture supplied a
raw directory instead of a fresh publisher-generated workspace. The
guard from #18932 remains unchanged. The same five cases passed earlier
Linux CI before that guard was present.
- The test-only alignment at `dcdee1bb6c2e2e6dd494382499d17c23a3ce71a8`
obtains the saved authority, path and run reference from real
`reserveRemoteWorkspace` and `prepareRemoteWorkspace`. All five original
cases and a raw-root no-launch/no-result/files-preserved control pass.
Their native result, file, catalog and single-execution assertions
remain intact. An initial setup attempt was refused for a noncanonical
allocation root; the fixture now resolves that owned root before
publication.
- At previous fixture head `dcdee1bb6c2e2e6dd494382499d17c23a3ce71a8`,
the generated server test-layer typecheck passed. All 580 tests in the
complete workspace/continuation family pass, with no skips. That
test-only commit left production files byte-identical to the sync head.
- Previous published-head CI was green at
`dcdee1bb6c2e2e6dd494382499d17c23a3ce71a8`, including required `ci /
verify` and `ci / e2e`. The [Linux Node 24.22.2 server
shard](https://github.com/paperclipai/paperclip/actions/runs/41842665765/job/126618334638)
executed all five original acceptance cases and the new raw-root refusal
case successfully. Its whole workspace recovery file passed all 245
tests.
- Greptile reviewed previous published head
`dcdee1bb6c2e2e6dd494382499d17c23a3ce71a8` at 5/5, with no findings or
open review threads. No unchanged-job rerun was used for the reproduced
sync-head failure.
- Causal red: the real HTTP regression started and failed the successor
at the native source-state guard while cleanup remained pending. A
second red proved that signed sandbox absence followed by copyback-owner
release lacked a queued-dispatch wake.
- Green: seven HTTP/PostgreSQL cleanup cases, the exact confirmed-owner
release notification control and seven existing retry lineage controls
pass. The real coordinator resumes from committed cleanup or
workspace-owner release, without a manual queue restart. One controlled
adapter execution produces the result after settlement.
- Earlier branch verification passed full repository `pnpm -r
typecheck`, `pnpm build`, module boundaries and `git diff --check`.
- The full stale-queue file control passed all 64 cases with unchanged
observer/hook limits. The initial broader affected run had 192 passes
and one non-assignee native-owner case miss its existing five-second
adapter observer while already running with no error. The isolated case
passed; the unchanged base passed all 64 cases but hit its existing
30-second teardown timeout. Those original results are retained. The new
queue hint has no dispatch consumer in that fixture.
- Earlier-head CI was green: 54 success, three skipped and two neutral
checks at `6adb9f1ba682282c512d5e751ba2aa663f614ca2`. One chat job was
cancelled by an external runner shutdown; its single unchanged-job rerun
passed. No source was changed to obtain that result.
- Greptile and Paperclip Review both rated earlier head
`6adb9f1ba682282c512d5e751ba2aa663f614ca2` 5/5 with no findings or
unresolved threads. These results do not qualify the master-sync head.
- At initial verification, a complete local `pnpm test:run` was not
repeated while the related setup repair's full local run was active. The
full earlier-head CI suites passed. A full local suite has not been
repeated for the mechanical master sync. Local verification uses the
affected HTTP, producer, lineage and queue suites. No full local-suite
pass is claimed.
- The fresh clone's missing plugin SDK links were repaired with the
repository's own postinstall helper before verification. The embedded
PostgreSQL package's own symlink helper was also required.
- No staging task was submitted again with this repair, and no
deployment is claimed.

## Risks

- A pending source cleanup can keep the queued run waiting. Its cleanup
owner still owns reconciliation and its existing limits.
- Stop, changed source, unknown termination and live ownership retain
their existing authority checks. A cleanup notification grants no
execution authority.
- A new cleanup change after claim still faces the existing final
dispatch guard.
- Cleanup notifications can change dispatch timing in a running
controller. The real coordinator cases verify commit-bound wakeups; the
broader current queue-file control passed all 64 cases. The earlier
loaded-run and baseline teardown failures are retained in verification.

## Model Used

OpenAI GPT-6 through Codex, with reasoning, code editing, tool use,
PostgreSQL and browser verification. This session does not expose a more
specific deployment 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-04 08:33:39 -06:00
DottaandPaperclip 396b7fc62a fix(runner): scope session diagnostics to the active run recorder (#18944)
## Thinking Path

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

## Linked Issues or Issue Description

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

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

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

## What Changed

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

## Verification

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

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

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

## Risks

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

## Model Used

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

## Checklist

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

---------

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

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

## Linked Issues or Issue Description

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

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

## What Changed

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

## Verification

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

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

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

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

## Risks

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

## Model Used

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

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 07:19:12 -06:00
DottaandPaperclip b6352578c5 fix(task-retry): retain chat Retry during identity contention (#18896)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Task Retry must preserve a person's request and enforce the current
chat authority.
> - Chat retry preparation takes nonblocking source locks before it can
authorize the saved source.
> - Sustained lock contention could discard the Retry request before it
was saved.
> - This pull request saves one unadmitted request in the existing chat
retry queue.
> - The worker checks the source and current access before normal
admission. The task shows a scheduled retry while it waits.

## Linked Issues or Issue Description

**What happened?**

Task Retry returned HTTP 500 when another transaction held the saved
chat principal's identity lock or the endpoint authorization row. The
source preparation error rolled back the canonical Retry intent. The
same gap existed when Retry had to restore a certified input batch that
had never reached a provider. With the bounded synchronous retries in
#18895, sustained contention returns 409 without a saved intent.

**Expected behavior**

HTTP 202 retains one Retry request. Acceptance does not grant source
authority or mean that a provider started. The existing worker prepares
the request if the source locks become available before its saved
deadline. It must reject changed input, revoked access, Stop, or
superseding work before input adoption, publication, or provider work.

**Steps to reproduce**

1. Prepare a failed task with its original admitted GitHub chat source.
2. Hold that principal's `chat-identity` advisory lock or the endpoint
row on a second PostgreSQL connection.
3. Send the normal task Retry HTTP request. Check the response and
persisted intent.
4. Release the lock. Process the existing chat retry queue and check
that one authorized successor is queued.

**Paperclip version or commit**

Reproduced on master `8771606fd87f69ed4057779e3dce3a9bca8088e6`.

**Deployment mode**

Source checkout, real Express Retry route and embedded PostgreSQL.
External chat transport is a test fixture. The live hosted fix has not
been deployed or verified.

Related: #18895 handles brief admission contention with bounded
synchronous attempts and returns 409 if it remains busy. This change
retains a durable request through sustained identity contention. It uses
the existing background retry owner and adds real HTTP, restored-input,
Stop, publication, restart, and task-presentation coverage.

## What Changed

- Preserve a strict pending-source action only when the identity
try-lock reports contention or the endpoint authorization query reports
lock-unavailable through an explicit Retry callback. Bind it to the
canonical task Retry, company, task, agent, endpoint, conversation, and
principal. Other database and authorization errors keep their existing
behavior.
- Revalidate the canonical intent, current board permission, saved
source, and latest execution before the existing worker can prepare that
action. Ordinary authorization rejects an unprepared action.
- Keep certified input restoration inside source preparation. Recheck
the saved deadline before the prepared payload write. Expiry, a nested
savepoint, or outer rollback cannot commit partial restoration.
- Give source authorization contention one saved 60-second deadline and
a separate counter. Keep its waiting phase until current source
authorization succeeds or the action terminates. It does not spend the
eight delivery attempts. Duplicate clicks, worker restart, and
background sweeps do not renew either limit. Reuse the existing work
notifications and outbox deadlines.
- Persist completion of the authorization wait under the current claim
before delivery. A restart revalidates current authority. Generic
delivery failure preserves the stored phase even when a commit
acknowledgment is lost; it does not grant a wake or renew the saved
deadline, counters, or claim.
- Project pending preparation and prepared but unsettled actions as the
existing scheduled-retry state with the actual outbox deadline. Keep the
exact task, intent, agent, and source binding. Active execution keeps
its normal working state. No UI or API enum change is needed.
- Add regressions and document the admission boundary.

## Verification

- RED: the original real HTTP route returned 500 under a held PostgreSQL
identity lock. The restored-input case also returned 500 while its 60
sibling source and budget cases passed. The current endpoint-row case
returned 409 without a saved intent under the bounded synchronous
fallback.
- RED: the worker failed a valid Retry during an actual nine-second
PostgreSQL lock. Removing only the latest-run guard makes the new
queued-run negative fail. Source preparation could also commit restored
input after its deadline, and an already prepared action could process
after a late lock release. The prepared but unsettled task view lost its
scheduled state.
- GREEN: all 91 selected real PostgreSQL integration cases passed.
Coverage includes the 62-case saved-input matrix; coalescing; restart;
one real heartbeat successor; source, actor, Stop, newer-work and
publication refusals; a real nine-second lock; fixed-deadline
exhaustion; the separate eight delivery attempts; certified restoration
rollback on expiry; prepared-action early and late release; a real
conversation-row wait that crosses the deadline during current
authorization; endpoint-row contention; unknown source; an
already-revoked actor; unchanged unrelated ingress errors; durable-work
idle settlement; actual outbox presentation deadlines; and recovery
after authorization commits, with Stop and a lost commit acknowledgment.
- GREEN: all 75 cases in `task-retry.test.ts`,
`task-interruption-presentation.test.ts`,
`chat-failed-run-retry-boundary.test.ts`, and `chat-action-work.test.ts`
passed without a name filter.
- RED → GREEN: stopping after source authorization committed left a
stale waiting flag, so a fresh worker failed a previously authorized
Retry after the deadline. The fix saves that phase in the same
transaction. A real PostgreSQL commit followed by an injected lost
acknowledgment also reproduced generic settlement restoring the stale
flag. The corrected settlement preserves the stored phase. No wake is
sent on the ambiguous response. Recovery queues once after current
checks; Stop still prevents a successor. The deadline, contention count,
original input, failed run, and publications stay unchanged.
- Module boundary checks pass. Full workspace typecheck and the full
build passed at `d8a4a1a739aa6825714b04691d4166a00991808b`. At this
exact head, both required checks (`ci / verify` and `ci / e2e`) pass.
The inventory has 53 successful, three skipped, and one neutral check,
plus the automatic Paperclip Review in progress. The legacy Snyk status
passes. Greptile is 5/5 on this head, with no new comments or unresolved
threads. A current Paperclip assessment has not been published yet.
- The full local `pnpm test:run` did not pass: its second chat shard hit
a 15-second timeout in the HTTP/restart test after 348 and 293 passing
cases. One bounded diagnostic of the same 642-case shard pair passed,
with that case completing in 606 ms. The matching Linux CI pair passed
too. The original timeout remains unexplained and nonreproduced. No
timeout changed. The earlier stopped run was incomplete, and both
conflicting documentation additions are retained.
- CI chat 3/3 exposed a separate fixture assumption: restoration can
commit before a durable wake receipt exists. One known pre-receipt
delivery failure reproduces the same missing-run error. The corrected
fixture drives the existing worker until the exact canonical intent and
wake agree. It proves one real wake, one successor, exact source/actor
binding, and one restoration. The exact failed shard pair now passes
locally: 635 cases. Workspace typecheck also passes at test-only head
`94d691c9df61b05019b96900d5800a8f1c484e9a`. Production source is
unchanged from the previous verified build. That head completed all
registered checks: 53 successful, two neutral, three skipped, plus the
successful legacy status. Greptile scored 5/5. The subsequent Paperclip
4/5 persistence finding is addressed by the source repair above; its new
exact-head review is pending.
- Compatibility: 111 selected cases passed with #18895 head
`9b5d877161ffcbf16c0fb6920e8155a3620c85b0` in a disposable worktree. The
branch is now synchronized with master `4a83ba4050`, which includes that
change.
- Preserve the old CI failure: at head
`37954951fb66f3b50d0f2a3bec78d6993c479e87`, server shard 6/12 initially
failed the empty organization-audience replacement versus member-removal
assertion in `tool-access-service.test.ts`. The exact race family passed
on unchanged base and head; all 501 file tests and two 30-case controls
passed on base. The one Linux replay passed 119 files and 2,833 cases,
with three skipped. No guard or assertion changed. The initial failure
remains unexplained and nonreproduced; this PR does not claim to fix it.
- The prior external Superagent scan could not start because its
Security Graph returned HTTP 503. Its neutral check was not a completed
security assessment.
- These tests exercise the real Express route, PostgreSQL transactions,
and heartbeat queue. External chat transport is a fixture. They do not
run a live model or prove recovery of an original hosted task.

## Risks

- A still-pending source authorization wait fails after its fixed
60-second contention deadline. A completed wait is saved, but new
contention keeps the original deadline. A healthy lock can exceed that
limit. Other delivery errors retain the eight-attempt budget. Current
authority checks remain required before admission. Inspection does not
reset either limit.
- The new pending phase must never be treated as an authorized chat
source. Normal authorization rejects it until the worker prepares and
binds the exact source.
- The task projection reads the existing linked outbox deadline. It adds
a scoped join and does not create a timer or change a business deadline.
- No schema migration, provider identity change, or new delivery queue
is included.

## Model Used

OpenAI GPT-6 through Codex, with reasoning, repository editing, tool
use, and test execution. The host did not expose an exact deployment
snapshot 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] Required Paperclip CI gates are green (`ci / verify` and `ci /
e2e`; automatic review progress is recorded above)
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 07:18:48 -06:00
DottaandPaperclip c5251b4c0e fix(chat): retry fully recorded setup failures before dispatch (#18882)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Connected tasks retain their exact external request across failures.
> - Task Retry must preserve that request and prove that old execution
cannot continue.
> - A workspace setup failure can settle before any provider dispatch.
> - The chat retry guard accepted one specific validation failure but
rejected other fully recorded setup failures.
> - This pull request reuses the same strict server settlement proof for
those failures.
> - The benefit is one safe continuation through the existing queue,
with all source and execution gates intact.

## Linked Issues or Issue Description

Refs #18803, #18845, #18852, #18853 and #18860. These changes preserve
workspace writers, bind completed preservation, or repair other
preparation and native continuation paths. This change repairs the
separate connected-chat guard for fully recorded legacy setup failures.
Search also found #18876, which repairs issue scope during deferred wake
promotion and does not repair this guard.

**What happened?**

A connected task failed during workspace reservation before provider
dispatch. Its source remained authorized. Task Retry saved an intent and
chat action, but the chat worker rejected that request as
`failed_chat_retry_not_authorized`. No successor was created. The old
failure had a bootstrap marker, a dispatch-only Runner profile, the
matching server error and durable measured settlement events, and no
runtime, process, output, result or environment lease.

**Expected behavior**

A deliberate board Retry can continue the exact saved input after the
complete never-dispatched proof passes. An unfinished earlier writer,
changed source, accepted result or unknown execution state must still
prevent execution.

**Steps to reproduce**

1. Admit an authorized request through a restricted chat endpoint.
2. Settle setup before provider dispatch, for example while an earlier
workspace writer is unfinished.
3. Keep the exact server error and durable measurement events, a
dispatch-only Runner profile, and no execution resources.
4. Press task Retry after the independent writer and ordinary admission
gates permit work.
5. The original guard saves the Retry action but refuses to create a
successor.

**Paperclip version or commit**

Reproduced on master at `a00109aa35`. The branch is rebased on current
master.

**Deployment mode**

Hosted task diagnosis. Local proof uses real Express HTTP routes, chat
authorization, heartbeat queue admission and disposable embedded
PostgreSQL on macOS arm64, Node 26.4.0. Provider work is deliberately
prevented by occupied fixture capacity.

## What Changed

- Extend only canonical task Retry's existing strict pre-provider
preparation proof to generic server-settled setup failures.
- Keep exact result shape, dispatch profile, controller, runtime,
process, output, event and resource checks. No diagnostic message
allowlist is added.
- Exercise GitHub and Telegram HTTP Retry with the original sender and
saved input. Keep the failed run unchanged and create at most one queued
successor.
- Cover provider events, retained native state/results, modified source,
unknown result metadata, live controllers, process identity, source
leases and an independent unfinished writer.
- Document the guarded contract and correct the obsolete access-map
sentence. No new endpoint or agent permission is added.

## Verification

- Red before repair: both setup causes failed through the real
HTTP/chat/queue path because the durable Retry had no successor. An
initial retained-native negative fixture had an invalid foreign-key
setup; that fixture was corrected before the final run.
- Green: 37 focused integration cases pass, including 22 new
GitHub/Telegram cases and 15 existing pre-provider source and execution
guards.
- Current head `1bf69d20e02a049248ec378f087505563daeb9a0`: all 37
focused integration cases and full repository `pnpm -r typecheck` pass
after the master rebase. The full `pnpm build` also passes.
- Map structure, module boundaries and `git diff --check` pass.
- Full local `pnpm test:run` is running; all six chat shards passed and
the remaining server suites are progressing. This is not a full-suite
pass claim.
- Greptile and Paperclip Review both reviewed this exact head at 5/5
with no actionable findings or review threads.
- Initial CI passed all jobs except the unchanged Runner package suite.
Its `runtime-host.test.ts` approve-reads case failed before its intended
assertions because a managed credential home could not acquire its
loopback-port quorum. The exact case passes locally. A wider 121-case
check hit the same lease error in a different Pi case (120 passed); all
121 pass with one worker. The exact competing port owner is unknown. One
unchanged retry of only failed CI jobs passed, including the Runner
package suite and aggregate `ci / verify`. All current-head required CI
gates are green; the original failure remains in workflow history.
- The dependency setup initially missed the embedded PostgreSQL
package's dylib symlinks. Running its own postinstall helper fixed the
local setup before assertions. This was not a product test failure.
- This change has not been deployed. The original hosted task has not
been replayed with the fix.

## Risks

- Never-dispatched proof must remain strict. A bootstrap marker or empty
process fields alone cannot authorize continuation. The regression cases
retain contrary-state holds.
- The inherited session display hint remains history and supplies no
resume authority.
- Current connection, principal, source integrity, pause, budget,
approval, dependency and workspace-owner gates remain in their existing
owners.
- No schema, provider profile, trust policy, model or credential
permission changes are required.

## Model Used

OpenAI GPT-6 through Codex, with reasoning, code editing, tool use and
PostgreSQL/browser verification. This session does not expose a more
specific deployment 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-04 07:18:40 -06:00
DottaandPaperclip d8c1b9d4b5 fix(runtime): reclaim failed remote checkpoint retries (#18877)
## Thinking Path

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

## Linked Issues or Issue Description

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

**Paperclip version or commit**

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

**Deployment mode**

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

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

OpenAI Codex, based on GPT-6, with reasoning, tool use, code editing and
shell execution. The runtime does not expose the exact model snapshot or
context window size.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 07:18:07 -06:00