Commit Graph
2256 Commits
Author SHA1 Message Date
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
Devin FoleyandPaperclip 46fb933b01 fix(recovery): keep the stop receipt when a file-preserving restart fails (#19181)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs can execute in a remote sandbox. When a run fails,
Paperclip stops the sandbox and records the provider's stop receipt on
the environment lease.
> - System workspace recovery can restart that stopped sandbox to save
its files. Before the restart, it removed the stop receipt from the
lease.
> - If the restart failed and the provider could not stop the sandbox
after that, the lease had no receipt. Continuation admission requires a
stop receipt on every remote lease of the failed run. So every Retry and
every comment waited for an environment that never stopped.
> - This pull request keeps the original receipt until the provider
confirms the restart. It restores the receipt when the restart fails and
the sandbox cannot stop. A restart that fails because the sandbox disk
is full now ends recovery with a fresh-workspace outcome.
> - The benefit is that a failed file-preserving restart no longer
blocks the task permanently. Saved input and Retry continue in a fresh
isolated workspace, and the original sandbox stays retained.

## Linked Issues or Issue Description

No public issue exists. This is a bug report in the format of
`bug_report.yml`.

**What happened?**

1. A native run failed because its Daytona sandbox filled its 10 GB
disk.
2. Paperclip stopped the sandbox normally and recorded the stop receipt
on the environment lease.
3. Some minutes later, system workspace recovery tried to restart the
sandbox to preserve its files. Before the restart,
`prepareStoppedSandboxForRecovery` deleted the stop receipt.
4. The restart failed on the full disk. The sandbox went into the
provider's `error` state.
5. The stop-only teardown after the failed restart also failed, because
the provider cannot stop a sandbox in that state. The lease stayed in
`pending_cleanup` with no receipt. The cleanup sweep retried the same
failing stop with backoff.
6. `admitExplicitNativeContinuation` requires a provider stop receipt on
every remote lease of the failed run. Every Retry and every comment was
deferred with "Waiting for the previous environment to stop. Your
message will start automatically." The message never started.

Also, after recovery was exhausted, a Retry replaced the exhausted
generation with a new one. That generation repeated the same restart,
and the Retry still reported success.

**Expected behavior**

A lease must not lose its stop receipt while the real sandbox state is
unknown or not running. When the file-preserving restart cannot succeed,
recovery must end in a state that lets the task continue, for example in
a fresh isolated workspace.

**Steps to reproduce**

1. Use a native run with a retained remote sandbox lease that has a
`stopped` receipt and a pending workspace recovery.
2. Make the provider resume fail (for example with `no space left on
device`), and make the provider stop fail.
3. Run `recoverRetainedWorkspace` until recovery is exhausted.
4. Save a user comment on the task and run the deferred-comment
admission.
5. Before this change, the lease is `pending_cleanup` without a receipt.
The comment stays deferred with reason `remote_cleanup`, and no
successor run starts.

**Paperclip version or commit**

Observed on a hosted staging build. Reproduced on `master` at
`6452e964a36` with the new tests in this PR.

**Deployment mode**

Hosted, with the Daytona sandbox provider plugin. The defect is in core
server recovery code, so it is not specific to one adapter.

## What Changed

- `server/src/services/sandbox-stop-and-retain.ts`
- `prepareStoppedSandboxForRecovery` still writes the stop-only intent
and moves the lease to `pending_cleanup` before the provider call. It
now keeps the prior `remoteExecutionTermination` (and
`sandboxStopAndRetainReceipt`, if present) as `priorStop` inside the
`sandboxStopAndRetain` intent.
- New `confirmSandboxRecoveryRestart` removes `priorStop` when the
provider resume returns. From that point, the sandbox may be running,
and only a fresh stop can settle the lease.
- New `restoreSandboxStopAfterFailedRestart` restores the prior receipts
and the `released` / `success` lease state. It works only while
`priorStop` is present, so only when no restart was confirmed. It checks
the restored receipt with `hasRemoteTerminationReceipt` first. It uses
the same attempt fence as `settleStopOnlyCleanup`, and it writes through
`writeEnvironmentCleanup` so dispatch and cleanup wake up.
- `settleStopOnlyCleanup` now shares that attempt fence
(`stopOnlyAttempt`). Its behavior does not change.
- `server/src/services/workspace-recovery.ts`
- After `resumeWorkspaceRecoveryLease` returns, recovery confirms the
restart before any other check.
- If the restart is not confirmed, recovery still tries the stop-only
teardown first. A successful stop records a fresh receipt, as before. If
the stop also fails, recovery restores the prior receipt instead of
leaving the lease without one.
- A later attempt that finds a pending stop-only lease (for example
after a process restart during the reopen) uses the same rule when its
stop fails.
- A resume error that reports a full sandbox disk (`No space left on
device` or `Disk quota exceeded`) becomes `source_unavailable` /
`use_alternative_strategy`. The generation is exhausted at once, and the
existing fresh-workspace continuation applies. Other resume errors keep
the existing bounded retry policy (`provider_unavailable`, at most six
attempts in 30 minutes). Because the receipt stays, that policy also
ends in a state that lets the task continue.
- `server/src/services/task-retry.ts`: Retry binds to an exhausted
generation with a `use_alternative_strategy` failure and no fresh
attempt, the same way it binds to a complete or running generation. It
does not create a new generation that repeats a step the policy already
rejected. The Retry continues in a fresh isolated workspace. An explicit
workspace recovery request (`requestWorkspaceRecovery`) still starts a
new generation.
- `doc/task-recovery.md`: describes the receipt handling around the
reopen and the Retry binding.
- `server/src/services/workspace-recovery.test.ts`: four new tests (five
cases) with the real database and mocked provider calls.

### Design choice

I kept the existing write-ahead fence. The lease still leaves `released`
and shows no receipt while the provider reopen is in progress. So no
admission, reuse or destructive cleanup can act in that window, and a
crash still leaves a stop-only obligation. The original receipt is not
deleted. It stays in the stop-only intent and is discarded only when the
provider confirms the restart. This makes one simple rule: the suspended
receipt exists only while no restart has been confirmed.

I did not use "restore only when the provider confirms that the sandbox
is not running". The Daytona plugin reports a sandbox in the `error`
state as `active` in `inspectLease`, and its `stopLease` cannot stop
that state. So with the current plugin, no provider confirmation exists
for this exact failure, and the hold would stay. A separate change to
the Daytona plugin's `stopLease` can recover an errored sandbox before
it stops it. This PR does not depend on that change. If that change
lands and its recovery succeeds, the existing stop attempt records a
fresh receipt, and the restore path is not used.

I also did not change continuation admission to accept a lease in
`pending_cleanup`. Many gates treat pending cleanup as unfinished work
(`hasPendingRunCleanup`, the remote stop check and others). A change
there would be wider and more sensitive.

The restored receipt is the provider's original receipt, bound to the
same company, run, lease, provider, allocation and plugin. No process ID
or run status takes its place.

## Verification

All commands ran from `server/` in a clean worktree of `master` on macOS
(Apple Silicon), with the embedded PostgreSQL test database. Vitest ran
from the real path.

- Baseline on `master` (`6452e964a36`), before any change: `npx vitest
run src/services/workspace-recovery.test.ts` → exit 0, 331 passed.
- The new tests fail on `master`. I reverted only the three source files
and kept the new tests:
- `npx vitest run src/services/workspace-recovery.test.ts -t
"file-preserving restart fails|without repeating a restart|keeps the
stop obligation once|interrupted restart that the provider"` → exit 1, 4
failed, 1 passed.
- In the comment cases, no successor run starts (`expected [] to have a
length of 1`). A temporary log (not committed) showed the lease in
`pending_cleanup` in both cases. In the `unknown_failure` case, the
comment was deferred with `remote_cleanup` / "Waiting for the previous
environment to stop. Your message will start automatically." This is the
same hold as in the incident.
- The guard test "keeps the stop obligation once the provider has
confirmed a restart" passes on `master`, as expected. It makes sure that
the fix does not restore a receipt after a confirmed restart.
- With this change, the same command → exit 0, 5 passed.
- `npx vitest run src/services/workspace-recovery.test.ts` on the
committed head → exit 0, 336 passed (331 existing and 5 new).
- Neighboring suites (`task-retry`, `explicit-native-continuation`,
`heartbeat/recovery`, `environment-runtime`, and the other server test
files that import the changed modules: `live-recovery-work`,
`retained-native-session-cleanup-work`, `execution-control-work`,
`legacy-workspace-restore-recovery`, `deferred-execution-receipt-work`,
`issue-agent-mutation-ownership-routes`,
`native-workspace-export-retry`,
`native-cancelled-workspace-preservation`,
`native-workspace-warm-reuse`, `native-workspace-reconstruction`,
`native-failed-workspace-recovery`, `heartbeat/workspace-preparation`) →
exit 0, 16 files, 1456 passed.
- `npx vitest run src/__tests__/chat-channels.integration.test.ts
src/__tests__/slack-conversation-lifecycle.test.ts
src/services/native-runtime/preserved-native-response-wait.integration.test.ts`
(other importers of `task-retry`) → exit 0, 3 files, 2074 passed.
- `pnpm --filter @paperclipai/server typecheck` → exit 0.
- Not run: the full `pnpm -r typecheck`, `pnpm test:run` and `pnpm
build` repo checks. CI runs them. No live Daytona probe ran.

New tests:

- `lets saved input continue after a file-preserving restart fails on a
sandbox that cannot stop` (`disk_full`, `unknown_failure`): resume fails
and stop fails. The lease keeps its original receipt and is `released` /
`success`. Recovery is exhausted (after one attempt for a full disk, and
after six attempts for an unknown failure). The saved comment starts one
successor run with `fresh_isolated_workspace` readiness. The source
files stay in the retained sandbox.
- `continues a Retry in a fresh workspace without repeating a restart
that cannot succeed`: after a full-disk exhaustion, Retry keeps the same
generation and binds its intent. The restart is not called again, and
the Retry starts one run in a fresh isolated workspace.
- `keeps the stop obligation once the provider has confirmed a restart`:
resume returns, a later check fails, and the stop fails. The lease stays
`pending_cleanup` without a receipt, and the intent has no `priorStop`.
- `restores the stop receipt after an interrupted restart that the
provider never confirmed`: the lease starts in `pending_cleanup` with a
suspended receipt, as after a process restart during the reopen. The
stop fails, the receipt is restored, and the attempt continues to the
normal reopen.

Assumption and limits (runtime dependency rule): the provider calls are
mocked, and the database is real. The tests do not exercise the real
Daytona error text. The full-disk check matches the patterns that
Paperclip already uses for sandbox disk-full errors. If the provider
error text does not match, recovery uses the bounded retry path, which
now also ends in a fresh-workspace continuation. The change adds one
database write after each confirmed reopen and one after a failed reopen
and stop. It adds no polling.

## Risks

- A resume can fail after the provider started the sandbox, for example
when the reply is lost. If the stop-only teardown then also fails,
recovery restores a `stopped` receipt for a sandbox that can be running.
The old run's processes do not survive the earlier stop, so the receipt
is still correct as proof that the old execution ended. But the retained
sandbox can run until the provider stops it or a later recovery or
cleanup stops it. Before this change, the same case caused a permanent
hold.
- A full-disk restart failure now exhausts recovery at once. The task
continues in a fresh workspace without the failed run's files. The files
stay in the retained sandbox. An operator can still request workspace
recovery explicitly after the disk is freed.
- Retry no longer renews an exhausted generation that has a
`use_alternative_strategy` failure and no fresh attempt. For destroyed
or missing sources, the result is the same as before but without the
extra attempt.
- No schema or migration change. A suspended receipt is one extra field
in the existing `sandboxStopAndRetain` metadata. Older servers ignore
the field. The intent equality checks compare the stored value, so a
mixed-version deploy keeps working.
- #18820 also changes `server/src/services/workspace-recovery.test.ts`.
The two PRs touch different tests.

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

## Model Used

- Anthropic Claude Opus 5.5 (model ID `claude-opus-5-5`), running as a
Claude Code agent with tool use (shell, file reads and edits, local test
and typecheck runs) and extended reasoning. The runtime does not expose
the context window size to the agent.

## 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 repo
runs are left to CI, as noted 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
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 21:34:14 -08:00
Devin FoleyandPaperclip 895cee28b3 fix(daytona): recover an errored sandbox before stop-only cleanup (#19178)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Remote agent runs use sandbox provider plugins. Daytona is one of
them.
> - After a failed run, Paperclip stops the sandbox and keeps its files
("stop-only cleanup"). A task can continue only when every remote lease
of the failed run has a provider stop receipt.
> - Daytona rejects Stop for a sandbox in its `error` state. A sandbox
that filled its disk went to that state. The plugin called Stop on it,
so every cleanup sweep failed with the same generic error.
> - The lease stayed in pending cleanup without a stop receipt. Each
Retry or comment waited forever with reason `remote_cleanup`. A manual
Daytona Recover was the only fix.
> - This pull request makes the Daytona stop-only hook recover a
recoverable sandbox once, with a bound, and then stop it. When it cannot
stop the sandbox, it returns a typed failure. The server escalates a
terminal failure at once instead of retrying it like a transient error.
> - The benefit is that a full-disk sandbox no longer blocks its task,
and a sandbox that cannot recover is visible to operators on the first
attempt.

## Linked Issues or Issue Description

No public issue exists. Bug description:

**What happened?**

A task sandbox filled its 10 GB disk. Daytona then reported the sandbox
as `state: error`, `desiredState: started` and `recoverable: true`. Its
`errorReason` was a Docker `no space left on device` error. The Daytona
`stopLease` hook calls Stop on any sandbox that is not `stopped` or
`archived`. Daytona rejects Stop in the `error` state. The pending
cleanup sweep ran about every 30 minutes, and it failed on every pass.
The lease stayed `pending_cleanup` with `cleanup_status=failed` and no
stop receipt. Continuation admission
(`server/src/services/explicit-native-continuation.ts`) requires a stop
receipt on every remote lease of the failed run. So each Retry and
comment was deferred with "Waiting for the previous environment to stop.
Your message will start automatically." It never started.

A manual Daytona `POST /api/sandbox/{id}/recover` fixed it. The
sandboxes reached `started` in about one minute. The next sweep stopped
them, recorded the receipt, and the tasks continued.

**Expected behavior**

Stop-only cleanup recovers a recoverable errored sandbox and then stops
it. When the sandbox cannot be recovered, cleanup returns a clear, typed
failure that the server can tell apart from a transient error. Cleanup
never deletes the retained sandbox, and it never loops on Recover.

**Steps to reproduce**

1. Run a task in a Daytona sandbox until the sandbox fills its disk and
Daytona shows `state: error`, `recoverable: true`.
2. Let the run fail, so the server records a `sandboxStopAndRetain`
intent and calls `environmentStopLease`.
3. Before this change, Stop fails with `Sandbox failed to stop with
status: error` on every sweep, and the task cannot continue.
4. The new tests in `plugin.test.ts` reproduce this with a mock sandbox
whose Stop fails from `error`.

**Paperclip version or commit**

Observed on a hosted deployment. Reproduced in unit tests on `master` at
`6452e964a36`.

**Deployment mode**

Managed Daytona sandbox execution with plugin-backed stop-only cleanup.

Related: #18172 (Daytona exports under disk pressure) and #19050
(session reset on every Daytona restart path). This PR does not change
the resume path in `ensureSandboxStarted`, which already recovers a
recoverable sandbox.

## What Changed

- **Daytona plugin
(`packages/plugins/sandbox-providers/daytona/src/plugin.ts`)**: after
`stopLease` confirms the retention policy, a sandbox in `error` goes
through `recoverSandboxForStop`.
- Recoverable: the hook calls `sandbox.recover()` once. The SDK waits
for `started`. The plugin bounds the wait to 20 s, plus the existing
liveness margin for a hung connection. Then the hook stops the sandbox
normally and returns the usual `stopped` receipt.
- Not recoverable: the hook makes no Recover or Stop call. It throws
`PluginEnvironmentStopError("resource_unrecoverable")`.
- Recover fails: the hook reads the provider state once. A sandbox back
in `error` gives `recovery_failed`, or `resource_unrecoverable` if
Daytona now marks it so. A sandbox that is still starting, or a failed
read, gives `recovery_incomplete`.
- The recovered sandbox fails again before Stop finishes (for example,
the disk fills again): a fresh read that shows `error` gives
`recovery_failed`. The hook never calls Recover a second time in the
same call.
- The hook never deletes or archives the sandbox. The non-error stop
path is unchanged.
- **Plugin SDK (`packages/plugins/sdk/src/environment-stop-error.ts`,
new)**: `PluginEnvironmentStopError`, `environmentStopErrorData` and
`readEnvironmentStopFailure`. This follows the existing
`PluginEnvironmentRunnerIngressError` pattern. The envelope holds only
one closed reason. The reader validates the reason and derives
`terminal` from it. `resource_unrecoverable` and `recovery_failed` are
terminal. `recovery_incomplete` is not.
- **Worker RPC host (`packages/plugins/sdk/src/worker-rpc-host.ts`)**:
forwards that envelope as JSON-RPC error `data` for
`environmentStopLease` only.
- **Pending cleanup sweep
(`server/src/services/heartbeat/recovery.ts`)**: the catch reads the
validated envelope.
- On a terminal failure, it escalates on the first attempt. It writes
the one-time "needs operator attention" warning and uses the existing 30
minute retry interval. Before, this happened only at the fifth attempt.
- The failure log uses `errorKind: "provider_stop_terminal"` and
includes the closed `providerStopFailure` reason. The sweep still never
logs exception fields.
- The lease stays `pending_cleanup`. Automatic cleanup continues, and an
explicit Retry still bypasses the cooldown. Untyped and transient
failures keep their current behavior.
- **Docs**: the SDK README ("Stop failures"), the Daytona README,
`doc/plugins/SANDBOX_PROVIDER_CAPABILITIES.md` and
`doc/native-workspace-finalization-recovery.md` describe the new
behavior.

Shared contract note (module boundaries): the SDK envelope is the
smallest addition that lets the host tell a terminal provider stop
failure from a transient one. The worker host drops all other error data
across the RPC boundary, so the existing contracts cannot carry the
signal. The only consumer is the pending cleanup sweep. When a provider
does not throw the new error, or an older host ignores it, behavior is
unchanged. This PR does not touch
`server/src/services/workspace-recovery.ts` or
`sandbox-stop-and-retain.ts`.

## Verification

All commands ran in a clean worktree from `master`. Vitest ran from the
real path.

- Red on `master` (only the new tests added):
- Daytona, with `plugin.ts` from `master`: `npx vitest run --config
vitest.config.ts src/plugin.test.ts -t "provider error state"` exits 1.
All 7 new tests fail. The main case fails with `Sandbox failed to stop
with status: error`, which is the production error.
- SDK, with `worker-rpc-host.ts` from `master`: `npx vitest run
tests/worker-rpc-host.test.ts -t "typed stop failure"` exits 1 (the
typed case fails).
- Server, with `recovery.ts` from `master`: `npx vitest run
src/__tests__/heartbeat-pending-cleanup-sweep.test.ts -t "terminal
provider stop failure"` exits 1. All 3 cases fail (`errorKind:
"destroy_failed"`, no escalation).
- Green on this branch:
- `packages/plugins/sandbox-providers/daytona`: `npx vitest run --config
vitest.config.ts` exits 0 (534 passed, 26 skipped live tests). `pnpm
typecheck` exits 0. `pnpm build` exits 0.
- `packages/plugins/sdk`: `npx vitest run` exits 0 (222 passed). `pnpm
--filter @paperclipai/plugin-sdk typecheck` exits 0.
- `server`: 15 related suites exit 0 (1498 passed): pending cleanup
sweep, `heartbeat/recovery`, environment runtime, environment cleanup
work, native startup cleanup, native remote failure retention, observed
workspace preservation, retained native session cleanup, idle sleep
safety, explicit native continuation, task retry, workspace recovery,
native failed and cancelled workspace preservation, and Daytona close in
the plugin worker manager.
- `pnpm --filter @paperclipai/server typecheck` exits 0. `pnpm
check:module-boundaries` exits 0.
- New tests:
- Daytona: a recoverable errored sandbox is recovered once
(`recover(20)`) after the retention check and before Stop, then stopped.
An unrecoverable one returns `resource_unrecoverable` with no Recover,
Stop, start, delete or archive call. A failed recovery makes exactly one
Recover call for each outcome (`recovery_failed`,
`resource_unrecoverable`, `recovery_incomplete`). A Recover call that
never settles is cut off by the bound. A recovered sandbox that fails
again before Stop gives `recovery_failed` with no second Recover.
- SDK: the envelope holds only the reason. A forged or non-SDK error
gives no data. Unknown reasons, accessor fields and a sender-supplied
`terminal` flag are not trusted. The worker host forwards the typed
failure and drops a plain error's detail.
- Server: terminal reasons escalate on attempt 1 with the long retry and
one operator warning. `recovery_incomplete` keeps the short retry. The
lease stays `pending_cleanup`, and the provider message never reaches
the logs.
- Not run: the full `pnpm -r typecheck`, `pnpm test:run`, `pnpm build`
and `pnpm run typecheck:build-gaps` repo checks. CI runs them. No live
Daytona probe ran. The mocks model the Daytona SDK `recover()` contract:
it waits for `started` and rejects when the sandbox returns to `error`.

## Risks

- Time budget: the host gives `environmentStopLease` at most 60 s. A
recovered stop uses up to 20 s for Recover and up to 30 s for Stop, plus
short reads. A slower recovery returns `recovery_incomplete`. The SDK
call can continue at Daytona after that, so the next sweep finds the
sandbox started and stops it normally. If the host deadline fires while
the worker still stops the sandbox, the next sweep sees `stopped` and
records the receipt.
- Classification: after a failed Recover, the plugin cannot tell a
rejected request from a sandbox that started and failed again. Both give
`recovery_failed` (terminal) when Daytona still shows `error`. A hung
Recover call with a sandbox still in `error` is also terminal. Terminal
failures still retry every 30 minutes, and each retry makes at most one
Recover call. An explicit Retry also makes one attempt. There is no
tight loop. A sandbox that fails Recover every time gets about two
Recover calls an hour until an operator acts.
- Recover restarts the sandbox container before Stop. The plugin runs no
command in it, and the retention policy is confirmed before Recover.
- The continuation message for a terminally failed lease still says the
message "will start automatically". This PR does not change admission. A
follow-up can use the typed reason there.
- Compatibility: a host without this change ignores the new error data
and keeps the old retry behavior. A plugin without this change sends no
typed data.

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

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with
tool use (shell, file edits, code execution for tests and typechecks).
The runtime does not expose the context window size to the agent.

## 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 repo
runs are left to CI, as noted 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
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 21:33:48 -08:00
DottaandClaude Opus 5.5 2141efc097 fix(native): answer Stop during slow native startup with 409 and settle it as cancelled (#19189)
## Thinking Path

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

## Linked Issues or Issue Description

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

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

---------

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

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

## Linked Issues or Issue Description

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

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

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

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

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

## What Changed

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

## Verification

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

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

Green after the fix:

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

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

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

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

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

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

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

## Risks

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

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

## Model Used

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

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 23:24:34 -06:00
DottaandClaude Opus 5.5 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
DottaandPaperclip f7a4c5678f feat(runtime): pre-copy completed harness history before enrollment (#19137)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - An ephemeral web server needs provider history in shared object
storage.
> - Offline enrollment must preserve old backups before it can detach a
volume.
> - A large copied dataset still exceeded the maintenance deadline with
parallel transfers.
> - Most of its bytes belong to successful turns with released runtime
authority.
> - This pull request pre-copies that history while unrelated executions
continue.
> - The final offline migration still verifies every source and grants
enrollment authority.

## Linked Issues or Issue Description

Refs #19108, #19038, #18943.

**What happened?**

A copied instance has 1,953 local harness backups and 54.8 GB of
declared source bytes. A resumed parallel migration still reached its
25-minute deadline. It cannot safely enroll by moving all history during
maintenance.

**Expected behavior**

Copy eligible completed history while the server remains online. Defer
active, failed or changed sources. Keep all existing data and final
enrollment checks.

**Steps to reproduce**

1. Retain many local native harness backups on a volume-backed instance.
2. Attempt a full offline conversion within the maintenance deadline.
3. Observe that most time moves historical bytes rather than current
execution state.

## What Changed

- Add finite `native-harness-prestage.mjs` preparation from a normal
runtime build. Require explicit opt-in and shared object storage.
Process one source at a time with a 90-minute deadline.
- Use the recorded released runtime owner of each successful, finished
turn. Do not assert that all writers stopped.
- Recheck checkout, related queued/running work, successful result
ownership, session, Runner, exact lease stamp and unchanged backup at
database publication fences.
- Defer source changes, malformed source JSON and unsafe backup roots.
Log each such source and its reason. Keep database failures and
cancellation fatal. Keep completed objects valid for retry. Never return
an enrollment-ready receipt.
- Reuse the bounded migration source walker. Keep offline admission and
final verification unchanged.
- Keep private lifecycle state, task ownership and provider policies
unchanged. Document the operator path and its limits.

## Verification

- A zero-source scan now checks the existing database obligation query.
A known mismatch blocks with a source-missing receipt; empty
fresh/already-durable scans and nonempty deferred scans remain valid
advisory results. The prior command fails the mismatch regression. All
16 command and runtime-bundle tests pass for this follow-up.
- At head `7ba3dbc7aa`, 87 focused command, database-timeout, storage,
archive, publication and runtime-bundle tests passed with real
PostgreSQL and filesystem restoration.
- The current follow-up passes all 68 backup and archive tests. A source
renamed after preparation fails the new regression on the previous head,
then defers and permits the next backup with this fix. An archive output
ENOENT remains fatal. Existing database-error and cancellation tests
pass.
- New race tests fail against the previous storage owner: a changed
backup, queued turn or revoked lease otherwise publishes.
- Tests prove unrelated executions stay running, same-task work defers,
failed history stays deferred, retry verifies existing objects, and
private bytes restore after deleting the old home.
- Command review fixes require the configured existing vault key, retain
it for the process, set database lock/statement/idle bounds, close
clients on cancellation, and cap shutdown at ten seconds. Abort
immediately sets failure status and prints one blocked receipt. A real
child with real shortened timers reproduces the old missing-receipt exit
and verifies the fix. Tests also use a real PostgreSQL lock.
- Malformed manifest or runner JSON and symlink replacement fail the new
preparation and publication-fence regressions on the previous code. The
fixed command defers each damaged source and copies the next eligible
backup. Separate tests keep database SyntaxError and cancellation fatal.
Offline migration still rejects damaged sources.
- The bundle test runs the emitted preparation entry with plain Node
without starting the app.
- A prior copied-data experiment completed with 1,738 new publications,
nine verified current objects and 206 deferred sources. Later checks
found different database and filesystem capture times: 43 source runs
were absent from the older database snapshot, and one required
historical manifest was absent from the retained filesystem archive.
This experiment does not prove a consistent restore or enrollment
readiness.
- Full current-head CI at ce63517d0c
passed: 54 successful checks, two skipped and one neutral, with no
failed or pending statuses. Typecheck, tests and build pass. CI:
https://github.com/paperclipai/paperclip/actions/runs/41970370960. The
earlier superseded run lost a spot runner after a shutdown signal; that
infrastructure cancellation is retained and is not counted as a passing
run.
- Greptile and Ponytail both rate this exact head 5/5; all actionable
review threads are resolved. Comment-only cleanup removes newly added
prose comments under CONTRIBUTING.md. TypeScript output with
removeComments:true is byte-identical for all seven changed TypeScript
files; 20 existing command, runtime and bundle tests pass after cleanup.
- Serving enrollment, full normal-image migration, empty-home
restoration and active-agent release handoffs remain separate acceptance
gates.

## Risks

- Preparation still consumes CPU, disk and storage requests. It uses one
transfer and at most two database connections. Check memory headroom for
a separate operator process.
- A task can gain work during upload. The final fence rejects that
publication and retains the previous current object.
- Deferred sources remain migration obligations. Preparation never
permits detaching the volume.
- Ordinary backup callers do not opt into the new preparation constraint
and keep their existing behavior.

## Model Used

OpenAI GPT-6.1 Codex, exact model ID `gpt-6.1-sol`. Used for reasoning,
code editing, shell tools and test execution. The runtime does not
expose its context-window size.

## Checklist

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

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Offline workspace import must save files before local volume
removal.
> - A task recovery hold can remain after native file finalization
succeeds.
> - The old importer rejects that hold even when its file source is
exact and no controller owns the run.
> - Clearing the hold would discard the task's recovery obligation.
> - This pull request saves files and carries each exact recovery source
forward without resolving the hold.
> - Accepted-result finalizers keep their existing retry path.

## Linked Issues or Issue Description

Refs #19144, #18943, #19038. This PR builds on merged #19144.

**What happened?**

Offline import reports `workspace_recovery_unresolved` for terminal
native runs with finalized workspace references and active task recovery
holds. Some holds also name an exact historical file allocation.

**Expected behavior**

Save the workspace durably. Keep task warnings, recovery rows, results,
coordinator phases and task pointers unchanged. Retain exact future file
recovery and accepted-result finalization retries.

**Steps to reproduce**

1. Retain a local task workspace and an active recovery hold for a
finalized native run.
2. Stop all application writers through offline maintenance.
3. Attempt workspace import.
4. Observe that the old importer rejects the hold.

## What Changed

- Add an owner module that checks the exact terminal native run,
finalized reference, released ownership and task relationship.
- Require the normal finalizer's successful company/run/task receipt for
an accepted-result retry. Support historical receipts without a
workspace foreign key.
- Validate recorded file allocations with the existing recovery-source
owner.
- Journal and recheck hold evidence. Lock recovery, coordinator, run and
allocation rows before publication.
- Give finalized file sources a separate migration marker. Keep legacy
execution-replay suppression out of accepted-result retry paths.
- Use the owning parser for older finalized references that omit
optional fields.
- Redirect future file recovery to immutable snapshot preservation. Keep
provider-stop and source checks intact.
- Verify native copyback or snapshot receipts for each exact allocation.
Require proof for every source, including completed mixed repairs.
- Retain existing successful copyback receipts. Keep a historical
folder's obligation on its real source workspace.
- Retain the finalized migration marker through the execution blocker
projection. Discover finalized child-task sources from their exact
ancestor workspace reference.
- Document the receipt and lock contracts.

## Verification

- Fresh local run at head 6ebe535751: 208
tests pass across the new preservation suite, migration suite,
publication concurrency suite and legacy restore recovery suite.
- `node ../node_modules/vitest/vitest.mjs run --config vitest.config.ts
src/services/finalized-workspace-recovery-migration.test.ts
src/services/remote-workspace-migration-concurrency.test.ts
src/services/remote-workspace-migration.test.ts
src/__tests__/legacy-workspace-restore-recovery.test.ts` from `server/`.
- Comment-only cleanup `a858d203c68a6c86fd39e30fb63dc56c1a303429`
removes added prose comments under CONTRIBUTING.md. TypeScript 6.0.3
output with `removeComments: true` is byte-identical before and after
for all six changed TypeScript files. Runtime behavior and the 208-test
evidence are unchanged; no new tests were added for this cleanup.
- The two file-restoration cases fail against the original importer with
`workspace_recovery_unresolved`.
- Positive tests delete the old folder and restore the saved archive.
They compare unchanged task, recovery, result and coordinator rows.
- Negative controls cover unknown kinds, mismatched sources, malformed
file markers, active ownership, active recovery and changed evidence
during upload.
- Red/green tests exercise getExecutionBlocker and the ancestor import
path. An accepted result without a successful finalize receipt remains
blocked.
- The mixed-receipt regression fails before the review fix and passes
afterward. A missing secondary receipt still blocks repair.
- Existing and future repair receipts remain readable. Accepted-result
finalization still returns its original successful receipt.
- Local server typechecking passed before the master replay. The
post-replay local check found a stale linked Runner declaration in
unchanged vendor code. Clean current-head CI Typecheck + Release
Registry and Build both pass.
- Greptile gives current head `a858d203c68a6c86fd39e30fb63dc56c1a303429`
5/5. All actionable findings are fixed and resolved.
- The prior CI run had one workspace shard cancelled after its spot
runner received a shutdown signal. No test assertion failed in that job.
That incomplete run was superseded by the equivalent comment-only
cleanup. The fresh full CI run passed on the current head; the original
infrastructure cancellation remains recorded here.
- [Current-head full
CI](https://github.com/paperclipai/paperclip/actions/runs/41970385102)
passes: 54 success, one neutral and two skipped statuses, with no failed
or pending checks. Typecheck, tests and build pass.
- Ponytail's source review rates the behavior head 5/5. It did not
execute tests; the local and CI runs above supply execution evidence.
- No provider, serving migration or copied-data deployment acceptance is
claimed.

## Risks

- This path depends on the existing offline deployment fence. It does
not prove that providers stopped.
- Unknown or unverified source boundaries remain blocked.
- Migration receipts must remain exact. Missing or changed finalized
file-source receipts fail closed.
- Root maintainers own merge, deployment and copied-data acceptance
#19144 is already merged.

## Model Used

OpenAI GPT-6.1 Codex, exact model ID `gpt-6.1-sol`. Used for reasoning,
code editing, shell tools and test execution. The runtime does not
expose its context-window size.

## Checklist

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

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

## Linked Issues or Issue Description

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

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

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

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

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

## What Changed

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

## Verification

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

## Risks

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

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

## Model Used

- Provider: Anthropic. Model: Claude Opus 5.5 (`claude-opus-5-5`), in
the Claude Code agent harness.
- Capabilities used: tool use, shell and code execution, repository
search, read-only browser reads of staging, and extended reasoning.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 23:07:42 -06:00
Nicky Leach 8c062fa481 fix(native): deliver plugin VM Runner usage to Core (#19162) 2026-11-04 21:02:22 -08:00
Nicky Leach 53937be283 fix(agents): select environment plugins only for agents on that environment (#19173) 2026-11-04 20:59:02 -08:00
Nicky Leach dc46b575ac fix(plugins): deliver agent runtime events only to the owning plugin (#19170) 2026-11-04 20:57:19 -08:00
DottaandPaperclip f5f5184f23 fix(workspaces): preserve retained branches and private commit sources (#19147)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Ephemeral web servers must preserve existing task files before
volume removal.
> - An old private checkout can be absent while its recorded branch
still exists.
> - A deleted branch can also retain its exact commit in the original
private object source.
> - The importer currently rejects these surviving sources.
> - This pull request saves the verified committed tree and records its
exact source proof.
> - It keeps task state, branch ownership and recovery obligations
unchanged.

## Linked Issues or Issue Description

Refs #19138, #19144, #18826.

**What happened?**

Existing Git workspace records can have missing checkout folders and
retained branch objects. The importer returns
`local_source_unsafe_or_missing`. A successful deletion receipt can also
have a valid private object source that the importer overlooks in favor
of a missing or changed project cache.

**Expected behavior**

Save the surviving committed tree from the exact recorded branch or
deletion receipt. Reject competing owners, live worktree registrations,
changed branches and invalid source identities. Do not use the current
default branch as a substitute.

**Steps to reproduce**

1. Retain an isolated workspace branch in its managed project cache.
2. Remove its clean checkout but keep the branch and original workspace
record.
3. Attempt workspace migration.
4. Alternatively, retain a deleted commit in its recorded private source
while its shared source is unavailable.

## What Changed

- Pin a retained branch in its verified company and project Git cache.
- Require the recorded private path, no competing workspace owner and no
surviving Git worktree registration or index authority.
- For a user-owned branch, require its exact successful unforced removal
receipt after archival. Preserve the branch ownership metadata.
- Prefer a verified recorded private object source for successful
deletion receipts when that source still exists.
- Recheck branch and receipt proof before publication. Preserve
compatibility with existing migration journals.
- Allow missing active records only through the existing offline writer
fence. Keep their status unchanged.
- Add byte-restoration and rejected-authority tests. Document the source
limits.

- Revalidate and honor interrupted journals' original Git source path.
Reject detached registrations and retained orphan index data. Reuse the
cleanup producer command formatter for paths containing spaces.

## Verification

- 172 focused migration, concurrency and isolated-source tests pass with
real PostgreSQL and Git repositories.
- The previous importer fails the retained-branch and offline-active
restoration cases. The previous source selection also fails the
private-cache and clean user-owned receipt cases.
- Tests remove the old home and restore the pinned commit and task file
bytes. New shared default-branch work is excluded.
- Tests reject competing owners, registered worktrees, missing branch
ownership, deleted branches, changed branch tips, forced removal and
revoked receipts.
- Read-only re-inventory of 79 actual failed source records makes 78
eligible with committed owner overlays. One older local test repository
remains blocked. This is source-overlay qualification, not full
released-image acceptance.
- A separate bounded archive replay completed six imports before its
25-minute deadline. That timing failure remains open; it does not
establish complete migration.
- Local server typecheck has no changed-file diagnostics. Stale linked
declarations prevent a clean local whole-server result. Full clean CI
remains required.
- Serving enrollment and empty-home recovery remain separate acceptance
gates.

- Latest rebased head 9f42901683 passes 172 focused
migration/archive/concurrency tests. Four review regressions fail on the
previous source; the revoked journal source already rejected correctly.
The real cleanup fixture uses an unforced removal under a home path
containing spaces. The orphan-index fixture is locked to keep Git
maintenance from deleting its synthetic retained state before
inspection.
- The prerebase layered server typecheck passed. Current-head full CI
remains required after integrating master. The local environment reset
removed earlier temporary logs; the current red/green logs and source
now live in persistent worktree folders.

- Current-head full CI passes on run 41965901512 after one bounded
failed-shard retry. The first run failed an unrelated status-card
fake-clock deadline assertion by 150 ms; the same source passed the
retry. Its cause remains unestablished, and the first failure log
remains in Actions and persistent local evidence. No migration-related
test failed.

## Risks

- A retained branch saves committed data that still exists. It cannot
certify recovery of files that were already absent.
- User-owned branches remain user-owned. A clean removal receipt grants
preservation authority, not deletion authority.
- Any retained registration, conflicting record or changed proof keeps
migration blocked.
- This PR does not resolve the remaining source, recovery or migration
timing gates.

## Model Used

OpenAI GPT-6.1 Codex, exact model ID `gpt-6.1-sol`. Used for reasoning,
code editing, shell tools and test execution. The runtime does not
expose its context-window size.

## Checklist

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

> - Paperclip manages AI agents and preserves their work.
> - Workspace migration must keep saved files and their recovery
records.
> - A crashed finalization attempt can remain running after a later
attempt succeeds.
> - The old row then blocks migration even when the saved result and
export have exact receipts.
> - Offline maintenance can reconcile the obsolete attempt after Cloud
stops all old writers.
> - This pull request fails only that attempt and records the receipt
that supersedes it.
> - Missing proof still blocks migration.

## Linked Issues or Issue Description

Related storage migration work: #18841 and #18943.

**What happened?**

An old running workspace finalization row can block offline enrollment
after a later finalization has saved the same run and workspace.

**Expected behavior**

Maintenance should reconcile the old attempt when exact saved receipts
prove it is obsolete. It must preserve the accepted result, successful
receipt, execution ownership, local files and recovery records.

**Steps to reproduce**

1. Retain a running finalization attempt for a native run.
2. Commit an accepted run result, a finalized workspace reference and a
later successful finalization receipt for the same company, task, run
and workspace.
3. Stop all application and worker deployments. Run offline enrollment.
4. The old attempt blocks enrollment. This change records it as failed
with receipt provenance before workspace import.

## What Changed

- Add a bounded offline reconciliation service for local workspace
sources that still need migration.
- Require the stopped-writer acknowledgement, maintenance switch and
existing active-execution gate.
- Lock the finalizer ownership, run, coordinator, workspace, accepted
result and operation receipts. Hold busy or incomplete records.
- Fail only the obsolete attempt. Preserve its metadata and add the
superseding receipt, result and workspace digests. Persist the activity
audit in the same transaction.
- Run reconciliation before workspace import. Keep unrelated, uncertain
and saved-work obligations held.
- Add real PostgreSQL tests for receipt proof, company scope, ownership
races, queued work, audit rollback and repeated maintenance.

## Verification

- `cd server && node ../node_modules/vitest/vitest.mjs run
src/services/offline-workspace-operation-reconciliation.test.ts
src/services/remote-workspace-migration.test.ts
src/services/remote-workspace-migration-concurrency.test.ts
src/maintenance/ephemeral-migration-service.test.ts`: 135 tests passed
across four suites before the final integration test. The final focused
suite passed all 33 cases, including a real
local-files-to-object-storage workspace import after reconciliation.
- The local server TypeScript check found one unrelated stale Runner
declaration: `codexUsageBaselineForNextRun` is absent from the reused
build output. It found no error in the changed files. CI must verify
fresh build outputs.
- Fresh GitHub Actions CI passed on
`d302fbddd430d42346611302ad7307cf809c3449`, including typecheck, build,
Runner, server and browser gates. Greptile gave 5/5 on the same head
with no findings.
- The external Paperclip Review check remains queued while it waits for
its assigned reviewer agent. Do not merge until that check passes.
- This PR does not claim live-data or deployed migration acceptance. A
legacy preparation attempt without a superseding receipt remains held.

## Risks

- The Cloud caller must establish the stop-all-deployments fence. An
environment switch cannot prove that old writers stopped.
- Only a succeeded native run with an unowned committed finalizer, an
accepted result, a finalized matching workspace and a later completed
successful receipt can pass.
- Future controller leases, retained workspace owners, busy rows,
uncertain work and independent save-work obligations stay held.
- Expired legacy controller lease fields remain unchanged. The service
changes no run, task, lease, recovery or source authority.

## Model Used

OpenAI Codex, exact model ID `gpt-6.1-sol`. Context window size is not
exposed. Used code reasoning, repository tools and local 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
- [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 22:25:08 -06:00
DottaandPaperclip a0250284e3 fix: preserve proposal revision turns and prove chat completion (#19105)
## Thinking Path

> - Paperclip manages work done by AI agents.
> - A completed delegated task must report its result to the originating
chat.
> - That report uses a real provider turn and must stay in cost
accounting.
> - Ordinary first-task proposals must also support the promised chat
acceptance path.
> - A revision response can reject an older proposal before it creates
the replacement.
> - The old rejection transition clears that active response's ownership
and blocks its next tool.
> - This change proves required completion work, guides ordinary
proposal policy and preserves the revision turn under the existing
locks.

## Linked Issues or Issue Description

No separate issue exists. The original failures remain in [the complete
Product
run](https://github.com/paperclipai/paperclip/actions/runs/41915965118).
The independent continuation repair is #19076. Related #12209 concerns
prior-run confirmation binding. This change fixes the ownership reset
during rejection, rather than changing binding authorization.

**What happened?**

The complete 20-case run finished with 13 passes and 7 failures. Three
delegated-chat cases counted one required `chat_task_completed` provider
turn as unexpected work. Two first-task agents chose optional human-only
cards, then correctly received 403 when they tried conversational
acceptance.

A [reviewed two-case candidate
run](https://github.com/paperclipai/paperclip/actions/runs/41931445311)
then used the changed first-task skill. Both models inherited the
ordinary resolver policy. Clarify/propose/accept passed all 12
predicates. Revise/accept failed after the new response rejected its
predecessor's proposal. The saved rejection was followed by
`paperclip_runner_tool_binding_not_authorized` on its next tools. The
final `native_session_lease_lost` label followed those binding failures;
this PR does not assume lease expiry caused them. Both cells cleaned up
and published their original grades.

**Expected behavior**

Keep the existing ordinary-turn bounds and account for one independently
proved completion notification. Inherit the normal resolver policy for
ordinary proposals that accept chat replies. When an active response
rejects an older proposal, save the rejection and retain that response's
current ownership so it can create the revised proposal. Keep human,
current-source and native completion-review rejection behavior and all
real approval restrictions.

**Steps to reproduce**

The original full run used `4740d251`. The two-case candidate used
trusted workflow `7d1e9b34` and target `74da56c4`. Run the focused
provider-free controls below. The dual-run API regression first fails
because rejection changes the issue to `todo` and clears the newer run's
execution and checkout fields.

## What Changed

- Prove one completion notification only for native Codex/local
`multi-repository`, `clarify-reuse` and `plan-handoff`. Keep the
ordinary bounds of 2, 3 and 4. Preserve actual totals of 3, 4 and 5, all
run IDs and all usage.
- Read those declared totals from the current catalog, which now
includes the completion reply. Check the total once and prove that
exactly one turn is the required notification. Do not add another turn
while waiting. The required-server controls use the actual catalog
tasks.
- Bind the notification to the same company, agent, conversation
generation, completed child, single delivery, accounted native run,
saved reply and rendered result link. Reject extra work, duplicate
deliveries, unknown events and mutations, including native work without
a Paperclip tool call.
- Collect workspace operations through the real run API for these three
proofs. Keep other collector calls unchanged. Keep the proof controls in
the existing required server test lane.
- Stop idle polling on a permanent completion-proof error. Continue to
retry transient observations within the original 240-second window.
- Guide ordinary chat-accepting first-task proposals to omit optional
`resolverPolicy` and inherit server/company defaults. Preserve explicit
and governed human-only restrictions. Do not lower or recreate saved
restricted cards.
- Under the existing locked issue and interaction rows, preserve the
active responding run when it rejects a different source run's proposal.
Change only the ordinary requeue predicate. Keep the saved rejection,
notifications, activity and existing Telemetry calls. Keep global
status, lease and tool-authority guards unchanged.
- Extend the existing native API controls with the actual dual-run
rejection/revision sequence and policy, provenance, human,
current-source and native completion-review countercontrols. No new
workflow or test framework is needed.

## Verification

- Current combined head is `a8567ded67e5ab811675390e9a5c5ed2194aee2f`,
integrated cleanly on `fb96537e28c4c7bfced4939909cb334392320868`. The
upstream completion-reply waiter is preserved. [Current required
CI](https://github.com/paperclipai/paperclip/actions/runs/41940780852)
passes. Actual test checkout `ff93c034d7bb40f3df7bc75cf51828fd07bb0d29`
merges this head into `9c6993f0aab7ebb22a26d78f5d227accb35a6ad1`.
- Fresh Greptile is 5/5 on exact head `a8567ded`, with no blocking
finding. All addressed review threads are resolved. The separate
automated reviewer emitted a paused-work response; that response is not
a review approval.
- Current required server CI actually passed all 11 native confirmation
API cases, 46 completion-proof controls and 13 onboarding asset checks.
Canonical typecheck, Build, the native package/server suites, all
browser and serialized shards, `ci / e2e` and `ci / verify` pass. No
current-head CI retry was needed.
- Local dual-run RED: the unmodified transition returned HTTP 200 and
saved rejection, then changed the issue to `todo` and cleared the newer
run's execution/checkout fields. Whole setup and check took 28.95
seconds; the case took 596 ms.
- Local treatment: all 11 real native/API cases pass. The same-session
response retains execution/checkout ownership, reads task context and
persists the recorded-shape revised confirmation. Its stored lease
receipt stays intact and one rejection activity is saved. Human-only
rejection, current-source rejection, human rejection with/without a
newer owner, Plan/Ask, foreign provenance, authentication and formal
approval controls pass.
- All 59 existing confirmation resolver controls pass in 16.62 seconds.
These cover cancellation, lost ownership, company/user scope, policy
changes, transactional rollback, concurrent retries and stale revisions.
- All 46 completion controls pass in 4.07 seconds. The two added clock
controls preserve the original idle deadline and prove immediate
permanent-proof failure plus transient observation retry. The existing
live collector-to-proof and native mutation negatives remain in this
required server file.
- The first current-catalog integration check failed 5 controls in 2.70
seconds. The old consumer added the already-declared reply again and
could accept an extra ordinary turn. The corrected consumer passes all
46 controls using the actual catalog. The existing full 20-case
catalog/reporting compatibility check also passes in 4.93 seconds. Case
identities, configured models and reporting match the declared 20.
- The API controls used one owned loopback PostgreSQL instance through
existing supported test hooks. It was stopped and removed. Earlier
missing-library/setup failures and one corrected current-source test
expectation remain retained; they are not counted as product failures or
passes.
- The unchanged first-task skill has SHA-256
`b789090996e7925676590c006418eb8b57f4514e646f6bca18ffd7b2b8e864de`. Both
candidate runtime copies matched it. The inherited policy was observed
in both models; one complete acceptance journey passed. This does not
qualify the new rejection repair.
- The three retained real completion streams pass the stronger local
proof, including native items, workspace observations and recorded DOM
links. Their original failed grades stay unchanged.
- Earlier related checks passed 153 hiring/non-execution controls and 13
asset checks. Their source is unchanged. Required discovery includes the
completion, API and asset files exactly once.
- Prior candidate `74da56c4` passed all required CI, including canonical
typecheck and build, in [CI run
41929148958](https://github.com/paperclipai/paperclip/actions/runs/41929148958).
Its fresh Greptile was 5/5. This is prior-head evidence.
- The intermediate `053150d9` head also received Greptile 5/5. Its [CI
run](https://github.com/paperclipai/paperclip/actions/runs/41940014165)
and review stay retained as superseded evidence. They do not qualify the
final catalog correction.
- A clean full local typecheck/build is not claimed because the reused
dependency cache has stale adapter-utils/Runner declarations. No source
or lock workaround was made. Fresh canonical CI must pass before merge.
- The original full-plan result remains 13/20. The separate two-case
probe remains 1/2. A fresh complete saved-plan run against delivered
source is required for acceptance. No case, assertion, model, budget,
timeout or failed grade is changed.

Focused commands:

```sh
# From server/; database URLs point only to an owned disposable local instance.
node ../node_modules/vitest/vitest.mjs run src/__tests__/chat-completion-proof.test.ts
PAPERCLIP_TEST_DATABASE_URL="$OWNED_TEST_DATABASE_URL" node ../node_modules/vitest/vitest.mjs run src/services/native-runtime/confirmation-comment-api.integration.test.ts
PAPERCLIP_TEST_POSTGRES_ADMIN_URL="$OWNED_TEST_DATABASE_URL" node ../node_modules/vitest/vitest.mjs run src/__tests__/confirmation-comment-resolution.test.ts
```

## Risks

- The ownership exception applies only to an agent acting as the current
execution run against a different non-null source run. Human, unowned
and current-source requeue behavior must stay intact. Native
completion-review handling remains separate.
- The completion proof fails closed on new native tools or event shapes.
A new shape needs review.
- A stored unchanged lease receipt is supporting local evidence. The
final real journey must prove full revision, execution and delivery.
- The new head still needs normal protected merge approval. The optional
queued Paperclip review is not a passing review. No causal repair of
unrelated CI intermittency is claimed.

## Model Used

OpenAI `gpt-6.1-sol` in Codex, with `max` reasoning effort. The runtime
reports a 475,000-token context window. Used reasoning, code editing,
shell tools and provider-free controls.

## 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 22:04:12 -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
DottaandPaperclip c8013d06ad fix(workspaces): preserve task-only warnings during offline migration (#19144)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Ephemeral web servers must save existing workspaces before volume
removal.
> - Offline migration checks file ownership and recovery authority.
> - The same gate also rejects task assignment and configuration
warnings.
> - Those warnings do not require the old filesystem.
> - This pull request preserves known task-only warnings while it saves
files.
> - File recovery and execution obligations keep their existing
blockers.

## Linked Issues or Issue Description

Refs #18943, #19038, #19138.

**What happened?**

Offline workspace migration refuses tasks with assignment,
configuration, disposition or deliberate-wait warnings. It returns
`workspace_recovery_unresolved` even when the warning has no file or
execution recovery authority.

**Expected behavior**

Save local workspace bytes while retaining the task warning unchanged.
Continue to block unknown recovery kinds, file sources, export failures
and execution recovery work.

**Steps to reproduce**

1. Retain a local workspace on a task with an active assignment or
configuration warning.
2. Stop all app writers through the offline maintenance entry.
3. Attempt workspace migration.

## What Changed

- Admit four known task-only warning kinds during offline file
migration.
- Keep file recovery markers, system recovery, native failure markers,
export-failure causes and unknown kinds blocked unless their exact
legacy source is already preserved.
- Keep the existing live-migration recovery exclusion.
- Recheck warning evidence before publication. Do not resolve warnings
or change their owners.
- Add restoration, rejection and publication-race tests. Update the
migration plan.

## Verification

- 112 focused migration and concurrency tests pass with real PostgreSQL
and filesystem archives.
- The four task-only restoration cases fail against the previous
importer with `workspace_recovery_unresolved`.
- Tests delete the old home, restore its saved bytes and compare each
warning row unchanged.
- File and execution markers, unknown kinds and export failures remain
blocked.
- A task warning that gains file recovery during upload prevents
publication.
- Local server typecheck has no changed-file diagnostics. Stale linked
dependency declarations prevent a clean local whole-server result. Clean
full CI remains required before merge.
- The PR was rebased onto the merged Git preservation fix after
resolving a plan-document conflict. The rebased head passed full PR CI
(41943153422). Current head ba18efc7b5 adds the reviewed deliberate-wait
case and reuses the canonical export-cause set; fresh full CI is
required. The earlier unmergeable head did not have a PR workflow run.
- Serving enrollment and full copied-data acceptance remain separate
gates.

## Risks

- The allowlist must remain narrow. New warning kinds are blocked by
default.
- A known task-only warning with file or execution recovery evidence
still blocks migration.
- This change depends on the existing offline provider fence. It does
not establish that old app writers stopped.

## Model Used

OpenAI GPT-6.1 Codex, exact model ID `gpt-6.1-sol`. Used for reasoning,
code editing, shell tools and test execution. The runtime does not
expose its context-window size.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 22:00:41 -06:00
DottaandClaude Opus 5.5 eab672e10a fix(workspaces): name the failing remote copy-back step without private output (#19152)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A native run in a Daytona sandbox saves its workspace after the
provider turn. The save is called copy-back.
> - Copy-back fails on staging after successful provider turns. Issues
become blocked with `retry_exhausted`.
> - The recorded cause is only `remote_workspace_operation_failed` or
`workspace_sync_out_failed`. Operators cannot see which step failed or
why.
> - The remote command wrapper drops every unrecognized sandbox failure
to the bare code. The finalizer retry keeps only the disk-full cause.
> - This pull request records a bounded, redacted cause: the operation,
its exit code or signal, and the failing capture stage with structured
error fields.
> - The benefit is that the next copy-back failure shows its first
cause, and sandbox output still cannot leak paths or credentials.

## Linked Issues or Issue Description

**What happened?**
Native runs on a Daytona sandbox finish their provider turn and accept
their result. Then workspace copy-back fails. The first
`workspace_finalize` operation records only
`remote_workspace_operation_failed`. Each retry records only
`workspace_sync_out_failed`. The run has no error text. After the
retries stop, the issue is blocked.

**Steps to reproduce**
1. Run a native task in a Daytona sandbox.
2. Make the sandbox capture step fail with an error that is not
disk-full or a known archive validation error. For example, make Git
exit with 128.
3. Read the run's workspace operations.

**Expected behavior**
The failed operation names the step that failed and a redacted cause.

**Actual behavior**
The failed operation shows only `remote_workspace_operation_failed` and
then `workspace_sync_out_failed`.

Refs #19050, #19059, #19061, #19063. These are related Daytona and
finalizer repairs. This PR does not duplicate them.

## What Changed

- `remote-workspace-snapshot-archive.ts`: add
`remoteWorkspaceFailureDetail`. It is self-contained and runs in the
sandbox. It reports only fixed keys (`code`, `exit`, `signal`,
`syscall`, `killed`, `program=git`, `name`, `reason`). Each value is at
most 64 `[A-Za-z0-9_]` characters and comes from structured error
properties. It never reports message text, command lines, or paths. The
archive child now writes `remote_workspace_archive_failed: <detail>` for
an unclassified capture error.
- `remote-workspace-transport.ts`: the helper-install and capture
scripts write `remote_workspace_capture_failed:
stage=helper|archive|publish <detail>`. The server accepts this text
only when it matches a strict grammar. Unrecognized failures now throw
`RemoteWorkspaceOperationError` with the message
`remote_workspace_operation_failed: operation=<op> exit=<n>[
signal=<SIG>][; <validated cause>]`. Existing exact diagnostics (Git
operation in progress, full disk, archive validation) do not change.
- `native-workspace-finalizer.ts`: a retried remote save records the
redacted cause in the operation stderr and in
`metadata.workspaceSync.cause`. Other error text still stays off the
record.
- Tests: new red/green cases. The old privacy tests now assert the
anchored redacted form, which still excludes any synthetic private text.

## Verification

- Red, before the fix (only the error class existed). 8 tests failed on
behavior:
- The real Git failure produced `Received:
"remote_workspace_operation_failed"`.
  - The signal-terminated command produced the same bare message.
  - The forged-cause cases produced the same bare message.
  - The finalizer retry case lost the cause.
- Green, after the fix:
`npx vitest run src/services/remote-workspace-transport.test.ts
src/services/native-runtime/native-workspace-finalizer-failure.test.ts
src/services/remote-workspace-snapshot-archive.test.ts` reported 126
passed.
- The new capture test runs the real helper scripts with a controlled
`git` that exits 128 and prints a synthetic private path. The error is
exactly `remote_workspace_operation_failed: operation=capture exit=1;
remote_workspace_capture_failed: stage=archive exit=128 program=git
helper_exit=1`. The private path is absent. The workspace files are
unchanged. The test passes with split output and with one-shot output.
- Forged causes are dropped. These cases are tested: a path in a value,
an extra line, an unknown key, and a prefixed text.
- Wider sweep: I ran all 63 server test files that touch remote
workspaces, copy-back, or finalization. 6381 tests passed. 2 tests
failed in `chat-channels.integration.test.ts` and
`git-workspace-expiration.test.ts`. These 2 tests also fail with this
PR's three source files reverted to `master`. This PR does not cause
them.
- Typecheck: `tsc` reports no errors in the changed files. Other errors
are local, from unbuilt workspace packages.
- Not done: I did not run a live Daytona copy-back. This change adds
diagnostics. It does not repair the underlying failure. The staging
failures can be diagnosed after this PR is deployed.

## Risks

- The error message for an unrecognized remote failure changes from the
exact bare code to the same code plus validated fields. No production
code compares the exact text. Only tests did, and I updated them.
- Privacy: the sandbox detail contains only structured error properties.
The server re-validates them with an anchored grammar with a length
limit and a field limit. A path, a message text, or an unknown key fails
the grammar, and the server drops the whole cause.
- Retry and classification policy do not change.
`classifyNativeWorkspaceFailure` does not read the new text.
- This PR does not address two items: the final snapshot operation stays
`running` after retries are exhausted, and no disk-retention cause was
proven for the ACPX disk-full report.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code with tool use (shell, browser read-only GETs on staging, 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 21:47:29 -06:00
DottaandPaperclip 5b99bdae47 fix(native): verify requested publication at finalization (#19130)
## Thinking Path

> - Paperclip manages work done by AI agents.
> - Native execution reports work completion through a saved result.
> - Some native providers can build that result from their final
message.
> - This path can bypass the finish tool's publication check.
> - A task can then be marked done without the document the user
requested.
> - This change checks publication again at finalization and status
commit.
> - Missing output uses the existing bounded repair path with specific
feedback.

## Linked Issues or Issue Description

**What happened?**

A native provider returned a final message about a local Markdown
briefing. It had not published a task document or a verified attachment.
Its synthetic result claimed success. The server accepted the claim and
marked the task done. A real PostgreSQL regression reproduced this on
public master.

**Expected behavior**

A requested task document must have valid saved publication evidence
before the task becomes done. The check must apply even when the
provider does not call the finish tool. A plain answer still needs no
document. An explicit document-free fallback requires the actual human
rejection for the current unchanged request.

**Steps to reproduce**

1. Request a Markdown briefing document on a task.
2. Complete a native turn with a synthetic
`provider:opencode:agent-message` result and no published output.
3. Admit the result through the normal control-plane port and finalize
it.
4. The old code marks the task done. The repaired code keeps it open and
queues one repair with publication instructions.

**Paperclip version or commit**

Reproduced from public master
`7d1e9b3477f12780e987c32fb8191d476c210fda`.

Related: #18661 added the finish-tool publication guard. This change
applies that guard to finalization. #18180 covers a different fallback
problem: no final text. This change preserves native provider fallback
generation and its saved claims.

## What Changed

- Run the existing deliverable guard before accepting a native done
decision. Keep the original provider result unchanged.
- Route missing publication through the existing single
incomplete-result continuation. Include the publication feedback in the
wake. A repeated incomplete result uses the existing named recovery
action.
- Inspect publication through a typed safe projection of the accepted
ledger. Keep whole-protocol validation at result admission. An
authoritative done decision must pass publication checks even when its
accepted ledger is partial.
- Recheck publication under the status-commit transaction. Hold only
matching proof rows with non-waiting share locks. Changed or locked
proof uses the existing bounded status replanning path.
- Permit an explicit document-free decline fallback only for the saved
human rejection selected by the current continuation. Require the same
original request, current saved description, source revision and source
contract. Model prose or an unrelated old decision is insufficient.
- Add real database controls for publication, plain answers,
authenticated decline, stale or wrong decisions, bounded recovery, and
deletion between assessment and commit.

## Verification

- The first regression failed before the repair: the synthetic result
marked an unpublished task done. The retained real PostgreSQL run took
21.846 seconds, including setup and cleanup.
- Two description-edit regressions also failed before their repair: a
description change after admission or at the commit boundary still let
the old decline waive the newly required document. Both now keep the
task open. The red run took 25.738 seconds.
- The commit-race regression also failed before its repair: removing
publication after assessment still allowed done. That run took 17.448
seconds.
- All thirteen focused real database controls pass after the repair.
They exercise the normal control-plane port, finalizer, semantic
publication tools, saved human rejection and status transaction. Owned
database processes, data and output storage are removed after each run.
- All 469 tests across the seven relevant and affected suites pass on
head `f8870469f9879a53551f0faaff0fe5b1234ea303`, based on master
`c7390bef20`. The six-suite run passed 426 tests in 97.319 seconds
including owned setup and cleanup. The unchanged reassessment-worker
suite passed 43 tests in 48.901 seconds. All required current-head
checks pass in [CI run
41937653209](https://github.com/paperclipai/paperclip/actions/runs/41937653209),
including aggregate verify, typechecking, build, general and serialized
tests, native Runner tests and all eight browser shards. Fresh Greptile
review is 5/5 on this exact head, with no open findings.
- Local scoped server typechecking reported no errors in the four
changed files and 11 errors in reused dependency declarations and
exports. That setup failure remains retained. Fresh current-head CI
passes the full Typecheck + Release Registry gate and Build. Local
typecheck is not claimed as passing.
- CI on the previous head found one shared status-projection regression
across Studio, routine, status-authority and worker controls. Those
phases use opaque accepted ledgers. The correction inspects only
publication references, preserves the accepted marker and bindings, and
forces the publication check for an authoritative done decision. A new
real commit control verifies that an empty accepted ledger cannot close
a requested-document task. All original affected assertions remain
unchanged. The retained failed run is
[41934314437](https://github.com/paperclipai/paperclip/actions/runs/41934314437).
- No provider call, paid campaign, staging deployment or eval regrade is
part of this PR. No model, timeout, assertion, workflow, dependency or
lockfile change.

Focused suites:

```sh
./node_modules/.bin/vitest run --config "$PWD/server/vitest.config.ts" --root server \
  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
```

## Risks

A native result that previously closed a task without its requested
output now leaves it open. The existing repair path can run once and
then records a named recovery action. Database contention does not grant
completion. The existing first-task decision gate still runs first. The
conditional decline check is deliberately conservative; unmatched
wording or missing authoritative source data does not waive publication.
This change closes a false completion path. It does not establish why a
provider omitted publication, prove a live provider repair, or qualify a
full evaluation plan.

## Model Used

OpenAI Codex, based on GPT-6, assisted with diagnosis, code and
controls. The exact serving model identifier and context-window size are
not exposed in this session. Capabilities used: repository inspection,
code editing, local code execution and GitHub tools.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have described the issue in-PR following the relevant 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 issue
identity
- [x] I have run relevant tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have documented the behavior and limits in this PR; no operator
command or public API changed
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open findings
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 21:46:29 -06:00
Devin FoleyandClaude Opus 5.5 b9fbbf8d30 fix(native): continue runs woken by withdrawn or cancelled interactions (#19163)
Withdrawing a card or cancelling a question wakes the assignee with that
interaction id. The native interaction bridge required every wake
interaction to be accepted, rejected or answered, so preparation failed
with native_interaction_unresolved before the provider started.

Project a cancelled interaction as a closed response with its stored
result. Consumers grant authority only for accepted, answered or pending
items, so the closed response carries context and no approval. Pending
requests and governed tool reviews keep their existing checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 19:10:59 -08: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
Devin FoleyandClaude Opus 5.5 7f9ef38273 fix(runner): report the upstream cause of OpenCode provider failures (#19166)
OpenCode reports some OpenRouter failures as an UnknownError whose message is the provider's JSON error body. The provider failure now reads its HTTP status, error type and message from that body, so the run shows the upstream message instead of raw JSON.

The OpenCode nativeTurnFailure diagnostic now carries the provider failure's category, retryability and HTTP status (closed values only; no provider text or code), and recognizes the driver's opencode_empty_response code. Run status, error codes and retries do not change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 18:57:26 -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
Devin FoleyandClaude Opus 5.5 7edbc1fc07 fix(server): keep a successor's issue lock when a finalizing native run releases its checkout (#19156)
`release_checkout` now clears `checkoutRunId` and the execution lock only when they belong to the committing run, so a successor that claimed the issue while its predecessor was finalizing keeps its claim and passes reviewed-chat attestation.

`attestReviewedExternalChatRun` reports the first failed check as a closed label. Setup failures store it in `resultJson.reviewedChatBinding.denialReason`, and Sentry run-failure diagnostics export it as `execution.reviewedChatBindingDenialReason` (no IDs or text). The error message is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 17:49:03 -08:00
DottaandClaude Opus 5.5 482ec0aea9 fix(native): run saved copy-back retries on schedule behind a slow export (#19145)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs copy their sandbox workspace back after the provider
finishes. A failed copy-back saves a retry 30 seconds later, with a
bound of 3 attempts.
> - One shared delivery queue runs these retries. A pass of that queue
ends only when its slowest check ends.
> - A legitimate sandbox export can take 10+ minutes. While it runs, all
other due retries wait.
> - On staging, a run active across a deploy showed "Reconnecting" with
an overdue retry for about 24 minutes. Its retries ran at +24 min and
+3.5 min instead of +30 s. It settled only after about 32 minutes.
> - This pull request lets idle check slots admit entries that come due
during a pass. It does not overlap a running check's resource and does
not start work after abort or drain.
> - The benefit is that the existing 3-attempt bound settles a failing
copy-back in about 1 minute, not after the slowest unrelated export.

## Linked Issues or Issue Description

**What happened?**
A native run whose result is durable but whose workspace copy-back
failed stays in `running` with the label "Reconnecting" for a long time.
Its saved retry time is in the past. The retry does not run until an
unrelated slow copy-back in the same queue pass finishes.

**Steps to reproduce**
1. Start two native runs on different tasks with accepted results.
2. Let the copy-back of run A take many minutes.
3. Let the copy-back of run B fail. It saves a retry 30 seconds later.
4. Watch run B. Its retry does not run until run A's copy-back ends.

**Expected behavior**
Run B retries on its saved schedule. After 3 failed attempts it settles
with `native_workspace_sync_out_retry_exhausted` and a recovery action,
while run A continues.

**Paperclip version or commit**
master at a0c0851766. Observed on a staging deployment of 69dea878.

**Agent adapter(s) involved**
Not adapter-specific (core bug). Observed with the Paperclip runner.

**Relevant logs or output**
In-run copy-back failed at 23:44:57 (retry saved for 23:45:28). The
deploy restarted the server at 23:49:45. One queue check exported
another task's workspace from 23:53:54 to 00:06:15. The overdue retry
ran at 00:09:06, then 00:12:39, and the run settled at 00:12:40 with
`native_workspace_sync_out_retry_exhausted`. About 20 other runs
followed the same timeline. Their failures at 00:12–00:13 are this same
retry exhaustion. They do not come from a reattach sweep.

## What Changed

- `registerScopedDeadlineWork` has a new opt-in
`concurrentChecks.admitDueWhileActive` option. While a pass still has a
running check, idle slots admit entries that are due (saved deadline,
failure backoff, and minimum spacing). They wait for the next deadline
or the next check completion.
- Admission skips entries owned by the pass and entries whose resource
keys a running check holds. It stops on abort or when `canRun()` is
false. The pass still ends only when all running checks end.
- The native workspace finalization queue opts in. Its `check` already
rereads the durable coordinator row before it acts.
- Regression tests: a real-database test in
`native-workspace-finalization-work.test.ts`, plus two helper tests
(resource-key safety and shutdown).

## Verification

- `cd server && npx vitest run
src/__tests__/native-workspace-finalization-work.test.ts
src/__tests__/scoped-deadline-work.test.ts
src/__tests__/delivery-work-coordinator.test.ts` passes (68 tests).
- Red before the fix: "keeps another task's saved retries on schedule
and settles it while a slow copy-back is running" failed because
`onWorkspaceSettled` was never called for the failing run. "admits saved
deadlines that come due behind a slow check without overlapping its
resource" failed too.
- Green after the fix: the failing run makes exactly 3 attempts, settles
as `terminal_failure` / `native_workspace_sync_out_retry_exhausted`, and
the slow copy-back is admitted once and then succeeds.
- `tsc --noEmit` shows no errors in the changed files.
- Not done: a live staging check. This change is not deployed.

## Risks

- Low to medium. Only the workspace finalization queue opts in. Other
queues keep the current behavior.
- An admitted entry is not reconstructed first. The finalization `check`
rereads durable state, so a stale in-memory entry cannot start a second
export. The advisory lock and owner receipt still prevent two copy-backs
for one run.
- This change does not fix the cause of the copy-back failures on
staging (`native_session_lease_lost` in-run, then
`workspace_sync_out_failed` with no detail). That needs a separate
change.

## 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 19:26:02 -06: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 3a72efebe5 fix(native): confirm no runner process when remote staging fails before launch (#19059)
## Thinking Path

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

## Linked Issues or Issue Description

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

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

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

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

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 18:26:02 -06:00
DottaandClaude Opus 5.5 d8dbe55368 fix(workspaces): let a missing task repository ref be corrected (#19072)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents can ask Paperclip to prepare a task repository at a specific
ref
> - Paperclip records that request and clones the repository at the next
admission
> - Paperclip did not check that the requested ref exists
> - When the ref was absent, every admission retried the same checkout
and failed setup
> - A corrected request was refused as a conflicting receipt, so the
task stayed blocked with no repair path
> - This pull request identifies a missing ref and lets a corrected
request replace it while nothing was published
> - The benefit is that a bad ref becomes a repairable error instead of
a permanent setup failure

## Linked Issues or Issue Description

**What happened?**
An agent asked for a catalog repository at ref `main`. The repository
default branch is `master` and has no `main` branch. Each later
admission failed with `setup_failed`: `git checkout main` reported
`pathspec 'main' did not match`. The task failed admission for more than
a week and became blocked. A new request with ref `master` returned 409
because the receipt with ref `main` already existed.

**Expected behavior**
Paperclip should report that the requested ref does not exist. A new
request with a valid ref should replace the bad ref when no checkout
exists.

**Steps to reproduce**
1. Use a repository whose only branch is `master`.
2. Request it as a task repository with `ref: "main"`.
3. Admit the task. Preparation fails, and the receipt goes to
`needs_repair`.
4. Request the same repository with `ref: "master"`. Before this change,
the request returns 409.

## What Changed

- `managed-repository-checkout.ts`: When `git checkout <ref>` fails with
Git's unknown-ref message, and each quiet `rev-parse --verify` probe
(`<ref>^{commit}`, `refs/remotes/origin/<ref>^{commit}`) exits 1, throw
`RepositoryRefNotFoundError`. The message tells the caller to request
the repository again with an existing branch, tag, or commit. Other
checkout failures keep their original cause.
- `execution-workspace-repositories.ts`: Record `failureCode:
"repository_ref_not_found"` for that error. Other failures keep
`repository_preparation_failed`.
- `execution-workspace-repositories.ts`: A request for the same
repository with a different ref replaces the ref only when all of these
are true: the receipt is `needs_repair` with `repository_ref_not_found`,
it has no pinned commit, the workspace is local (no remote authority),
and the checkout path is absent. The update is conditional on that state
and writes a `workspace.repository.ref_corrected` activity entry. All
other ref changes keep the existing 409 conflict.
- `execution-workspace-repositories.ts`: Local admission re-reads each
receipt inside the checkout lock and claims it with a conditional update
on its current ref. A correction that commits after the receipts were
listed is used, not discarded.
- Tests: one regression for the full missing-ref path, and one safety
test that keeps the receipt immutable when files exist at the checkout
path or when the failure was not a missing ref.

## Verification

- Red before the fix: `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/task-workspace-repositories-persistence.test.ts` failed
with `expected ... /ref "main" does not exist/ but got 'Failed to
prepare managed checkout fo…'`.
- Green after the fix: the same file passes (11 tests). The new test
admits with ref `main` (fails, `repository_ref_not_found`, no checkout),
corrects to `master`, admits again (ready, pinned to the source commit),
and then confirms that a later ref change after publication still
returns a conflict.
- The regression also holds the checkout lock while admission waits,
corrects the ref, and then releases the lock. Without the in-lock
re-read it fails with 409; with it, admission publishes the corrected
ref.
- `heartbeat-managed-clone-credentials.test.ts` passes: an
authentication failure during checkout keeps its cause and is not
reported as a missing ref.
- Other checkout suites pass (9 files, 317 tests): reopen, base storage,
expiration, project env, rehome, nested clone migration, object cache,
trust, source copy privacy.
- Neighbor suites pass: `managed-repository-checkout.test.ts`,
`task-workspace-repositories.test.ts`,
`task-workspace-repositories-trust.test.ts`,
`heartbeat-project-repositories.test.ts` (47 tests). In one parallel run
under heavy machine load, `heartbeat-project-repositories.test.ts` had
timing failures in different tests on each run. The same file passed
alone with this change (47/47) and on master.
- `tsc --noEmit` for `server` passes.

## Risks

- Low risk. The new correction path is narrow and fails closed. Remote
(sandbox) workspaces keep the existing conflict because the control
plane cannot inspect the remote checkout.
- Existing receipts that failed before this change have
`repository_preparation_failed`. They need one more failed admission to
get `repository_ref_not_found` before a correction is accepted.
- No schema change. `failure_code` is free text.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, agentic coding with
tool use (shell, file edit, browser read-only diagnostics).

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 18:25:41 -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 9cdbcbeddc fix(workspaces): restore archived commits from verified project objects (#19138)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - An ephemeral web server must preserve historical task files outside
its local disk.
> - Archived isolated Git workspaces can have a successful
branch-deletion receipt.
> - Their private checkout can be gone while the project cache still
owns the exact commit.
> - The importer rejects that cache because it applies private-checkout
validation.
> - This pull request validates the managed object source for
receipt-based restoration.
> - The importer restores the recorded commit without using the current
shared working tree.

## Linked Issues or Issue Description

Refs #18826, #18943, #19038.

**What happened?**

An archived isolated workspace has a valid successful cleanup receipt
and a retained commit. Offline migration rejects it with
`archived_git_source_unavailable,local_source_unsafe_or_missing`. The
private checkout was removed. The company and project cache still
contains the exact commit.

**Expected behavior**

Restore the immutable commit from the verified project object cache.
Keep strict validation for saved private edit recipes. Reject missing
receipts, a foreign cache, a different origin and revoked cleanup
authority.

**Steps to reproduce**

1. Create an isolated Git workspace from a managed project cache.
2. Archive it and retain its successful branch-deletion receipt.
3. Remove its private checkout while the verified project cache retains
that commit.
4. Attempt offline workspace migration.

## What Changed

- Extract the existing managed Git object-source validator.
- Use that validator only for exact successful deletion receipts that
refer to a canonical company and project cache.
- Keep private restore-point source validation unchanged.
- Fetch immutable objects at the recorded commit into private scratch.
Recheck receipt authority before publication.
- Add real Git and database tests for restoration and rejected authority
changes. Document the preservation path.

## Verification

- 129 focused migration, concurrency and isolated-source tests pass
across three files.
- The new restoration case fails against the previous importer with the
reported source-unavailable error.
- Tests remove the old home and restore the retained commit. A newer
shared working tree and unrelated files are excluded.
- Tests reject a foreign company cache, wrong origin, missing receipt
and receipt revocation during upload.
- Local server typecheck has no diagnostics in changed files. Stale
linked dependency declarations prevent a clean local whole-server
result. Current-head full CI passes (54 passing checks, three skipped).
Greptile is 5/5 with no unresolved review threads.
- A replay of the 32 affected records is running in an isolated staging
copy. Two committed owner overlays are under test. This is not
released-image or serving-instance acceptance.

## Risks

- A deletion receipt must identify an exact commit that still exists.
Missing objects continue to block migration.
- Shared working-tree contents must never substitute for archived
private files. Only the recorded commit is fetched through this path.
- The change does not recreate private edit authority, retire recovery
records or alter serving configuration.

## Model Used

OpenAI GPT-6.1 Codex, exact model ID `gpt-6.1-sol`. Used for reasoning,
code editing, shell tools and test execution. The runtime does not
expose its context-window size.

## Checklist

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

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native runs in a remote sandbox go through workspace preparation
before the Runner process starts.
> - When the server restarts, restart recovery decides if a remote run
can safely restart its bootstrap.
> - A remote run that never started a Runner process, never had a lease
owner, and has no provider evidence can restart its bootstrap. Recovery
calls this an "unclaimed bootstrap".
> - A graceful shutdown (SIGTERM) suspends every running native
coordinator. It sets `leaseExpiresAt = now`, also when the coordinator
has no lease owner.
> - The unclaimed-bootstrap check requires `leaseExpiresAt` to be null.
So a deploy during workspace preparation turns a safe bootstrap restart
into `native_restart_recovery_blocked` /
`ambiguous_remote_recovery_evidence`. The task is blocked and a board
recovery action is created.
> - This pull request lets the check accept an elapsed expiry when the
coordinator has no lease owner.
> - The benefit is that a deploy during remote workspace preparation no
longer blocks the task. Every other contradictory-evidence guard stays
in force.

## Linked Issues or Issue Description

**What happened?**

On a staging instance, three remote-sandbox runs failed with
`native_restart_recovery_blocked` and
`ambiguous_remote_recovery_evidence` after one deploy. Each run was in
native workspace preparation (`snapshot.git`, `pack`, `seed.verify`,
`stage.workspace`). No run had a process start, a process identity, a
provider event, or a checkpoint. The graceful shutdown event was the
only coordinator change. The next server then blocked each run in
restart recovery.

**Expected behavior**

A remote run that was suspended during preparation, before any Runner
launch or lease claim, restarts its bootstrap on the same run. Recovery
does not block the task.

**Steps to reproduce**

1. Prepare a remote native run (prepared native workspace sync, fenced
preparation, no process, no checkpoint).
2. Call `drainRunningRunsForShutdown("SIGTERM")` for the run.
3. Call `claimNativeRestartRecoveries` as a successor controller with
`restartKind: "hard"`.
4. Observe `{ kind: "blocked", reason:
"ambiguous_remote_recovery_evidence" }`. Without step 2, the same run
gets `bootstrap_incomplete`.

**Paperclip version or commit**

Reproduced on `master` at `be20d4ad97`.

**Deployment mode**

Remote sandbox environments with PostgreSQL.

Related: #18877 (remote checkpoint retry reclaim) and #18854
(pre-provider startup retry). Neither changes this predicate.

## What Changed

- `claimNativeRestartRecoveries`: the unclaimed remote bootstrap check
accepts `leaseExpiresAt` when the coordinator has no lease owner and the
expiry has elapsed. A null owner with an elapsed expiry grants no
authority.
- New regression: a remote run suspended by the real graceful-shutdown
drain during preparation is claimed as `bootstrap_incomplete`, with no
recovery action.
- New safety case: an ownerless coordinator with an unexpired lease
expiry still blocks with `ambiguous_remote_recovery_evidence`.

## Verification

- Red before the fix: `npx vitest run
src/services/native-runtime/native-runner-restart-recovery.integration.test.ts
-t "graceful shutdown suspended during preparation"` returned `blocked`
/ `ambiguous_remote_recovery_evidence`.
- Green after the fix: the same test returns `bootstrap_incomplete` with
`providerAttempt: 0` and `controllerGeneration: 1`.
- The existing contradictory-evidence cases (`claimed`, `generation`,
`process`, `launch`, `provider-event`, `checkpoint`,
`invalid-workspace`, `unfenced`) and the new `unexpired-lease` case
still block.
- Full `native-runner-restart-recovery.integration.test.ts` suite and
server `tsc --noEmit` pass locally.
- Not run: a live deploy on a remote sandbox. The fix is not deployed.
The original blocked tasks are not recovered by this change.

## Risks

- Low risk. The change relaxes one field of one predicate. All other
unclaimed-bootstrap conditions stay: prepared workspace, fenced
preparation, attempt 0, generation 0, no controller identity, no lease
owner, no process identity, no session, no checkpoint, no provider
evidence, and no launch or tool event.
- The executor still requires the durable PRP root to prove an
incomplete bootstrap before it starts a fresh bootstrap.

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

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with
tool use (shell, file edits, browser reads of staging). Standard 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
- [ ] 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:24:47 -06:00
DottaandPaperclip 7d46532c6d fix(runtime): bound parallel offline harness backup transfers (#19108)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - An ephemeral web server must retain provider history outside its
local disk.
> - Offline enrollment moves verified local harness backups to object
storage.
> - A cold rehearsal reached 699 current backup rows while processing
1,953 sources before its 25-minute deadline.
> - The serial transfer loop prevents this existing dataset from
enrolling.
> - This pull request overlaps at most four transfers within the
existing total scratch limit.
> - The benefit is faster preservation with the same ownership and
publication checks.

## Linked Issues or Issue Description

Refs #19038, #18943, #18932.

**What happened?**

Offline enrollment processed 1,953 local provider backups one at a time.
The normal Cloud image reached 699 current backup rows (including nine
pre-existing rows) before maintenance expired. The serving instance
remained unchanged during the isolated test.

**Expected behavior**

Enrollment must preserve every source within bounded time and disk use.
It must stop all in-flight transfers before it reports failure.

**Steps to reproduce**

1. Prepare a settled instance with multiple verified native harness
backups.
2. Run offline enrollment against shared object storage.
3. Observe that the serial owner waits for each archive, upload,
readback and publication before starting the next source.

## What Changed

- Process groups of up to four verified offline harness backups.
- Reserve space for the archive and encrypted copy before each group
starts. Keep the total scratch limit equal to one maximum-size serial
transfer.
- Derive a smaller archive limit from a closed manifest directory set.
Preserve unlisted files with an exclusive maximum-size reservation.
- Enforce the reservation while streaming. Keep the existing maximum for
ordinary runtime backups.
- Cancel and await peer transfers after failure. Keep completed
publications valid for retry.
- Preserve the original archive-stream failure when the tar writer also
fails after destruction.
- Document the migration bound and add integration regressions for
overlap, restoration, cancellation, retry, extra files and archive
limits.

## Verification

- Serial baseline: the new overlap regression fails with one active
transfer when four are required.
- Candidate: 59 targeted tests pass with real PostgreSQL, S3, filesystem
archives and restored provider bytes. The bundled-entry test is skipped
locally because this source worktree has no runtime bundle. The full
normal image rehearsal remains required.
- Follow-up: 48 archive/storage checks pass after correcting the
PAX-header allowance. The real encoder produces a 972,801,024-byte
full-entry projection, above the old 805,306,368-byte allowance and
below the new 1,024,001,024-byte bound. The existing global object limit
remains in force. The old formula fails the new regression. Exact-size
restoration and one-byte-over-limit rejection pass.
- The previous revision passed full CI after one setup-route timeout
retry. That failure remains recorded; the corrected current revision
f7c2be1289 passed fresh full CI (41937700826) and Greptile 5/5. Its
review thread is resolved.
- Local server typecheck found four test-only Promise library errors;
those are fixed. The remaining 26 diagnostics come from stale linked
dependency declarations outside the changed files. Clean full CI is
required before merge.
- The resumed isolated staging-copy experiment also exceeded its
25-minute deadline. It reached 1,389 current backups. This improves
transfer throughput but does not by itself qualify enrollment; online
preparation is tracked in #19137.
- Full fresh normal-image migration, empty-home restoration and hosted
release handoffs remain deployment acceptance gates.

## Risks

- Parallel work increases concurrent object-storage and database
requests to at most four transfers. The scratch reservation still bounds
archive and ciphertext files together.
- A malformed or changed source can fail its streaming limit. The
migration retains its source and stops before export.
- Partial completed publications survive failure. Retry verifies them
under the existing hash and ownership rules.
- No serving configuration, deployment pin, database contract or
provider lifecycle policy changes in this pull request.

## Model Used

OpenAI GPT-6.1 Codex, exact model ID `gpt-6.1-sol`. Used for reasoning,
code editing, shell tools and test execution. The runtime does not
expose its context-window size.

## Checklist

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

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A native run finishes in two steps: the provider turn, then
finalization. Finalization saves the workspace and commits the result.
> - When finalization exhausts its retries, `recordRetryableFailure`
sets the run to `failed` and blocks the task.
> - That code writes the cause only to `resultJson.failureCode`. It does
not write the run's `errorCode` or `error`.
> - So the run list, inbox, and agent status show a failed run with no
reason. Operators cannot tell why the run failed.
> - This pull request writes the finalizer's terminal failure code and a
readable message to the run row when the finalizer fails the run.
> - The benefit is that every failed run now names its cause.

## Linked Issues or Issue Description

**What happened?**

A hosted instance had 11 assigned native runs with status `failed`,
`errorCode` null, and `error` null in one day. In each run, the provider
turn succeeded. The remote workspace copy-back then failed three times.
The finalizer set the run to `failed` and blocked the task. The run kept
no error code. `resultJson.failureCode` was
`native_workspace_sync_out_retry_exhausted`, but no screen shows that
field as the failure reason.

**Expected behavior**

A failed run has an `errorCode` and an `error`. The failure surfaces
show why the run failed.

**Steps to reproduce**

1. Create a native run with a finalization coordinator.
2. Call `recordNativeFinalizationFailure` three times with
`native_workspace_sync_out_failed` and `failureScope: "workspace"`.
3. Read the run. Its status is `failed`, and its `errorCode` and `error`
are null.

**Paperclip version or commit**

`master` at be20d4ad97.

## What Changed

- `recordRetryableFailure` in
`server/src/services/native-runtime/native-run-finalizer.ts` now sets
`errorCode` to the terminal failure code and `error` to a readable
message when it projects a run to `failed`. This covers
`native_workspace_sync_out_retry_exhausted`,
`native_workspace_sync_out_unrecoverable`, and
`native_finalization_retry_exhausted`.
- The update keeps an earlier root `errorCode` and `error` (`coalesce`).
- The update does not change a run that Stop already cancelled. It uses
the same row-level `case` as the status write.
- The update does not add an error when the finalizer projects the run
as `succeeded` or `cancelled`.
- New tests in `native-run-finalizer-telemetry.test.ts` cover the failed
projection, root-cause retention, concurrent cancellation, a retry that
remains, the provider-scope failure, and the provider-succeeded
projection.

## Verification

- `cd server && npx vitest run
src/services/native-runtime/native-run-finalizer-telemetry.test.ts`
- Before the fix, three tests failed because `errorCode` was null:
"bounds workspace-only retries…", "records the finalization error code
when provider-scope retries fail…", and "blocks immediately when the
sandbox… is gone".
- After the fix, all tests in the file pass.
- Related suites pass: `native-workspace-export-retry`,
`native-run-projection-work`, `explicit-native-continuation`,
`execution-control-work`, `native-status-arbiter-corpus`, and
`workspace-recovery` (971 tests). One `workspace-recovery` test timed
out at 30 s under the parallel load. It passes when run alone.
- Downstream checks: the export retry path
(`native-workspace-export-retry.ts`) and the success projection
(`native-run-projection.ts`) already clear `error` and `errorCode` when
they reopen or succeed a run. A later recovery therefore does not keep a
stale error.

## Risks

- Low risk. The change only writes two columns that were null for these
runs.
- `error` is now also the agent status reason and the lease failure
reason for these runs. Before, the agent reason fell back to
`native_workspace_sync_out_failed`.
- This change does not fix the first cause of the copy-back failures.
The remote capture reports only the generic
`remote_workspace_operation_failed`, because the capture output is
redacted. That cause needs separate work.

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

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), used in Claude Code
with tool use (shell, file edit, read-only 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
- [ ] 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>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 18:07:35 -06:00
DottaandPaperclip 818d2bb661 fix(native): preserve closed post-result failure diagnostics (#19126)
## Thinking Path

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

## Linked Issues or Issue Description

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

**Paperclip version or commit**

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 17:45:53 -06:00
DottaandClaude Opus 5.5 0e27369fa4 fix(native): restart sandbox prep interrupted before its sync descriptor (#19054)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs on sandbox environments sync the host workspace into the
sandbox before the provider starts
> - Runtime selection writes the native execution input first. It writes
the workspace sync reference only after the sandbox is seeded, and
before any provider dispatch
> - A server restart between those two writes leaves a run with an input
but no sync reference
> - Restart recovery then fails the run during workspace preparation
with `native_workspace_sync_descriptor_binding_missing`, although the
sandbox never received any work from that run
> - This pull request lets recovery re-admit the host workspace in that
state, so sync preparation starts fresh on the same run
> - The benefit is that a deploy or restart during sandbox preparation
no longer turns queued work into a terminal `setup_failed` run

## Linked Issues or Issue Description

**What happened?**
After a server restart, six native sandbox runs failed with
`setup_failed` and the error
`native_workspace_sync_descriptor_binding_missing`. Each run timeline
shows the same order: run started, `snapshot.git`, `snapshot.baseline`,
then "Server shutdown suspended native controller ownership", then a
restart recovery claim ("Restarting an incomplete native runner
bootstrap" or "Resuming the durable native provider session"), then the
error. No run had a `nativeWorkspaceSync` reference. Each run had a
persisted `nativeExecutionInput`.

**Expected behavior**
When sandbox sync preparation did not finish, restart recovery prepares
the sandbox workspace again from the host workspace and continues the
run.

**Steps to reproduce**
1. Start a native run on a sandbox environment.
2. Restart the server after runtime selection persists
`nativeExecutionInput` but before `prepareNativeWorkspaceSync` persists
its reference.
3. Restart recovery re-enters workspace preparation and fails with
`native_workspace_sync_descriptor_binding_missing`.

**Paperclip version or commit**
Reproduced on `master` at be20d4ad97.

**Relevant logs or output**
`native_workspace_sync_descriptor_binding_missing` thrown from
`server/src/services/heartbeat/workspace-preparation.ts`.

Related: #17838 (preparation recovery ownership). No open PR covers this
guard.

## What Changed

- `server/src/services/heartbeat/workspace-preparation.ts`: remove the
guard that failed sandbox recovery when the run has no workspace sync
reference. The existing fallback admits `executionWorkspace.cwd`, which
is the same host directory that `prepareNativeWorkspaceSync` then uses
as `workspaceLocalDir`. A comment records the ordering invariant that
makes this safe.
- `server/src/services/heartbeat/workspace-preparation.test.ts`: the
Daytona "absent descriptor" recovery case now expects preparation to
succeed and to re-admit the host workspace. The "corrupt descriptor"
case still fails closed with
`native_workspace_sync_descriptor_digest_mismatch`.

## Verification

- Red before the fix: `npx vitest run
src/services/heartbeat/workspace-preparation.test.ts -t "validates the
admitted host binding"` (from `server/`) failed for "daytona recovery
with absent descriptor" with
`native_workspace_sync_descriptor_binding_missing`, the same error as
the failed runs.
- Green after the fix: the full
`src/services/heartbeat/workspace-preparation.test.ts` file passes,
including the local, ssh, runner-task, valid-descriptor and
corrupt-descriptor recovery cases.
- Ordering check (code read): `runtime-selection.ts` persists
`nativeExecutionInput` in the selection transaction, then calls
`prepareNativeWorkspaceSync`, which persists the reference as its last
step. `heartbeat.ts` calls `executeHeartbeatRuntime` only after runtime
selection returns. No code path removes `nativeWorkspaceSync` from
`runnerProfileJson`.
- Not run: a live restart on a sandbox environment. The original staging
runs are not recovered by this PR.

## Risks

- Low risk. The change only affects native recovery on sandbox targets
(not runner-task, ssh, remote-authority or controller-only runs) when
the sync reference is absent.
- A present but invalid descriptor still fails closed.
- If a future change dispatches a provider before the sync reference is
persisted, this path would re-seed over remote work. The comment at the
call site records that invariant.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), via Claude Code with tool use
(shell, file edits, browser reads of run records).

## 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 17:45:00 -06:00
35d086213e fix(native): explain failed accepted results on the run (#19091)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A native run ends when the server projects the accepted result onto
the run row.
> - Operators read `errorCode` and `error` on a failed run to learn why
it failed.
> - Three native writers can set a run to `failed` without these fields.
#19063 repairs the finalizer retry-exhaustion writer.
> - The two result-projection writers still set `failed` with no cause
when the provider failure is not classified.
> - This pull request makes both projection writers record a cause when
they project `failed`.
> - The benefit is that, with #19063, no native writer can leave a
failed run unexplained.

## Linked Issues or Issue Description

Refs #19063.

**What happened?**

A staging soak found 11 failed native runs with `errorCode` null and
`error` null. In each run the provider turn succeeded. Workspace
copy-back then failed three times, and the finalizer retry-exhaustion
path set the run to `failed`. #19063 fixes that writer. An audit of
every `heartbeat_runs` status writer found two more writers with the
same defect. `finalizeNativeRun` and `projectCommittedRun` project an
accepted result with `runTerminalState: "failed"`. They write the error
fields only when a classified provider failure exists.

**Expected behavior**

A run with status `failed` always has an `errorCode` and an `error`. A
cause that is already recorded stays.

**Steps to reproduce**

1. Accept a native result with `runTerminalState: "failed"` and no
`turn.failed` event.
2. Call `finalizeNativeRun` with `projectRunStatus: true`, or project
the committed run through `projectCommittedRun`.
3. Read the run. On master its status is `failed`, and its `errorCode`
and `error` are null.

## What Changed

- Add `explainFailedRun` in
`server/src/services/native-runtime/native-run-failure-explanation.ts`.
It builds SQL SET expressions. They keep a non-empty existing
`errorCode` or `error`. Otherwise they write `native_run_failed` and a
readable message. By default the expressions leave a run that is already
`cancelled` unchanged, as the status writers do.
- `finalizeNativeRun` uses the helper when it projects `failed`. The
existing provider-failure and recovered-failure values remain the
preferred causes.
- `projectCommittedRun` uses the helper for a `failed` accepted result.
This writer sets status unconditionally, so it does not use the
cancelled branch.
- Writer audit: all legacy and heartbeat callers of `setRunStatus*` that
pass `failed` already set both fields. The other direct native writers
also set both fields.

## Verification

- Red, with the source change stashed: 3 new cases failed with
`errorCode: null`. These were the finalizer projection, recovery
projection with no cause, and recovery projection with an empty error.
The control case with a recorded cause passed.
- Green: `npx vitest run
src/__tests__/native-run-projection-work.test.ts
src/services/native-runtime/native-run-finalizer-telemetry.test.ts
src/__tests__/native-status-arbiter-corpus.test.ts` (225 passed).
- Wider run: all `server/src/__tests__/native-*.test.ts` and
`server/src/services/native-runtime/*.test.ts` (7416 passed). Two
failures come from the local environment. `plugin-runner-probe` needs
the Rust runner source. `native-ingress-bootstrap-retry` needs the
controller-host OpenCode proxy.
- This change was not exercised on staging. It changes only fields that
are written at terminal projection.

## Risks

- Low risk. The change affects only rows that are projected to `failed`.
A recorded cause is never replaced. Succeeded and cancelled projections
are unchanged.
- The staging failures need #19063 too. This PR alone does not change
their writer.
- The cause behind the staging copy-back failures is still unknown. The
sandbox command reports the redacted
`remote_workspace_operation_failed`, which also appears in a separate
signature. This PR does not change that diagnostic.

## Model Used

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

## 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>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 17:40:05 -06:00
1a7eb70a37 fix(run-dispatch): save a redirect sent while a stopped run settles (#19122)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A user can stop a running agent and then send a new message that
redirects the work
> - The stopped run keeps settling for some seconds after Stop (backup
and checkpoint persist)
> - A message sent in that window is queued as a normal run before
settlement records the workspace recovery hold
> - The dispatch gate then withholds the unstarted run, but it saved the
message only for sources with a native workspace-sync baseline
> - For other sources (remote workspace, snapshot-only) it marked the
wake `skipped`, so the message was lost and the task stayed in progress
with no run and no wake
> - This pull request saves the withheld message for any source when the
message was written after the Stop
> - The benefit is that a redirect after Stop is acted on exactly once
instead of disappearing

## Linked Issues or Issue Description

**What happened?**
After Stop, a user posted a redirect comment with resume intent. The
follow-up run was created while the stopped run was still persisting its
checkpoint. When settlement finished, the dispatch gate cancelled the
follow-up run with `execution_reconciliation_required` and an
empty-looking error ("Recovering saved work…"). The user's wake was
marked `skipped`. Nothing woke the task again. It stayed `in_progress`
with no active or queued run.

**Expected behavior**
The message sent after Stop runs exactly once after the stopped run
settles. The task never rests in progress with no run and no pending
wake.

**Steps to reproduce**
1. Start a native run whose stopped source has no native workspace-sync
baseline (for example a remote-workspace sandbox run).
2. Stop the run.
3. Post a comment with resume intent while the stopped run still
persists its checkpoint.
4. Let settlement record the workspace recovery hold.
5. Observe that the queued follow-up run is cancelled with
`execution_reconciliation_required` and its wake is `skipped`.

**Root cause**
`deferUnstartedUserComment` in
`server/src/modules/run-dispatch/adapters/postgres.ts` saved a withheld
user comment only when the stopped source had a native workspace-sync
baseline (the copyback replay case). The enqueue path saves or admits
the same message when it arrives after the hold exists. The gap was only
timing.

Refs #18332 (added the native-baseline-only deferral this PR widens).

**Paperclip version or commit**
Reproduced on `master` at e107a75755. The
touched code is unchanged on current `master`.

**Agent adapter(s) involved**
Not adapter-specific (core bug). Seen with the Paperclip runner adapter.

## What Changed

- `deferUnstartedUserComment` now saves a withheld user comment as a
`deferred_issue_execution` receipt for any stopped native source when
the latest saved comment was written after the source's Stop
(`finishedAt`).
- The native-baseline copyback path is unchanged.
- Input written before the Stop keeps the old behavior (`skipped`).
Older queued input cannot undo a Stop.
- Delivery uses the existing deferred-receipt path
(`resumeRemoteStopComments` / `resumeDeferredExecutionReceipts` →
`admitExplicitNativeContinuation` with `queuedCommentRequestId`). All
continuation gates still apply.
- New regression in `workspace-recovery.test.ts` that controls the
timing: the message is queued while the stopped run is still active,
then the hold is recorded, then the gate runs.
- The existing snapshot-only queued-reset tests asserted the dropped
message. They now assert that the message is saved and delivered once,
like the native-baseline case.

## Verification

- Red before the fix: the new test failed with the wake `status:
"skipped"`.
- Green after the fix:
- `cd server && npx vitest run src/services/workspace-recovery.test.ts`
(308 passed)
- `cd server && npx vitest run src/modules/run-dispatch
src/__tests__/heartbeat-undispatched-execution-wait.test.ts
src/__tests__/issue-queued-comments-routes.test.ts
src/services/explicit-native-continuation.test.ts
src/__tests__/heartbeat-process-recovery.test.ts
src/services/execution-continuation.test.ts`
- Safety cases in the new test:
- The redirect is saved, then delivered as exactly one successor run
with the original user's authority. The old provider run is not
executed.
- Input written before the Stop stays skipped and creates no successor.
- The UI Stop button uses the same `POST /heartbeat-runs/:id/cancel`
path. A comment with `interrupt: true` cancels through the same
`cancelRun` and then queues the comment wake. Both reach the same gate.
These were checked by code reading, not by a separate test.
- Not done: a live staging reproduction at the fixed revision.
Deployment is out of scope.

## Risks

- Behavior change: for sources without a native baseline, a post-Stop
message is now saved and delivered after recovery, instead of dropped.
Admission still blocks it until workspace repair is verified and the
previous execution has stopped. While blocked, the task shows "Your
message is saved."
- If admission never succeeds, the message stays saved and visible.
Before, it was silently lost.
- No schema or API change.

## 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: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 17:39:48 -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
3634c4a845 fix(chat): log the underlying error when a publication becomes delivery_unknown (#18875)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Chat connectors (Slack and others) publish agent messages to
external conversations through the publication worker
> - When the provider accepts a message but the local commit after the
send fails, the publication becomes `delivery_unknown`
> - A `delivery_unknown` row blocks the later publications in the same
conversation until an operator resolves it
> - The log line for this case contains only the generic reason. It
drops the error that the post-send commit threw
> - Operators cannot see why the commit failed, so they cannot tell a
race from a lease loss or another cause
> - This pull request adds the underlying error, redacted and
length-limited, to the existing warning for this case only
> - The benefit is that operators and maintainers can find the real
cause of stalled chat threads

## Linked Issues or Issue Description

Refs #18874

I searched open and closed issues and pull requests for
`delivery_unknown`, Slack publication failures and "could not confirm
its durable result". I found no duplicate. #18874 describes the bug and
the proposed behavior fix. This pull request does not change behavior.
It only makes the cause visible.

## What Changed

- `server/src/services/chat-channels/publications/failures.ts`: when
`providerAccepted` is true, the `chat publication failed` warning gets a
`cause` field. The field contains the underlying error message, redacted
with `redactSensitiveText` and limited to `MAX_ERROR_TEXT`, like the
existing `error` field.
- The stored `redactedError`, the disposition and the operator
resolution flow do not change.
- `server/src/__tests__/chat-run-milestone-work.test.ts`: the existing
`rechecks resolved prompt ownership after delivery ...` test now spies
on `logger.warn`. It asserts that `delivery_unknown` logs the `cause`,
and that `retry` and `failed` do not log it.

## Verification

- Ran in a `node:24-bookworm-slim` container on this branch:
- `npx vitest run src/__tests__/chat-run-milestone-work.test.ts` (in
`server/`): 72 passed.
  - `pnpm run typecheck` (in `server/`): passed.
- Negative check: I removed only the new `cause` field and ran the test
again. The two `delivery_unknown` cases failed. The `retry` and `failed`
cases passed.

## Risks

- Low risk. The change adds one field to one warning log line, only for
publications that the provider already accepted.
- The new field uses the same redaction and length limit as the existing
`error` field, so it does not add a new path for secrets into the log.

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

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), 1M context window, in
Claude Code with tool use (shell, file edits, code search). A human
reviewed and requested the change.

## Checklist

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

Co-authored-by: lyuboslavmedarov <285605601+lyuboslavmedarov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 17:38:39 -06:00
Svetlana ZolotenkovaandClaude Opus 5.5 bce9551a36 fix(recovery): skip successful-run handoff for routine executions (#9129)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies, and the
issue board is the surface those agents work from
> - Recovery is the subsystem that notices when an agent run finished
but left no next step, and hands the issue back so the work is not
silently dropped
> - `decideSuccessfulRunHandoff` implements that check: after a
successful run it looks for a valid disposition on the issue and
enqueues a corrective handoff wake when it finds none
> - Routine execution issues (`originKind = "routine_execution"`)
deliberately stay `in_progress` between cron fires — one open issue is
coalesced across dispatches through the
`issues_open_routine_execution_uq` unique index
> - Recovery read that intended steady state as a *missing* disposition,
so every successful routine run enqueued a handoff wake, exhausted its
single allowed attempt, and left the issue `blocked` with a
`source_scoped_recovery_action` that re-woke the assigned agent
> - This pull request exempts routine executions from the successful-run
handoff, mirroring the existing issue-monitor maintenance skip
(`isIssueMonitorMaintenanceRun`)
> - The benefit is that routines stop generating self-inflicted recovery
churn while genuine stranded issues keep being caught

## Issue

_No public issue exists for this yet; describing it inline per
CONTRIBUTING.md ("Link Issues or Describe Them In-PR"), following the
bug report template._

**What happened?**
After every successful run of a routine execution issue, recovery
enqueued a corrective handoff wake because the issue had no terminal
disposition. The single allowed handoff attempt was then exhausted and
the issue landed in `blocked` with an active
`source_scoped_recovery_action`, which re-woke the assigned agent.
Observed in production as roughly 27 runs per hour on a single
orphan-check routine, which additionally tripped a false productivity
review on the assigned agent.

**Expected behavior**
A successful routine run that leaves the routine execution issue
`in_progress` is the intended waiting state. Recovery should skip the
successful-run handoff for `routine_execution` issues and let the
routine lifecycle own their disposition.

**Steps to reproduce**
1. Create a routine that dispatches an execution issue on a short cron
interval.
2. Let one dispatched run complete successfully without writing a
terminal status on the routine execution issue.
3. Observe the corrective handoff wake, the exhausted attempt, and the
issue moving to `blocked` with an active source-scoped recovery action.
4. Repeat on the next cron fire — the cycle restarts and the run rate
climbs.

**Paperclip version or commit**
`master` at the time this branch was cut; the guard is absent on current
`master`.

**Deployment mode / adapters / database**
Adapter-independent — the path is server-side in
`server/src/services/recovery/successful-run-handoff.ts`. Covered in
tests against embedded Postgres.

## What Changed

- `decideSuccessfulRunHandoff` returns `{ kind: "skip", reason: "routine
execution owns its own disposition" }` when `issue.originKind ===
"routine_execution"`, alongside the existing issue-monitor maintenance
skip.
- `originKind` was added to the handoff `IssueRow` projection and to the
issue select in `handleSuccessfulRunHandoff` in the heartbeat service,
so the guard has the field it needs.
- Added a regression test in `successful-run-handoff.test.ts` asserting
that a `routine_execution` issue is skipped rather than enqueued.

**Related PRs** (dedup search):
[#8622](https://github.com/paperclipai/paperclip/pull/8622) touches the
same routine-execution uniqueness index from the migration side;
[#6527](https://github.com/paperclipai/paperclip/pull/6527)
auto-resolves stale source recovery actions, which is the symptom this
PR stops producing. No PR was found that implements this guard.

## Verification

- `cd server && pnpm exec vitest run src/services/recovery --config
vitest.config.ts` — recovery suites pass.
- `cd server && pnpm exec vitest run
src/__tests__/heartbeat-process-recovery.test.ts --config
vitest.config.ts` — passes; 59 tests across both suites.
- Regression check: with the `routine_execution` guard removed, the new
test falls through to `enqueue` and fails; with the guard it passes.
- `pnpm --filter @paperclipai/server typecheck` — clean.

## Risks

- Low risk, behavioural only. No schema change and no migration in this
change.
- The guard is narrow: it keys on `originKind === "routine_execution"`,
so ordinary assigned issues keep their existing successful-run handoff
behaviour unchanged.
- If a routine execution issue ever did need corrective handoff, it
would now be skipped. That trade-off is deliberate — the routine
lifecycle already owns these issues, and the coalescing unique index
guarantees at most one open issue per routine.

## Model Used

- Claude (Anthropic), Claude Opus 4.6 via the Claude Code agent harness,
extended thinking enabled, with tool use (file edits, shell, ripgrep)
against a local checkout.

## Checklist

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

---

**Review request (2026-08-19).** Rechecked against `master` a5f3ac6b4:
`decideSuccessfulRunHandoff` skips on `hasActiveRoutineContinuation`
(another live run on the same issue) but never on `originKind ===
"routine_execution"`, so a routine issue coalesced across fires still
draws a missing-disposition handoff. Green and merges cleanly.

We cannot use the reviewer field as outside contributors, so flagging
the recent maintainers of the touched files instead: @cryppadotta,
@devinfoley. Happy to close if this is not wanted — no need to reply
otherwise.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 17:35:34 -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
Nicky LeachandClaude Opus 5.5 2c39a2866b feat(plugins): let managed config grant worker variables to a plugin (#19049)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A managed instance gets its plugins from the control plane, and
people do not install them
> - Some of these plugins need deployment values, such as a service
origin and a capability
> - The plugin worker environment is scrubbed, and only a fixed list of
first-party providers gets deployment variables
> - Core also sends lifecycle events and project instance secrets only
to plugins with a saved config row
> - So a provisioned plugin cannot work until a person saves
configuration for it
> - This pull request lets the managed config grant named variables to a
selected plugin
> - The benefit is that a distribution plugin can run with no tenant
configuration, and Core does not need to name it

## Linked Issues or Issue Description

No issue exists. This describes the problem. I searched open pull
requests for related work and found no duplicate.

**What existing behavior does this improve?**

How a plugin provisioned through `plugins.autoInstall` receives its
deployment settings.

**Current behavior**

`buildPluginWorkerEnv` forwards variables only from a fixed list that
names first-party packages. A distribution plugin cannot be on that list
without Core naming it. A plugin with no `pluginConfig` row gets no
resource lifecycle events, and project instance secrets refuse it.

**Proposed behavior**

The managed config can include `plugins.workerEnv`. It maps a catalog
key to variable names. The worker of that plugin receives those
variables from the server process environment. The managed config can
also include `plugins.enabledCompanies`. It maps a catalog key to
company IDs. In those companies, the plugin gets lifecycle events and
can use project instance secrets with no config row.

**Reason and benefit**

The control plane stays the only place that decides which plugin gets
which values. Tenants do not handle these values, and they are not in
the database or in the managed config document.

**Alternatives considered**

- Create a config or enablement row for each company. The control plane
would need a new tenant write API, and a row can stay after the document
removes access or return after a database restore. The document is never
saved, like the other managed settings.
- Give a grant access to every company. This has no per-company opt-in.
- Put the values in the managed config document. The document does not
hold secrets, and this rule stays true because it holds only names.

## What Changed

- `parseManagedConfigEnv` accepts an optional `plugins.workerEnv`
object. Each key must be in `plugins.autoInstall`. Names must be
uppercase variable names, with no duplicates.
- Startup fails if a name looks like a host credential, such as a name
with `KEY`, `TOKEN`, `SECRET`, `PASSWORD`, `DATABASE` or `JWT` as a
word, or if it controls how the worker loads code, such as
`NODE_OPTIONS`.
- `parseManagedConfigEnv` accepts an optional `plugins.enabledCompanies`
object. Each key must be in `plugins.autoInstall`. Values must be
lowercase company UUIDs, with no duplicates.
- `managedPluginWorkerEnvGrant` and `managedPluginEnabledCompanies`
return values by plugin key. A managed instance only starts a selected
plugin from the current image, so the saved package path is not part of
this check. It can be old or null.
- `buildPluginWorkerEnv` forwards granted variables. This does not
depend on the `environment.drivers.register` capability.
- Plugin lifecycle delivery includes a plugin with no config row only
for the companies the document names. It still skips a company that
disabled the plugin. The runtime event listener follows the same rule.
- Project instance secrets accept a plugin with no config row only for a
company the document names.
- A variable grant alone gives no company access.
- `doc/plugins/DISTRIBUTION-PLUGINS.md` describes both sections.

## Verification

- `npx vitest run` in `server/` on `managed-config`,
`managed-plugin-worker-env`, `plugin-lifecycle-delivery`,
`project-instance-secrets`, `plugin-database` and `bundled-plugins`: 201
tests pass.
- The new tests cover a digest-verified distribution plugin, a stale
saved package path, a catalog key named after an inherited property, a
company the document does not name, a company that disabled the plugin,
a named company that does not exist, a grant with no enabled company,
and host credential names.
- `tsc --noEmit` in `server/` shows no errors in the changed files. My
worktree does not have the built `@paperclipai/paperclip-runner`
package, so unrelated files show errors locally. CI runs the full
typecheck.
- I did not run the full test suite locally. CI runs it.
- I did not test this change on a running instance.

## Risks

- **Older builds:** a build without this change refuses a document with
`plugins.workerEnv` or `plugins.enabledCompanies`, because it fails
closed on unknown keys. A control plane must send these sections only to
a build that supports them.
- **Trust:** the granted plugin gets the variable values in its process.
The image owner and the control plane already trust this plugin. The
name check stops a mistake with host credentials, but a name check
cannot prove that a value is not sensitive. Core cannot use an
allowlist, because it must not name the variables of a private plugin.
- **Company scope:** the control plane sets which companies a plugin
serves. A new company needs a new document, and the server reads the
document when it starts. Self-hosted instances do not change.
- **Rotation:** a new value reaches the worker only when the server
restarts. Rotation without a restart is not part of this change.
- **Background calls:** a grant does not add proactive company access. A
plugin that makes background calls with no invocation still needs a
saved config for that company.
- This changes core plugin behavior, and I did not discuss it in `#dev`
first. Maintainers can tell me if they want a discussion before review.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 15:24:43 -08:00
Devin FoleyandPaperclip c7390bef20 fix(server): stop one slow copy-back from starving workspace retries (#19111)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A native run can accept its result before the workspace copy-back
finishes. A background worker retries a failed copy-back from the
retained sandbox without another provider turn.
> - Until that retry succeeds, the run stays `running` and keeps its
task's execution lock. User answers and wakes for that task stay queued.
> - The worker checked one entry at a time (`batchSize: 1`, no
concurrent checks). A slow sandbox export held every other task's due
retry for the whole export.
> - Work without a saved retry time used a deadline of `0`. Copy-backs
that a restart interrupted always sorted ahead of retries that were due
hours earlier.
> - On a busy server that restarts often, scheduled retries never got to
the front of the queue. Their tasks stayed stuck.
> - This pull request runs up to four checks at once, one per task, and
orders pending work by when it became pending.
> - The benefit is that one slow export cannot stall unrelated tasks,
and an old retry runs before newer work.

## Linked Issues or Issue Description

Refs #17734 (added this worker), #18421 (the same concurrency pattern
for native reply recovery) and #19063 (open; records why finalization
failed a run). This pull request does not change their behavior.

**What happened?**

On a hosted instance, sandbox exports were slow for a few hours. A
harness-state save timed out after about 5.5 minutes in 27 runs, and
worker copy-back attempts took 2 to 34 minutes each. Eleven native runs
then waited in `retryable_failure` (or `workspace_finalizing` with an
expired lease) for more than three hours. Each one had exactly one
copy-back attempt and no retry. The worker's operation receipts show its
attempts ran strictly one after another. After each restart, the worker
first took the copy-backs that the restart interrupted. They have no
`nextAttemptAt`, so their deadline was `0`. The server restarted every
30 to 40 minutes, so the overdue retries never ran. One stuck run held a
task whose user had answered a question card. The answer's wake stayed
queued for the same three hours.

**Expected behavior**

A slow copy-back for one task does not delay due copy-backs for other
tasks. Due work runs oldest first. New work without a saved retry time
does not go ahead of older due retries.

**Steps to reproduce**

1. Create two native runs with accepted results on different tasks. Put
both in `retryable_failure` with an overdue `nextAttemptAt`. The first
is older.
2. Make the copy-back for the older run block.
3. Start the worker. On `master` the second task's copy-back never runs
while the first one blocks.

The new worker test runs these steps against PostgreSQL.

**Paperclip version or commit**

`master` at `69dea878c0b`.

**Deployment mode**

Any server with native runs that use remote sandbox workspaces.

## What Changed

-
`server/src/services/native-runtime/native-workspace-finalization-work.ts`:
- Remove `batchSize: 1`. Add `concurrentChecks: { limit: 4 }` keyed by
`issue:<company>:<issue>`. Runs that share a task keep their copy-backs
serialized. Ownership is still checked per run by the existing advisory
lock and owner receipt.
- Work without a saved retry time uses the coordinator's `updatedAt` as
its deadline, not `0`. The value is clamped to now, because the database
clock can be slightly ahead of the process clock. Saved retry times and
lease expiry behave as before.
- `doc/native-workspace-finalization-recovery.md`: one paragraph on
worker concurrency and ordering.

## Verification

- Failing first: "retries another task while an older copy-back is still
running". On `master` the second task's copy-back never ran while the
older one blocked (the test timed out after 15 s). Now the second task
finalizes while the first is still blocked. After release, the first one
also finalizes.
- Failing first: "orders work without a saved retry behind older overdue
retries". On `master` the interrupted copy-back had deadline `0` and
sorted first. Now it sorts after the older overdue retry, and its
deadline is not in the future.
- `cd server && npx vitest run
src/__tests__/native-workspace-finalization-work.test.ts
src/__tests__/scoped-deadline-work.test.ts
src/__tests__/server-startup-feedback-export.test.ts`: 3 files, 159
passed. After rebasing onto `69dea878c0b`, the first two files: 38
passed.
- `cd server && npx tsc --noEmit -p tsconfig.json`: pass.
- Not verified: the worker on a live instance under slow exports. The
diagnosis comes from that instance's operation receipts and run events.

## Risks

- Up to four copy-backs can now run at once on one server. Each one uses
its own dedicated database connection and sandbox transfer. Live runs
already finalize in parallel, so this is within the existing load
pattern.
- The batch size is now the default (25). A pass ends when its slowest
check ends, but the other lanes keep processing the rest of the batch.
- `updatedAt` changes on any coordinator write. A pending entry with new
writes moves later in the order, but it stays due.
- No schema migration.

## Model Used

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

## Checklist

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

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 15:14:39 -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