Commit Graph
2 Commits
Author SHA1 Message Date
DottaandPaperclip 7351a9d74f fix(native): restore retired backups after warm lease handoffs (#19299)
## Thinking Path

> - Paperclip manages AI agents and keeps their work across turns.
> - A native session can keep the same sandbox through several warm
lease handoffs.
> - Each handoff keeps the old lease row and records matching receipts
on both owners.
> - The retired-backup guard required exactly one row for a provider
allocation.
> - That rule rejected valid history after Paperclip destroyed the
sandbox.
> - This change uses the existing handoff proof to select the final
owner.
> - A verified backup can then restore without discarding ownership
checks.

## Linked Issues or Issue Description

Refs #19208.

**What happened?**

A follow-up received a new sandbox after its old sandbox was destroyed.
The old allocation had a normal warm lease handoff. Both lease rows
remained in PostgreSQL. The guard rejected them with
`runner_harness_state_mismatch: backup_without_reusable_lease`. Its
later same-run retry failed with
`native_remote_recovery_lease_mismatch`.

**Expected behavior**

Restore the verified backup when one final owner has a valid destroyed
receipt and every earlier owner has a matching handoff receipt. Keep
uncertain ownership blocked.

**Steps to reproduce**

1. Complete a native warm turn.
2. Hand the reusable sandbox to another turn.
3. Complete that turn and save its backup.
4. Close the workspace and destroy the sandbox.
5. Send a follow-up. The new allocation must restore the saved session.

**Paperclip version or commit**

Reproduced in the PostgreSQL regression on the parent of this change.
The affected deployed build also contains #19208.

## What Changed

- Select the single final lease owner. Validate all predecessor rows
with `retiredReusableLeasePredecessors`.
- Keep the destroyed receipt, exact backup stamp, and
newer-provider-work checks.
- Add database regressions for one and three handoffs. Reject missing or
changed receipts, an active predecessor, missing history, an unrelated
owner, a stopped source, another backup, and later provider work.
- Document the valid lease-history shape.

## Verification

- Red before the source change: the two valid-handoff cases returned
false. The other 16 database cases passed.
- Green after the change: all 18 database cases passed. They also passed
on current head `d16e098b8c2e6c0dda70984c1df21e3dc792ab34`.
- Command: `pnpm --filter @paperclipai/server exec vitest run
src/services/native-runtime/native-retired-backup-source.integration.test.ts`.
- The database, executor, retry-admission, and lease suites passed: 4
files, 1,308 tests.
- `pnpm -r typecheck` and `pnpm build` passed. The build stamp names
head `4ee733c3a979436196522f36537ff182e9df5398`. The full local suite
stopped in chat shard 3 after a 15-second timeout in `materializes
direct external-chat finals once and accepts the next turn without agent
bookkeeping writes`. Earlier shards passed 702 cases; that shard passed
303 other cases. The isolated test passes on this branch and unchanged
master `10a7d039c8efeac0fd655c24970df5ae3658414b`. All current-head CI
chat shards also passed. The earlier full-suite attempt remains failed;
its timeout cause is not established. No test or timeout was changed. CI
is waiting for its last Runner job.
- Current head `d16e098b8c2e6c0dda70984c1df21e3dc792ab34` removes four
comment lines from the previously tested head. Its 18 database cases
passed. Greptile reviewed this head at 5/5 with no actionable findings:
https://github.com/paperclipai/paperclip/pull/19299#issuecomment-6665446098.
CI remains pending.
- Live inspection found the matching predecessor/successor receipts and
the final owner's destroyed receipt. This was read-only evidence from
the affected build.
- A live provider run at the fixed revision has not been exercised.
Local Daytona credentials and an immutable test image are unavailable.
No deployment or original-task retry is claimed.
- Initial wider tests could not resolve the unbuilt plugin SDK. The SDK
build is now available. A sandboxed database launch could not bind
loopback; the same regression passed with the disposable database
outside that restriction.

## Risks

- A wrong owner could allow an old backup to rewind a session. The
shared validator must account for every lease row and match both sides
of each handoff.
- Live, unrelated, incomplete, or conflicting history stays blocked. The
existing backup integrity and provider-progress checks remain.
- No migration, permission, model, trust, timeout, or retry-policy
change is included.
- The original task still needs the fixed build and normal recovery.
Hosted acceptance remains unverified.

## Model Used

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

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-05 12:20:10 -06:00
DottaandClaude Opus 5.5 f5891564ec fix(recovery): continue from a retired sandbox's backup and hand spent export repairs to the board (#19208)
## Thinking Path

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

## Linked Issues or Issue Description

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

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

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

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

## What Changed

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

## Verification

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

## Risks

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

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 02:40:40 -06:00