Commit Graph
6880 Commits
Author SHA1 Message Date
DottaandClaude Opus 5.5 5eac099fbb fix(agents): batch independent follow-up questions into one card (#19634)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Agents ask users for input with the `request_human_input` tool or
the `ask_user_questions` interaction. Paperclip shows each call as one
question card.
> - In an onboarding walk, a Codex execution agent asked five
independent questions (name, menu and prices, contact, style, output).
It posted five cards, one after the other. Each card came 20-60 s after
the previous answer.
> - The tool description said: "For one question at a time, ask only the
next unanswered question and wait for its real answer." Agents read this
as a rule. The paperclip skill showed only a one-question example and
did not tell agents to batch.
> - No server cap exists. A question set accepts up to 64 questions.
> - This pull request changes the guidance. Agents now ask independent
inputs together in one card. They use a follow-up card only when a
question depends on an earlier answer. One-at-a-time stays available
when the user asks for it.
> - The benefit is fewer waits for the user: one card and one agent turn
instead of five.

## Linked Issues or Issue Description

**What happened?**
An execution agent asked a user five independent follow-up questions in
five separate question cards. Each card appeared only after the user
answered the previous card and the agent finished another turn (20-60 s
each).

**Expected behavior**
When an agent needs several independent inputs, it asks them together in
one multi-question card. The onboarding interview already does this ("1
of 4" pager).

**Steps to reproduce**
1. Complete onboarding on the Codex API path and hand off a task such as
"Create printable cake menu and logo".
2. Watch the execution agent's first turns.
3. See five sequential single-question cards.

**Paperclip version or commit**
master at 4938a930d6 (local dev server).

**Agent adapter(s) involved**
Codex (gpt-5.6-sol). The guidance is shared by all adapters that use the
Runner `request_human_input` tool or the `paperclip` skill.

## What Changed

-
`packages/paperclip-runner/src/protocol-actions/request-human-input.ts`:
replace the ambiguous "For one question at a time…" sentence. The new
text tells agents to ask independent inputs together in one questions
card. They use a follow-up card only when it depends on an earlier
answer. It keeps one-at-a-time for when the user asks for it.
- `skills/paperclip/SKILL.md` and
`skills/paperclip/references/api-reference.md`: add the same batching
rule to the human-input sections.
- `packages/mcp-server/src/tools.ts`: add the batching hint to the
`paperclipAskUserQuestions` tool description.
- Regenerated runner contracts (`semantic-tool-contracts.json`,
`protocol/manifest.json`, `native-execution-seeded.json`) and updated
the legacy MCP alias semantics in `spec/capability/mcp-tool-map.yaml`.
- New tests: `core-contracts.test.ts` (tool description) and
`paperclip-skill-utils.test.ts` (skill and API reference).

## Verification

Red on master (tests added, source unchanged):

```
packages/paperclip-runner: npx vitest run src/protocol-actions/core-contracts.test.ts -t batch
 × tells agents to batch independent questions into one card instead of asking them one by one
AssertionError: expected 'Create a durable human question or ap…' not to contain 'For one question at a time'
 Tests  1 failed | 20 skipped (21)

npx vitest run server/src/__tests__/paperclip-skill-utils.test.ts -t batch
 × tells agents to batch independent human-input questions into one card
AssertionError: expected ' For an open answer, use a text field…' to contain 'ask them together in one card'
 Tests  1 failed | 21 skipped (22)
```

Green after the fix:

```
packages/paperclip-runner: npx vitest run src/protocol-actions src/drivers/runner-tool-bridge.claude-schema.test.ts
 Test Files  4 passed (4)   Tests  46 passed (46)
npx vitest run server/src/__tests__/paperclip-skill-utils.test.ts            Tests 22 passed (22)
npx vitest run server/src/services/native-runtime/paperclip-runner-tool-authority.test.ts   Tests 41 passed (41)
node scripts/generate-semantic-contracts.mjs --check / generate-capability-contract --check / generate-semantic-action-catalog --check / generate-replay-goldens --check: current
```

`packages/mcp-server` has one failure ("allows create issue requests to
omit status…"). It also fails on master without this change, so it is
not related.

This is a guidance change. The automated check proves that the guidance
the agent receives tells it to batch. I did not run a live-model eval.

## Risks

- Low risk. The change is text only: tool descriptions and skill docs.
No schema, API, or storage change.
- An agent could put a dependent question into the same card. The text
tells agents to use a follow-up card for dependent questions.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`) in 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
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 07:48:28 -06:00
DottaandPaperclip 8812c1941d perf(storage): overlap bounded S3 multipart payloads (#19817)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The storage service uploads large workspace snapshots through S3
multipart uploads.
> - The current uploader permits four requests but fills each streamed
body before it starts the next one.
> - Slow payload delivery therefore remains serial even when request
acknowledgements overlap.
> - This pull request fills bounded part buffers and sends those
payloads concurrently through the existing SDK streams.
> - This lets the four-request window carry payload data while it
retains backpressure and failure cleanup.

## Linked Issues or Issue Description

Related implementation: #18659. This fixes payload serialization within
that existing multipart owner. A search found no duplicate open storage
multipart fix.

**What happened?**

A controlled 24 MiB upload started part 2 only after part 1 had
delivered 99.22% of its bytes. The same sequence occurred for part 3.
The four-request cap overlapped acknowledgement waits but did not
provide useful overlap during payload delivery.

**Expected behavior**

Up to four part payloads can travel at the same time. Memory stays
bounded. Exact lengths, bytes, ordered completion, cancellation and
abort cleanup still apply.

**Steps to reproduce**

Run the new slow-consumer test against the previous uploader. Hold all
part consumers until four requests have started. The previous
implementation starts one request and the assertion fails. The revised
implementation starts four requests and then verifies every payload hash
and byte count.

**Paperclip version or commit**

Reproduced on the source from `585c885168`. The final branch is rebased
onto `ae1f781cac`.

**Deployment mode**

The S3 storage provider. Local tests use fake credentials and an owned
loopback HTTP server. No hosted writes were made.

## What Changed

- Share a 32 MiB part-buffer allowance across uploads. Stream up to four
buffered payloads concurrently through the existing SDK requests. When
the allowance is full, use the existing streaming path without an upload
queue.
- Keep the original streaming path for dynamic parts larger than 8 MiB.
This retains the provider part-count limit without larger buffers.
- Extend the existing tests for concurrent slow consumers, the shared
memory window and cancellation in both paths. Verify allowance release
after source and request failures. Check part identity independently of
acknowledgement order.

## Verification

- Baseline RED: the new controlled test failed with one request started
instead of four. Its cleanup released and awaited the upload.
- Final review-repair head: 26/26 tests passed across
`storage-s3-multipart.test.ts`, `storage-s3-failure-diagnostics.test.ts`
and `storage/config.test.ts`.
- These tests include the real S3 HTTP client, exact payload hashes and
ordered parts, excess and truncated input, early acknowledgements,
sibling failure, abort cleanup, stalled input and the bounded four-part
window.
- Strict server TypeScript, module boundary checks and `git diff
--check` passed on the final review-repair head. CI then caught an
optional SDK `Key` type in the new test fixture. A focused test
typecheck reproduced both TS2345 errors. The fixture now binds its
required controlled key; that typecheck and the named shared-window
regression pass. The uploader source is unchanged by this repair.
- Controlled 24 MiB comparison: 640.58 ms before and 393.88 ms after.
The new requests started within 7.37 ms. Both runs used 1 ms per
consumed chunk and 250 ms acknowledgement delays. This measures the
controlled fixture only. It does not predict hosted S3 throughput or
total migration time.
- Review RED: two blocked uploads consumed 64 MiB of source chunks under
the original per-upload buffer scheme. The new shared allowance kept the
source reads within 32 MiB plus stream lookahead. Three new controls
passed for concurrent uploads and allowance release after source/request
failures.
- The first treatment run passed 12/13 cases. Its remaining test assumed
completion order matched part order. The test now binds lengths to part
numbers and still checks the ordered completion request. An initial
typecheck setup attempt lacked the built plugin SDK; the canonical
dependency build and final typecheck passed.

- Final head `fbadf4f1cf427b896bbc255ffe2e7ad480ce06d3`: [full
CI](https://github.com/paperclipai/paperclip/actions/runs/42373571408)
passed, including build, test shards, Typecheck + Release Registry and
`ci / verify`. All 61 reported contexts are terminal: 56 success, four
skipped and one neutral review. Greptile and Paperclip both gave a fresh
5/5 for this head. No review threads remain open.

## Risks

- Multipart uploads share at most 32 MiB of live part buffers per
process, plus existing stream and source-chunk overhead. The previous
path used smaller streaming buffers. With no buffer allowance left, an
upload uses the original streaming path. It does not wait for a shared
queue.
- Objects whose dynamic part size exceeds 8 MiB retain the current
serial payload behavior. No new object-size ceiling is introduced.
- Request bodies remain non-replayable streams. The SDK retry policy,
multipart protocol, cleanup policy, object format and public storage API
remain unchanged.
- No disk spool or cache is added. All current-head CI gates and both
reviews passed.

## Model Used

OpenAI `gpt-6.1-sol`, with reasoning, tool use and code execution. The
context window size is not exposed by this session.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
canary/v2026.1107.0-canary.13
2026-11-07 06:58:10 -06:00
DottaandPaperclip c92bc9e5ec perf: reuse authenticated workspace upload readback (#19811)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Workspace import saves local work to an encrypted immutable snapshot
before it changes storage authority.
> - The snapshot uploader reads the whole object back to verify its
ciphertext.
> - The importer then downloads that same object again for authenticated
decryption and restore.
> - Both reads are required by the current call sequence, but they need
not transfer the object twice.
> - This PR lets that caller retain the authenticated upload readback in
private scratch.
> - The importer still restores the full archive, recaptures the source,
checks ownership, and publishes through the existing CAS.

## Linked Issues or Issue Description

Refs #19642, #19805, and #19810 for the existing workspace import and
preservation owners.

**What happened?**

A fresh workspace import performs a PUT and two full GETs of the same
encrypted snapshot. The first GET verifies and discards the object. The
next GET decrypts it for restore. Already-uploaded and lost-reply
recovery paths also repeat the GET.

**Expected behavior**

Use one authenticated readback for the exact final winning reference.
Preserve the default upload behavior and returned reference. Keep every
restore, source, recovery, ownership, and publication check.

**Steps to reproduce**

1. Capture a local source through the real workspace archive codec.
2. Upload it through the real snapshot ledger and filesystem object
backend.
3. Load and restore its archive. Count the object GETs.
4. The three baseline cases each make two GETs. The opt-in path now
makes one and restores the same bytes after the original source is
removed.

**Paperclip version or commit**

Baseline `585c885168`. The first PR head was `5b18121609`; the follow-up
repairs the review findings without changing the storage optimization.

## What Changed

- Add optional `verifiedArchivePath` to the existing upload owner. The
default path and `WorkspaceSnapshotRef` return contract stay unchanged.
- Authenticate each readback with the existing ciphertext, GCM,
key-binding, and plaintext checks.
- Bind retained files to the full winning reference. Recheck private
file ownership and plaintext SHA/size before atomic publication to a new
caller-owned path.
- Clean all invocation scratch on success or failure. Do not overwrite
caller files or reuse state across calls.
- Use this path in POSIX workspace import. Keep full extraction, the
second source capture, closed alias/source/F/writer/task checks, and CAS
publication. Windows keeps the existing download path and does not
bypass private-file checks.
- Cover real cold restore, fresh/replay/lost-reply GET counts, late
winners, private-path rejection, local tampering, and GCM failure.

## Verification

- Genuine baseline RED: three real PostgreSQL/crypto cases each report
two GETs where the new contract requires one.
- GREEN: all 30 snapshot storage cases, one actual online migration
case, and seven focused migration controls pass; 38 total. The other 232
cases in those files were unselected locally.
- Review RED: four obsolete second-GET fault triggers and one
uploaded-journal retry case failed before repair. The triggers now fire
on the retained readback and assert that they fired. The
uploaded-journal case interrupts after its real ledger commit and proves
recovery and cold Git restore through the same journal.
- A focused platform-selection control proves Windows retains the
original two-GET path. This is not native Windows acceptance.
- Strict server TypeScript, module boundaries, and `git diff --check`
pass.
- Local command from `server/`: `node
../cli/node_modules/tsx/dist/cli.mjs
../packages/db/src/test-postgres-command.ts -- node
../node_modules/vitest/vitest.mjs run
src/services/remote-workspace-snapshot-storage.test.ts
src/services/remote-workspace-online-source.test.ts
src/services/remote-workspace-migration.test.ts --testNamePattern
'remote workspace snapshot storage|imports an alias with
unrelated|removes plaintext verification files|rechecks a service that
becomes|rechecks a writer that appears|preserves a local edit|keeps the
existing download path|missing-base-uploaded-retry' --hookTimeout 90000
--maxWorkers 1 --no-file-parallelism --fsModuleCache`.
- The codec timing control used an exact earlier owner and a synthetic
72.9 MB archive. Discarding final-capture output saved only 3%; this PR
keeps that capture unchanged.
- This removes one full object GET. It does not claim a measured hosted
wall-clock improvement.
- Fresh Greptile review is 5/5 on `0933a0a29a`, with no open findings.
Exact-head CI is complete and green: 56 successful checks, four intended
skips, and one neutral result. The second review returned Ship (4/5);
its optional test-maintainability suggestion is deferred. No source
change or CI restart was made for that suggestion.

## Risks

- A late upload can win with a different reference. The retained file
must match all fields of that final reference; otherwise the owner
authenticates the winner.
- A private file can change before relay. Ownership, no-follow reads,
stable file identity, plaintext SHA/size, and no-overwrite publication
protect that boundary.
- Authentication may overlap the encrypted upload scratch. Peak local
disk use can grow by up to one ciphertext file. Data remains streaming;
no whole-object memory buffer is added.
- Opt-in callers must supply a canonical private directory owned by the
process. Existing callers that omit the option retain their behavior.
- No archive format, database schema, provider lifecycle, writer
protocol, admission, or background polling changes.

## Model Used

OpenAI `gpt-6.1-sol`, with reasoning, tool use, and local code
execution. The context window size is not exposed.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
canary/v2026.1107.0-canary.12
2026-11-07 06:33:12 -06:00
DottaandPaperclip ae1f781cac fix(native): preserve web files without settling remote recovery (#19814)
## Thinking Path

> - Paperclip manages work that AI agents perform for users.
> - Native sessions retain files on the web server and state in a remote
provider.
> - Replacing the web server must preserve its files without changing
remote recovery authority.
> - The existing maintenance owner requires destroyed-provider receipts
even when all web writers have settled.
> - This can keep the old web volume required only because a remote
recovery obligation remains unresolved.
> - This pull request adds a closed web-byte evidence mode to the
existing preservation owner.
> - The benefit is durable web-file coverage with unchanged remote
references, failures and continuation policy.

## Linked Issues or Issue Description

Refs #19748, #19806. A search for native harness filesystem evidence and
open web/harness changes found no duplicate implementation.

**What happened?**

A settled successful native source can retain a raw controller, a
separate warm supervisor and provider-home files while its historical
external lease has no termination receipt. The existing
filesystem-independence owner refuses before it can preserve these web
files. One retained skill file can also remain physically present after
its version row was deleted.

**Expected behavior**

Preserve every nonexcluded web file as authenticated evidence. Keep
missing files absent and deleted skill versions deleted. Leave VM
references, recovery actions, failures and execution authority
unchanged. Validate the published web-byte receipt on an empty
replacement using database bindings only.

**Steps to reproduce**

Use the included successful-source fixture with a settled web owner, an
unresolved external lease, real controller/supervisor files and a
retained skill whose version row is deleted. The baseline rejects at its
destroyed-provider guard. The new explicit maintenance mode preserves
the files and validates on a replacement. A separate fixture uses a
prior successful source, a failed retained last run, a genuinely absent
controller, a warm supervisor and 267 durable checkpoint parts.

**Paperclip version/commit**

Baseline `384fa2b7be`; final branch rebased onto `585c885168`. Current
head: `990210d7e9`.

**Deployment mode**

Original-volume maintenance followed by an ephemeral replacement. No
hosted operation or provider request is part of this PR.

## What Changed

- Add explicit `web_evidence` maintenance mode and a separate
`paperclip.native-harness-web-evidence.v1` receipt. Keep the existing
terminated-provider mode and old manifest shapes.
- Archive the closed canonical session-root inventory, the separate warm
supervisor and exact admitted skill manifests through the existing
encrypted `original_volume_source_evidence` owner. Preserve bytes and
modes. Keep canonical credential exclusions.
- Preserve local skill edits and deleted-version files as forensic
evidence. Create no skill version, executable checkpoint, provider
backup or recovery authority.
- Require original-volume capability, settled web writers,
physical/finalizer locks, exact task/source/cohort/input bindings and
final byte/mode/membership comparisons. Keep genuinely absent raw
controllers absent.
- Bind allocation and stamp identity while allowing later external
status and cleanup observations. Validate replacements from durable
database and archive bindings without the original anchor, filesystem or
storage reads.

## Verification

- Node 24.22.2, PostgreSQL 18.1, macOS arm64. The unchanged baseline
fails at the real destroyed-provider guard. The final revision passes
all 85 owner, original-volume capability and unchanged
continuation-policy cases in 21.14 seconds.
- Tests perform real encryption, persistence through the S3 storage
interface backed by the owned local fixture, authenticated restore and
exact byte/mode comparisons. The 281-byte file remains present while its
version row remains absent. The evidence archive stays separate from the
provider-session backup scope.
- The supervisor-only case preserves 267 database checkpoint parts,
keeps the raw controller absent and validates without original-volume
identity. Existing strict-mode and old receipt controls pass.
- Negative controls cover active web writers, expired but owned
finalizers, physical owners, foreign controller/supervisor identities,
missing manifests, symlinks, changed bytes/modes/membership/source
during upload, changed durable bindings, incomplete receipt coverage and
invalid late publication. Concurrent attempts retain one valid winner
and one audit event.
- Server TypeScript, module boundaries, diff checks and private-data
scans pass. The verification planner's first invocation used a wrong
script path; its canonical invocation refused a plan because fixture
inputs changed during selection. Neither attempt counts as a pass.
Required CI will provide broader application coverage.
- Earlier fixture and treatment failures remain recorded. They exposed a
missing helper, JSONB property-order instability, missing
separate-supervisor mode coverage and fixture timing/source/count
errors. The final same owner suite passes without weakened assertions.
- A canonical cleanup audit found two additional observation fields: the
cleanup controller and manual-attempt ID. Their real publication updates
reproduced false receipt invalidation on the first PR head. The narrow
correction keeps WEB coverage valid through those observations while
changed allocation identity still fails. The exact same 85-case suite
passes afterward. The final `990210d7e9` head has fresh Greptile 5/5
with no comments or unresolved threads. Required CI run
[42370607882](https://github.com/paperclipai/paperclip/actions/runs/42370607882)
passes: 55 successful checks and four intended skips, plus successful
Snyk. The old head's CI was superseded by this correction and is not
counted as a pass.

From `server/`:

```sh
node ../cli/node_modules/tsx/dist/cli.mjs ../packages/db/src/test-postgres-command.ts -- node ../node_modules/vitest/vitest.mjs run src/services/native-runtime/native-harness-filesystem-independence.integration.test.ts src/services/native-runtime/native-harness-original-volume.test.ts src/services/native-runtime/native-harness-filesystem-independence-continuation.integration.test.ts --hookTimeout 90000 --maxWorkers 1 --fsModuleCache
```

## Risks

This changes a maintenance trust boundary. It proves web-byte
preservation, not provider recovery or termination. External recovery
obligations and existing resume failures remain. Unknown layouts,
missing manifests, active local owners and changed authority fail
closed. Invalid receipts are not automatically replaced.

The inventory has independent limits of 128 entries, 16 MiB per file and
64 MiB total. Larger valid data stays on the original volume and blocks
this mode. Canonical credential exclusions remain excluded. Evidence
uses the existing encrypted ledger and retention roots; it does not
become an executable backup. Old receipts remain byte-compatible.

Startup adds bounded per-retained-scope database reads. Measure the
affected cohort's actual serving startup cost before enrollment.
Validation adds no filesystem or object read, worker or recurring poll.
Live source qualification and publication remain separate after review,
merge and normal delivery.

## Model Used

OpenAI Codex, GPT-6 family, with reasoning, repository tools and
PostgreSQL/filesystem test execution. The runtime does not expose a more
specific serving model ID or context-window size.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-07 06:16:04 -06:00
DottaandPaperclip 585c885168 fix: preserve prepared sources held by provider artifact failures (#19810)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Online workspace migration archives local files before it changes
their storage authority.
> - A failed provider session can leave an active board-owned recovery
hold over a prepared local source.
> - The existing migration owner accepts finalized sources and
workspace-export failures.
> - It rejects a settled provider-artifact failure even when the
prepared source and recovery allocation have complete bindings.
> - This PR permits that exact preservation case under the existing
source and finalizer locks.
> - The remote recovery hold remains active. The archive preserves local
bytes and the existing recovery recipe.

## Linked Issues or Issue Description

Refs #19642 and #19805 for the existing online migration and
source-closure owners.

**What happened?**

Online workspace migration rejects a prepared source held by a terminal
`runner_remote_provider_artifact_incompatible` recovery. This also
blocks another local workspace that shares the Git source. The provider
session had executed work. A pre-execution exemption would be unsafe.

**Expected behavior**

Preserve the local source through the existing immutable archive owner
when the board recovery, task version, terminal run, finalizer, and
declared allocation all match. Keep the remote recovery action and
authority unchanged.

**Steps to reproduce**

1. Create a prepared native source and a failed run with provider
session and tool events.
2. Set its finalizer to an unowned terminal artifact failure. Add the
matching active board recovery and task status version.
3. Remove the local descriptor and inspect or migrate a shared local Git
alias.
4. Before this change, the source proof is rejected. The regression
fixture reproduces this failure.

**Paperclip version or commit**

Reproduced on master `1674902b4f`. The final branch is based on
`5a92d53f7d`.

## What Changed

- Accept only the matching active board-owned artifact failure with a
current task block, no live execution authority, an exact producer
fingerprint, and a declared prepared-source allocation.
- Include the task version and complete existing source/recovery
evidence in publication revalidation.
- Lock the proven recovery tasks in sorted company scope before
publication checks. A held descendant task produces the existing busy
error.
- Add a real PostgreSQL regression with 124 tool executions, a missing
descriptor, shared Git aliases, immutable download, and cold restore of
the local archive.
- Add stale, foreign, owned, controlled, malformed, and publication-race
rejection cases.
- Document that this archive preserves local bytes and the recovery
recipe. It does not prove remote export or release the remote hold.

## Verification

- Genuine baseline RED: the positive migration regression rejects the
source before the production fix.
- Focused treatment: 12/12 PostgreSQL cases pass.
- Review-race RED: both a selected source and a Git companion published
while a descendant recovery task was locked. The corrected tests reject
both cases.
- Two affected owner suites after the lock repair: 91/91 pass in 23.18
seconds.
- Strict server TypeScript check passes after the lock repair.
- Module boundaries and `git diff --check` pass.
- [Current-head
CI](https://github.com/paperclipai/paperclip/actions/runs/42367152081)
passes: 56 successful checks, four conditional skips, and one neutral
review check. No checks remain pending or failed.
- Greptile and Paperclip reviews are both 5/5 on `0a6e6c3ece`, with no
open findings.
- The first descendant fixture attempt hit the immutable task/privacy
guard before it reached the race. The corrected fixture creates that
task binding at admission and produces the two genuine REDs described
above.

## Risks

- A broad classification could let a migration move local authority
without a complete recovery recipe. The new branch requires the exact
failure, producer, task version, source allocation, and released
execution owners.
- Evidence can change during upload. Existing locks and full publication
revalidation reject those races.
- The remote allocation may contain changes absent from the local
source. This PR preserves only local bytes and the durable recovery
route. It does not mark remote work exported, finalized, or safe to
discard.
- No schema, provider lifecycle, admission, or polling behavior changes.

## Model Used

OpenAI `gpt-6.1-sol`, with reasoning, tool use, and local code
execution. The context window size is not exposed.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-07 05:27:55 -06:00
DottaandPaperclip 384fa2b7be Allow context compaction while Agent Chat waits (#19801)
## Thinking Path

> - Paperclip manages agents and their work.
> - Agent Chat can wait for a user between useful turns.
> - The task Properties panel already lets that user request context
compaction.
> - The queue treated that maintenance request as an ordinary wake and
refused it while the chat waited.
> - Compaction must maintain that session without starting a new
conversation turn.
> - This change admits the authenticated request, preserves the waiting
state during execution, and prevents competing requests from saving
extra maintenance work.
> - Ordinary and unauthorized wakes remain blocked.

## Linked Issues or Issue Description

Refs #19378, which introduced the existing Compact now action. Refs
#19377, whose review identified this independent queue admission gap.
Searches for `context_compaction` with `waiting` and `Compact now` with
`waiting` found no existing fix for this admission bug.

**What happened?**

An owner clicks Compact now for a waiting Agent Chat conversation. The
real compaction service reaches the heartbeat queue, receives no run,
and reports that compaction did not start.

**Expected behavior**

The authenticated owner can request one separate compaction run while
the chat waits. The request must keep its existing actor, task, agent,
budget, and execution checks. Ordinary waiting-chat wakes must stay
blocked.

**Steps to reproduce**

1. Use an open Agent Chat conversation whose agent supports compaction.
2. Wait until the conversation is waiting for its owner.
3. Request Compact now from the task Properties panel.
4. Observe the refusal before this fix. The new database regression
calls the real compaction service and queue to reproduce it.

## What Changed

- Admit only the existing complete manual compaction envelope. Require a
user actor, no coalescing, the compaction intent, and matching nonempty
request IDs.
- Preserve the waiting conversation state, session generation, reply
boundary and unanswered questions in the existing conversation
preparation owner.
- Recheck active and deferred maintenance under the existing issue
admission lock. Competing clicks cannot save a second maintenance
request.
- Add execution coverage with and without a pending question. Add
concurrent admission and deferred-request controls, including admission
after the pending receipt is cancelled. Keep the existing authorization
and ordinary conversation controls.

## Verification

- The original service-to-queue request failed before the admission fix.
- The execution regression then caught the review finding: compaction
succeeded, but both waiting-state controls failed. The same complete
controls passed after the conversation preparation fix.
- A real concurrent request test recorded two durable maintenance
receipts before the atomic admission fix. It counts deferred authority
as well as admitted runs.
- Current integrated candidate: normal current Runner core and surface
TypeScript exports passed. All seven normal App typecheck programs
passed. The whole queue/execution suites passed 86/86, the whole
context-compaction suite passed 5/5, and 11 selected ordinary
conversation controls passed. There were 102 actual passes; the 29
unselected conversation cases are not counted as passes.
- Database controls use fresh dedicated PostgreSQL 17 databases, Node
24, one test worker and pools capped at four. Private fixture adapters
use the existing dedicated database and normal migrations. They keep
test bodies, assertions and time limits intact.
- Earlier fixture and setup failures remain recorded separately from
product failures. These include an omitted external URL, an invalid
closed-task fixture, a missing crypto import, an incorrect
result-envelope matcher, and an unavailable test-library helper. Each
was corrected at its owning boundary. A test-output text matcher also
missed Vitest's abbreviated failure; the raw assertion stack and failure
were retained.
- Local session tests exercise the actual admission, native Runner
lifecycle, accepted result and persisted conversation state through the
supported test backend. They do not prove live provider compaction, UI
rendering or deployed suspension.
- Exact-head remote CI passed: 59 successful checks, three intentional
skips and one neutral check. Greptile and Ponytail both reviewed this
head at 5/5 with no actionable findings. Ponytail explicitly marked it
Ship.
- Master advanced during review. A local merge evaluation preserves
every unrelated master change and all four PR files. The current App
programs and all three execution controls passed again on that projected
squash tree to check the workspace-finalization seam.

## Risks

The change affects admission and preparation for one maintenance
request. A broad exception could start ordinary work or reopen a waiting
chat. The request therefore keeps the complete existing manual envelope
and authorization checks. Conversation preparation still checks the
session generation. The existing issue lock owns duplicate admission.
There is no new timer, database migration or provider protocol.

## Model Used

OpenAI Codex based on GPT-6 managed the change and reviewed the shared
queue fix. OpenAI `gpt-6.1-sol` authored the focused regression
coverage. Both used reasoning, repository tools and local code
execution. The runtime 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 (the
existing Compact now documentation remains accurate; no new
configuration is added)
- [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>
canary/v2026.1107.0-canary.11
2026-11-07 05:11:44 -06:00
5a92d53f7d fix(adapter-utils): handle closed child stdin safely (#14057)
Handle closed child stdin without losing the child result.

Keep each child stdin error handler scoped to that child. Expected EPIPE
and stream-destroyed failures no longer crash the host. The child close
event remains the source of its exit result.

Verification: 2,024 tests passed and 12 live/platform cases skipped across
87 complete adapter-utils suites. The final 140-test server-utils suite
passed. The regression reproduces the EPIPE crash on unchanged master.

Final qualification: all applicable CI passed and exact-head Greptile Apex
scored 5/5 with no unresolved findings.

Co-Authored-By: Grzegorz Golaś <193330749+greegorij@users.noreply.github.com>
Co-Authored-By: Devin Foley <devinfoley@users.noreply.github.com>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-11-07 03:01:03 -08:00
DottaandPaperclip d2efe3392b fix(native): preserve retired authority evidence (#19806)
## Thinking Path

> - Paperclip manages work that AI agents perform for users.
> - Native sessions retain control-plane files when execution authority
moves to a later run.
> - Volume migration must preserve these historical bytes before a
server can stop depending on its old volume.
> - The existing preservation owner archives the current controller and
supervisor, but refuses a nonempty authority-epoch directory.
> - A settled four-run session can therefore remain blocked after its
current controller archive is complete.
> - This pull request adds a closed historical inventory to the existing
encrypted evidence archive.
> - The benefit is preserved source evidence and database-only
replacement validation without granting execution or recovery authority.

## Linked Issues or Issue Description

Refs #19748. A search of pull requests for filesystem independence and
issues for authority-epoch history found no duplicate change. This fixes
its uncovered historical-file case.

**What happened?**

A settled native session retained three prior controller files in
authority-epoch directories. Its three historical database checkpoint
headers were absent. Maintenance preserved the current controller, then
refused filesystem independence because those historical files remained
uncovered.

**Expected behavior**

Preserve the exact historical bytes and modes through the existing
encrypted evidence owner. Keep original files, prior archive rows, run
outcomes, warnings, stamps, session pointers, and recovery policy
unchanged. Validate the resulting receipt on a replacement without the
source volume.

**Steps to reproduce**

Use the included four-run fixture with a current-controller-only
archive, three prior authority epochs, destroyed provider receipts,
released ownership, and absent historical checkpoint headers. Call the
canonical filesystem-independence owner. The base rejects the nonempty
directory; the treatment preserves the complete evidence and validates
it after removing source-volume identity from the test process.

**Paperclip version/commit**

Baseline `ef83d566cacf1d5f740d701560597dad2bfa4619`; final branch
rebased onto `27170dca49`.

**Deployment mode**

Original-volume maintenance followed by an ephemeral replacement. This
PR performs no hosted migration or provider operation.

## What Changed

- Add optional closed authority-epoch entries to the existing
`original_volume_source_evidence` archive. Preserve nested file bytes
and modes through the existing encryption, readback, ledger, audit, and
retention owners.
- Bind each epoch to its retained run, canonical checkpoint identity
when present, or explicit header absence plus the exact admitted
authority binding. Keep current-source checkpoint and artifact checks
strict. Historical evidence never becomes an executable checkpoint.
- Recheck the complete historical inventory at archive and receipt
publication fences. Keep physical locks, all-run ownership and
termination checks, source capability checks, and final CAS.
- Bind the history inventory and its database facts into the
authenticated archive manifest. Replacement validation uses database
bindings only. Later header presence, removal, retirement, or changed
identity invalidates the receipt.
- Leave history-free manifest bytes and receipt shapes unchanged. Keep
an existing current-controller-only archive when a later complete
archive is added. Unknown children and other nonempty history
directories remain blockers.

## Verification

- Node 24.22.2, PostgreSQL 18.1, macOS arm64. The final four-run fixture
fails on the baseline owner at its nonempty-history guard. The same
final fixture and treatment pass all 51 focused PostgreSQL/filesystem
cases.
- The positive case performs actual encryption, object-store
persistence, authenticated restoration, and exact byte/mode comparisons.
It proves replacement database-only validation with absent source paths
and storage reads refused.
- Controls cover foreign identities, unknown children, owned prior runs,
changed bytes or new files after upload, missing or newly present
historical headers, malformed and retired headers, an unknown authority
override, receipt-identity tampering, and a late concurrent winner.
Existing current-controller evidence and history-free manifest bytes
remain intact.
- The baseline/treatment driver restored only its two owned source files
in `finally` and verified their exact hashes. Earlier setup and
treatment failures remain recorded: missing bundled PostgreSQL library
links, an overly narrow raw-schema check, and an incorrect synthetic
transient-workspace binding. The final baseline and treatment use the
corrected same fixture; those earlier attempts are not counted as
passes.
- Review follow-up `21183b75f4` closes the missing-history-field gap.
Deleting history from an actual published receipt reproduced an invalid
true result before the fix. Unconditional manifest reconstruction now
rejects it; all 46 owner cases pass. Genuine history-free manifests
retain their exact bytes.
- Review follow-up `f3d305dd36` adds suspended historical Runner-state
bytes and mode checks through authenticated restore, active/foreign
state refusals, exact 16 MiB and 64 MiB boundaries, and retention of an
oversized file without publication. All 51 cases and server TypeScript
pass; production behavior is unchanged.
- CI run `42362784772` on the previous head had one unrelated 30-second
timeout in `manual-private-cell-report-assets.test.ts`, with 6,864
support tests passing. The failure remains recorded. The prior head
`21183b75f4b223cd3ec28124cdc194f24b4f79ba` passes [required CI run
42363408227](https://github.com/paperclipai/paperclip/actions/runs/42363408227),
including Runner support: 55 successful checks and four intended skips,
plus successful Snyk. The final test/documentation head
`f3d305dd3690a7db38b57c4a77389c17206cb76b` passes [required CI run
42364958386](https://github.com/paperclipai/paperclip/actions/runs/42364958386):
55 successful checks, four intended skips, one neutral review check, and
successful Snyk. Fresh Greptile is 5/5; all four review threads are
resolved. No timeout or assertion was weakened.
- The count warning is resolved as independent bounds: the actual
descriptor accepts 32 control-plane-only epochs (97 entries) and 32
epochs with 31 Runner states (128 entries), and rejects 129 entries. The
32-epoch and 128-entry maxima both apply; the capture budget is
unchanged.
- Server TypeScript and module-boundary checks pass. The local
verification planner selects ten broad application lanes; required CI
supplies that wider coverage instead of duplicating it locally.
- Reproduce the focused owner suite from `server`:

```sh
node ../cli/node_modules/tsx/dist/cli.mjs ../packages/db/src/test-postgres-command.ts -- node ../node_modules/vitest/vitest.mjs run src/services/native-runtime/native-harness-filesystem-independence.integration.test.ts --hookTimeout 90000 --maxWorkers 1 --fsModuleCache
```

## Risks

This changes a maintenance trust boundary. A source capture is limited
to 128 entries, 16 MiB per file, and 64 MiB total. These limits are
intentionally below the Runner's 256 MiB state maximum. Larger valid
epochs remain on the original volume and continue blocking filesystem
independence. It uses bounded directory reads and file descriptors,
rejects aliases and unknown paths, and rechecks bytes before
publication. Storage failures retain original files and ledger evidence;
they do not settle recovery or authorize a retry.

Startup adds bounded database reads for retained historical header
bindings or their absence. It adds no source filesystem or storage
reads. The selected cohort's serving startup cost still needs
measurement before enrollment. Other historical layouts require their
own canonical source qualification. No missing provider history is
declared recovered, and no automatic corruption repair is added.

## Model Used

OpenAI Codex, GPT-6 family, with reasoning, source editing, shell tools,
and PostgreSQL/filesystem test execution. The runtime does not expose a
more specific serving model ID or context-window size.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-07 04:55:18 -06:00
DottaandClaude Opus 5.5 1674902b4f fix(native): settle a crashed controller's running workspace finalize once a later attempt finishes (#19807)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A native run saves its workspace in a `workspace_finalize` operation
after the agent gives a result
> - A crash loop can kill the server after it records this operation as
`running` and before it finishes
> - The next server resumes finalization and records a new attempt,
which succeeds
> - Nothing settles the first attempt, so it stays `running` forever and
looks like live work
> - This pull request settles the old attempt when a later attempt for
the same run finishes, under the proof that the old controller is gone
> - The benefit is that no workspace operation stays `running` after its
owner is dead

## Linked Issues or Issue Description

Refs #19666, #19759, #19721, #19717

**What happened?**
A control-plane restart chaos test sent SIGKILL to the server while a
native run finalized its workspace. The server crash-looped. One boot
recorded a `workspace_finalize` operation as `running` and then died. A
later boot resumed finalization. Its new operation (with
`resumedFromOperationId` set to the old one) succeeded, and the run
succeeded. The old operation stayed `running` at rest.

**Expected behavior**
When a later finalization attempt for the same run finishes, and the
owner of the earlier attempt is proven gone, the earlier attempt gets a
terminal status with a reason. A live owner keeps its attempt.

**Steps to reproduce**
1. Start a native run that finalizes a workspace.
2. Kill the server with SIGKILL after it inserts the
`workspace_finalize` row, and before it updates the row.
3. Restart the server and let it finalize the run again.
4. See that the first `workspace_finalize` row stays `running`.

**Paperclip version or commit**
`ef83d566ca` (also on `27170dca49`).

## What Changed

- Root cause: `attemptNativeWorkspaceFinalization` in
`server/src/services/native-runtime/native-workspace-finalizer.ts` reads
the previous attempt only to set `resumedFromOperationId`. It records
the new attempt but never settles the previous `running` attempt. The
early "already succeeded" return path also left it alone. Only the
recorder that started a row can settle it (`workspace-operations.ts`
`recordOperation`), and a killed process never does.
- Add `settleSupersededNativeWorkspaceFinalizations` in
`native-workspace-finalization-supersession.ts`. It sets earlier
`running` native finalize attempts for the run to `failed`. It writes
`metadata.supersededFinalization` (reason
`superseded_workspace_finalization`, superseding operation id and
status), an stderr note, a terminal-workspace wake (registered before
the write), and a `workspace_operation.reconciled` activity entry.
- The proof is the run's workspace-finalization ownership, not age. The
caller holds the advisory lock and the durable owner receipt. That claim
requires the previous controller to have released it or to be proven
stopped (same-host process proof, platform proof, or a completed
copyback barrier). The function also skips an attempt that this process
is still running, and attempts with independent obligations
(`uncertain`, `saveWork`). It only touches rows that
`native_workspace_finalizer` recorded and that are older than the
superseding attempt.
- The finalizer calls it after a new attempt reaches a terminal status,
and on the "already succeeded" path. A settlement error is logged and
does not change the finalization result.

## Verification

- `cd server && npx vitest run
src/services/native-runtime/native-workspace-finalization-supersession.test.ts
src/services/native-runtime/native-workspace-finalizer.test.ts` on
embedded Postgres: 72 passed.
- New tests in `native-workspace-finalization-supersession.test.ts` (a
separate file, so `native-workspace-finalizer.test.ts` stays under 1,000
lines):
- A `running` finalize from a dead controller plus a resumed attempt
that succeeds: the old row becomes `failed` with the supersession
reason, no `running` rows remain, and one activity entry exists.
- The resumed attempt fails: the old row is still settled, and
`supersedingStatus` is `failed`.
- A `running` finalize older than an already succeeded attempt: the old
row is settled on the next sweep.
- While another controller holds ownership, the `running` row stays
`running`.
  - A `running` finalize recorded by another service stays `running`.
  - Attempts with `saveWork` or `uncertain: true` stay `running`.
- Red/green: with the settle call disabled, the three settle tests fail
(row stays `running`). With the fix, all pass.
- `npx tsc --noEmit -p server` passes.

## Risks

- Low. The change only writes rows that are already proven abandoned,
and only under the finalization ownership that already fences copyback.
- A settled attempt now reads as `failed`. Readers pick the latest
attempt by creation time, and the superseding attempt is always later,
so the run's finalization outcome does not change.
- Existing leaked rows on runs that never finalize again stay `running`.
This PR does not add a backfill.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 04:52:08 -06:00
DottaandPaperclip e801be7aa9 fix: fence nested Git repositories during workspace migration (#19805)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Online workspace migration must preserve local files before it
assigns remote authority.
> - A task directory can contain a Git repository below its root.
> - The source inspector treated that directory as ordinary, but the
archive owner correctly required a shared Git source fence.
> - This change includes each nested repository in the existing source
ownership and locking checks.
> - The archive still captures the full task directory, including
sibling files and staged and unstaged Git work.

## Linked Issues or Issue Description

Related: #19688 and #19409. Those changes cover physical source fences
and nested Git archive reconstruction. This change repairs source
classification for an ordinary wrapper that contains a repository. No
duplicate fix was found.

**What happened?**

Online migration admitted a managed task directory whose root had no
`.git` marker. A repository below `.review/repository` then reached the
archive owner without a shared Git source fence. Capture failed with
`remote_workspace_archive_online_source_git_requires_shared_fence`. Its
operation and pending snapshot remained available for canonical resume.

**Expected behavior**

Migration must include the nested Git stores and all their source owners
in the existing physical fence. It must preserve the wrapper directory
and sibling files. It must refuse foreign aliases, unowned stores, busy
shared sources, and source changes.

**Steps to reproduce**

1. Create a managed ordinary task directory with a sibling file.
2. Create a real Git repository below `.review/repository` and leave
staged and unstaged edits.
3. Run canonical online workspace migration.
4. Observe the shared-fence refusal, instead of a durable archive that
can restore all the work.

## What Changed

- Add a bounded metadata walk for nested Git roots. Reuse the existing
strict Git pointer, common-directory, and alternate-store reader.
- Add nested source identities to the existing alias closure, company
checks, source locks, and metadata fingerprints. Keep the wrapper as the
capture root.
- Validate bounded Git-maintained reverse references in selected and
borrowed stores. Require stable two-way pointers and containment by
included registered workspaces. Bind the listings and pointer receipts
into publication revalidation.
- Add seventeen integration cases for fresh migration, pending-journal
resume, shared linked and alternate stores, unrelated progress, foreign
and unowned stores, and late metadata changes.

## Verification

- Genuine baseline RED: both fresh and pending-journal cases failed at
the existing archive shared-fence guard before the source fix.
- The first treatment passed six of seven cases. The late-change case
correctly failed at the earlier recapture guard; its expected-error
assertion was corrected. The final test also verifies that the canonical
Git fingerprint changed.
- Head `8bb9f5a3ce`: real PostgreSQL, all **187 tests passed** in 59.22
seconds across `remote-workspace-migration.test.ts` and
`workspace-git-source-roots.test.ts`. The canonical wrapper creates
disposable databases and completed cleanup. Run from `server/`:
  ```sh
node ../cli/node_modules/tsx/dist/cli.mjs
../packages/db/src/test-postgres-command.ts -- node
../node_modules/vitest/vitest.mjs run
src/services/remote-workspace-migration.test.ts
src/services/workspace-git-source-roots.test.ts --hookTimeout 90000
--maxWorkers 1 --no-file-parallelism --fsModuleCache
  ```
- Required alias RED: both hidden ordinary-wrapper cases failed before
the reverse-reference fix. The shared case bypassed its held writer
lock; the foreign case was incorrectly eligible. Both now pass. A
further RED proved the same omission through a borrowed store; its
hidden writer is now included too.
- Review RED: a repository below `.review/.paperclip-runtime` reached
the archive guard. Discovery now excludes the runtime name only at the
capture root, matching the archive.
- The positive cases download the authenticated immutable archive,
remove the disposable source, and cold restore the sibling file, HEAD,
index content, and working content. Resume retains the exact operation
and pending snapshot IDs.
- Resume fidelity: migration, snapshot, and layout owners are
byte-identical to source revision `2d4f80504e`. Source kind comes from
the workspace row, not online classification. The unchanged serializer
writes the same empty source, source fingerprint, marker, fixed empty
child scope, and pending generation-1/epoch-1 snapshot at the tested
failure boundary. The test asserts that shape and retains the exact IDs
on resume. The fixture injects the failure at that unchanged boundary;
it does not execute the old binary.
- Shared-store cases hold a real PostgreSQL source lock. The related
import is refused before a journal exists, while an unrelated ordinary
import completes.
- Invalid-link controls reject foreign and unregistered roots, malformed
pointers, and symlinked metadata before a journal exists. A new detached
worktree after upload changes the listing fingerprint and blocks
authority publication.
- Final head `e525b6b1a5`: all 17 focused real PostgreSQL cases pass in
16.23 seconds after the enclosing-host-repository review fix. Its true
RED first showed the valid managed wrapper as ineligible. The cold
restore now excludes the ancestor Git directory and host-only file and
leaves the host HEAD unchanged. Server TypeScript passes again.
- Server TypeScript and module boundary checks pass. Canonical agent
preflight passes. Environment: macOS arm64, Node 24.22.2, real
PostgreSQL and Git.
- A first test launch lacked the built plugin SDK, and a later sandbox
launch could not open TSX IPC. Both were setup failures, not regression
results. The existing dependency build and permitted isolated test
runner resolved them.
- Final exact-head CI passes: [run
42363940320](https://github.com/paperclipai/paperclip/actions/runs/42363940320).
The final gate has 56 successful checks, four skipped checks, one
neutral check, and no pending or failed checks. Greptile reviewed this
head at 5/5, with zero open review threads. No hosted workspace was
changed or imported by this PR work.

## Risks

- Nested discovery and reverse-reference checks add bounded directory
metadata I/O. Reverse listings allow at most 256 worktrees per store,
and all links must resolve to registered physical workspace roots. Stale
or unsupported links fail closed.
- Nested discovery refuses more than 100,000 entries, 64 directory
levels, or 64 repositories. A large or unsupported tree remains
ineligible. This includes dependency or build trees that exceed those
limits. No broad directory skip or metadata cache bypasses final source
checks.
- The archive guard, source lock owner, runtime writer checks,
publication checks, and recovery predicates remain required. This change
grants no permission to retire source work or ignore a held journal.
- No schema, API, runtime setting, provider lifecycle, or background
worker changes.
- This PR documents the internal source-classification contract above.
It adds no operator command or configuration to document separately.

## Model Used

OpenAI `gpt-6.1-sol` through Codex, with reasoning, repository tools,
code execution, and regression tests. The context window size is not
exposed.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-07 04:40:08 -06:00
795ad16c1f fix(server): bound the shutdown steps before the run drain (#19165)
Bound initial shutdown waits and finish workspace repairs before teardown.

Share a 20-second budget between waits before the heartbeat run drain.
Retain the original workspace recovery stop promise and join it after the
run drain and finalizers, before application and database teardown. Keep
that join for hot restart when the run drain is skipped. Log step and
drain durations and bound telemetry flush separately after the drain.

Verification: all 154 tests across the three relevant shutdown/startup
suites and every server source/test typecheck layer passed. Both actual
server lifecycle variants reproduce premature teardown without the fix.
All applicable CI passed and exact-head Greptile Apex scored 5/5.

Co-Authored-By: Devin Foley <devinfoley@users.noreply.github.com>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-11-07 02:24:23 -08:00
Devin FoleyandPaperclip 51ab7ebdc6 fix(recovery): admit recovered scheduled preparation retries (#19802)
Allow explicit Retry after a recovered scheduled setup failure.

Recognize the exact host scheduled-retry promotion only when its saved
parent, attempt, deadline and reason match the source run and context.
Keep changed, extra or foreign evidence held. Preserve the existing
provider-absence, stopped-source, recovered-file and authorization gates.

Keep refusal tests receipt-only until they change saved proof, then
invoke the real retry handler. Positive tests retain real HTTP delivery.

Verification: 217 unique tests passed across five recovery suites. The
12-test affected suite and its typecheck layer passed again after the
test-race fix. All applicable CI passed; exact-head Apex review was 5/5.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-11-07 02:20:02 -08:00
Devin FoleyandPaperclip bf12b2d8ad fix: retry reviewed chat setup after temporary contention (#19787)
Retry reviewed chat preparation with the original accepted input.

Bound retries for authorization proof waits that expire before provider
work. Preserve the original answer receipt and queue later wakes
separately. Keep plan confirmation semantics by assigning the interaction
marker only to question answer delivery and legacy question payloads.

Verification: 433 tests across four backend suites, all 17 project-chat
browser tests, and all server source/test typecheck layers passed. The
plan approval regression was reproduced on the previous head before the
fix. All applicable CI passed and exact-head Greptile Apex scored 5/5.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-11-07 02:14:42 -08:00
DottaandPaperclip 27170dca49 fix(workspaces): preserve the read-only capture phase budget (#19803)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Remote workspace saves must preserve the files produced by a
completed turn.
> - When a sandbox disk is full, the existing fallback transfers a
stable source and seals it on the controller.
> - The fallback gives metadata checks and the portable codec the same
two-minute timer.
> - Slow but successful metadata checks can leave the codec only
milliseconds, although normal sandbox capture allows five minutes.
> - This change keeps the metadata budget and gives the local codec the
existing finite capture budget.
> - All source checks, archive checks, ownership checks, and failure
cleanup remain required.

## Linked Issues or Issue Description

Refs #19465. Related: #19364 and #19619. The open diagnostic change in
#19619 records fallback causes; it does not separate these phase
budgets. No duplicate budget fix was found.

**What happened?**

A retained workspace failed five read-only exports at `stage=archive`
with `transport_timeout`; four warnings also named `ABORT_ERR`. The
fallback had already transferred the source and passed the received-tree
and source inventory checks. Its controller codec received the same
two-minute signal used by those checks.

**Expected behavior**

Metadata checks share a finite two-minute budget. Sealing uses the same
finite five-minute policy as normal sandbox capture. The final source
check uses only the unused metadata budget. A changed source, expired
budget, or cancelled codec must not produce a preservation receipt.

**Steps to reproduce**

1. Create a standalone Git fixture with staged changes, an untracked
result, and a confined symlink.
2. Make the initial sandbox helper report the exact disk-full error.
3. Transfer real files and run the actual inventory scripts. Delay a
successful second inventory reply until just before the native
two-minute deadline.
4. Observe that the real portable codec starts with a live signal, then
aborts when that shared timer expires.

## What Changed

- Name and reuse the existing 300-second command policy for controller
capture.
- Retain the aggregate 120-second metadata budget. Preserve its unused
remainder for the final source recheck.
- Keep the exact disk-full error, source comparisons, file/Git checks,
and cleanup behavior.
- Add six real-filesystem and real-codec controls. Document the separate
phase limits and the current absence of a caller abort signal on the
save interface.
- Repair the existing onboarding test's fake-environment mapping with
the same real-agent PATCH mapping used by its sibling. Verify the
persisted agent through a real GET. Production UI code is unchanged.

## Verification

- Native-timer RED on base `35d41f11bb`: a successful source reply
arrived at 119,981.62 ms. Its command took 119,854.47 ms within its
119,873 ms cap. The actual codec started at 119,982.70 ms with a live
signal and aborted at 120,001.84 ms. Its nested cause matched the
retained warning: `stage=archive reason=operation_failed
transport=transport_timeout code=ABORT_ERR`. The source inventory was
unchanged and private staging was removed.
- The real-archive control sealed 10,166,395 bytes and 533 entries in
555.55 ms. It restored HEAD, staged data, untracked data, and the
symlink, and passed Git fsck. The recorded delay is synthetic; it is not
a measurement of the failed hosted workspace or provider throughput.
- The native RED leaves about 18 ms for the final check too. The
repaired code must still refuse that exact exhausted-metadata case; it
is not claimed as a native-clock GREEN.
- The committed fast regressions control phase time and delegate to the
actual inventory commands and portable codec. They cover successful
restore after the old metadata signal expires during sealing,
pre-capture metadata expiry, codec deadline/cancellation, and final
metadata expiry after a real completed archive.
- From `server/`: `node ../node_modules/vitest/vitest.mjs run
src/services/remote-workspace-readonly-capture-budget.test.ts
src/services/remote-workspace-transport.test.ts
src/services/remote-workspace-readonly-inventory.test.ts
src/services/remote-workspace-transfer-timeout.test.ts --fsModuleCache`
— 156 tests passed in 42.30 seconds before removal of a redundant test
duration assertion. The final-head six-case regression also passes after
rebasing onto `fff70fe49e`; only unrelated Runner and E2E files changed
in that rebase.
- Review follow-up `ad565dd984` makes the phase clock fully controlled.
It checks the requested final 2,000 ms budget separately from the real
subprocess's 30-second test limit. All six controls pass in 11.94
seconds, and server TypeScript passes again. The earlier
real-time-plus-offset clock could falsely fail on a loaded machine; that
review finding is fixed.
- Server TypeScript and module boundary checks pass. The first
TypeScript attempt could not resolve the unbuilt plugin SDK. Building
that existing workspace prerequisite resolved the setup errors; no
source workaround was added.
- The first preflight attempt used the host pnpm shim and was stopped.
The second lacked sandbox network access and was stopped. The canonical
cached preflight then completed in 22.1 seconds with the Runner build
restored from cache. A later test launch used the wrong working
directory for a test rewrite; its wrapper was stopped before waiting
through a second native-timer test. None of these interrupted attempts
is a pass.
- Final-head CI first failed one unchanged Runner-support privacy test
at 30 seconds: [job
failure](https://github.com/paperclipai/paperclip/actions/runs/42359560195/job/128365192635).
The other 6,864 tests passed and four were skipped. The failing test and
its report owners match base `fff70fe49e`; the exact case passes locally
unchanged in 2.97 seconds (one pass, 40 unselected). Job cleanup
reported an orphan `unzip`; that does not establish the timeout cause.
GitHub rejected the scoped retry while the parent workflow was still
running, so no retry has started. No old-head retry was started because
the separately proven onboarding fixture repair requires a new head and
a fresh full pipeline. The original Runner failure remains recorded.
- The same CI run failed onboarding case 160 at 60 seconds; its retained
page showed `Environment not found`. The fixture injected an environment
only in browser responses, then sent that fake ID to the real agent
update. The original case passed locally in 37.44 seconds despite
rejected HTTP 422 updates. Adding a real update-success guard produced a
local RED in 26.83 seconds (expected 200, got 422). Reusing the sibling
fixture's fake-ID-to-local-default PATCH mapping produced GREEN in 22.52
seconds. A real GET verifies the same persisted agent ID, name, and
default environment. Existing sign-in, cancellation, and no-hire
assertions are unchanged. CI screenshot/error context and the local
persistence RED remain retained.
- All other product checks on `ad565dd984` passed, including build,
TypeScript, all general and serialized server shards, and native Runner
checks. Final head `58bb4ee2411dfefca462f0e9b5a701d84ce1326e` passes the
fresh pipeline: [CI
run](https://github.com/paperclipai/paperclip/actions/runs/42360927780).
Runner support passes in 3m36s and onboarding shard 5/8 passes in 5m46s,
with no Runner-support source change. The original failures remain part
of this verification history.
- Environment: macOS arm64, Node 26.4.0, real Node/Git/filesystem
operations. No provider or recovery calls, replayed turn, source
mutation, or hosted recovery acceptance was performed. Final-head
required CI and review pass; the exact gate state is recorded below.

- Exact-head final gate: 57 successful checks, three skipped, two
neutral, and no failures. Both required gates (`ci / verify` and `ci /
e2e`) pass. Greptile reviewed this exact head at 5/5, and zero review
threads remain open. The non-required companion `review` job is still
queued with no steps; it has not passed, and GitHub therefore reports
`UNSTABLE` while the PR remains mergeable.

## Risks

- Controller work after transfer can now take up to two minutes for
metadata plus five minutes for sealing. Each phase remains bounded.
Existing entry, repository, configuration, and transfer limits stay in
force.
- The save interface has no caller abort signal. This change adds no
Stop guarantee and does not replace the recovery retry window with an
operation deadline. Existing physical ownership and publication fences
remain in force.
- This proves the timer mechanism with a synthetic slow inventory. It
does not measure the actual dirty workspace size or show that its
archive finishes within five minutes. A guarded file-only recovery and
independent durable readback remain required after normal delivery.
- The onboarding fixture maps only its exact synthetic environment ID.
It still calls the real agent update and read routes. It does not loosen
production environment validation.
- No schema, provider transport, task status, recovery policy, or source
cleanup authority changes.

## Model Used

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

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
canary/v2026.1107.0-canary.10
2026-11-07 03:58:24 -06:00
DottaandClaude Opus 5.5 2468b3f907 fix(native): tell a restart continuation that earlier processes may be gone (#19804)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - After a control-plane restart loses a turn, the native runtime sends
a same-conversation continuation (#19501, #19699, #19771)
> - A restart can also kill shell or background processes that earlier
turns started
> - In three restart storms, the continuation agent could not inspect
its killed `bench.sh` and set the task to blocked
> - The continuation prompt told the agent not to "blindly rerun an
action whose outcome is unknown", so the agent did not re-run the
command
> - This pull request tells the agent that earlier processes may be
gone, and to check the workspace outputs and re-run a command whose
expected output is missing
> - The benefit is that a task can finish after a restart instead of
blocking on a process that no longer exists

## Linked Issues or Issue Description

Refs #19501, #19699, #19771

**What happened?**

Restart chaos workload B5 (Codex and Claude) failed this way three
times: storm 6b t14, s7111 t16 and storm 10 t12. The agent started
`bench.sh` in the background. A restart killed it. The restart
continuation agent tried to find its tool session and got "process
inspection is denied". It then blocked the task. The comment read:
"Blocked by the Paperclip runtime restart: the benchmark session is
unavailable…". It added that it "cannot verify whether the original run
is still active or recover its output, so I have not rerun it".

**Expected behavior**

The agent knows it is in a resumed session after a restart. It checks
the workspace for the command's outputs. If the expected output is
missing, it re-runs the command and finishes the task.

**Root cause**

`packages/paperclip-runner/src/native-session-runtime.ts`,
`restartContinuation`: the prompt did not say that processes from
earlier turns may be gone. It told the agent to inspect each unfinished
command's "current state" and not to "blindly rerun an action whose
outcome is unknown". With the process gone, the agent could not inspect
it, so it blocked.

## What Changed

- The restart continuation prompt now says:
  - This is a resumed session after a Paperclip control-plane restart.
- Shell commands and background processes from earlier turns may no
longer be running, and their tool sessions may be gone. Do not block
only because one cannot be found or inspected.
- Check the workspace for the outputs or artifacts the command should
have produced. If the expected output is present, use it. If the
expected output is missing, re-run the command.
- Do not repeat completed work. Before repeating an action with external
effects (a push, a message, a deployment), check whether it already took
effect. If you cannot tell, report the blocker.
- The prompt does not say that re-running is always safe. Process
teardown (#19727) has not merged.
- `doc/execution-semantics.md`: one additive sentence.

## Verification

- `native-session-runtime.test.ts`: the three restart continuation tests
(Codex, ACPX, OpenCode) now check the new guidance. They were red before
the prompt change and are green after it.
- Guard: "uses the admitted session cwd in the task envelope" now checks
that fresh turns and ordinary resumed turns do not contain the restart
guidance.
- `npx vitest run src/native-session-runtime.test.ts src/drivers/codex/
src/live/runnerd-restart-continuation.test.ts` in
`packages/paperclip-runner`: 603 passed.
- `tsc -p tsconfig.json` and `tsc -p tsconfig.test.json` for
`packages/paperclip-runner` are clean.
- Not run: a live restart-chaos replay with a real model.

## Risks

- Low. This changes only the prompt text of the restart continuation.
Ordinary turns do not change.
- An agent may now re-run a command whose earlier run had partial side
effects. The prompt asks it to check external effects first, and to
re-run only when the expected output is missing.

## Model Used

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

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 03:46:20 -06:00
DottaandClaude Opus 5.5 6fb6220d96 fix(restart-chaos): rebuild the Daytona plugin when its sources change (#19798)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness runs Daytona waves with a local control
plane that installs the bundled Daytona sandbox-provider plugin from
`packages/plugins/sandbox-providers/daytona`
> - That plugin is excluded from the pnpm workspace, so workspace builds
and `scripts/agent-preflight.mjs` never rebuild it
> - The harness built it only when `dist/` was missing. A long-lived
chaos worktree that moved across many master commits kept serving the
plugin it built on day one
> - As a result, plugin-side fixes such as #19696 (reclaim sandboxes
left by a restart during acquire) and #19754 (keep a runner sandbox
alive through the reconnect grace) were not under test in those waves,
while server and runner fixes were
> - This pull request keys the plugin build on a content hash of its
inputs and rebuilds when the hash changes
> - The benefit is that every Daytona wave tests the plugin of the
commit it claims to test

## Linked Issues or Issue Description

**Bug description**
`ensureDaytonaPlugin()` in `tests/restart-chaos/main.ts` returned early
whenever `dist/manifest.js`, `dist/worker.js` and the Daytona SDK
existed.

**Steps to reproduce**
Build the plugin on commit A, check out a later commit B that changes
`packages/plugins/sandbox-providers/daytona/src`, and run a `--env
daytona` wave. The wave installs the commit-A plugin. In one chaos
worktree, `dist/plugin.js` was 20 hours older than `src/plugin.ts` and
lacked `runnerReconnectGraceMs` from #19754.

**Expected behavior**
The harness rebuilds the plugin whenever its sources, lockfile or the
plugin SDK source changed since the last build.

Refs #19544

## What Changed

- `tests/restart-chaos/plugin-build.ts` (new): `pluginBuildInputHash`
hashes the plugin's `package.json`, `pnpm-lock.yaml`, `tsconfig.json`
and `src/**`, plus extra inputs: the plugin SDK `src`, `package.json`
and `tsconfig.json`, and the root `tsconfig.json` and
`tsconfig.base.json` that both packages extend. It excludes
`node_modules` and `dist`. `pluginBuildState` compares that hash with a
stamp in `dist/.restart-chaos-build-input.sha256` and reports a reason.
- `tests/restart-chaos/main.ts`: `ensureDaytonaPlugin` installs, links
the SDK and builds when the state is stale, then writes the stamp. It
records `daytona_plugin_build_state` and `daytona_plugin_built` in the
timeline.
- `tests/restart-chaos/plugin-build.test.ts` (new): an unstamped build
is stale; the stamped build is up to date; a changed source, lockfile or
SDK source is stale; changes to `node_modules` and `dist` are ignored;
missing outputs or dependencies are stale.

Other out-of-workspace packages, checked as requested:
- `pnpm-workspace.yaml` excludes only
`packages/plugins/sandbox-providers/**`.
- The restart chaos local mode (`--env local`) does not install a
sandbox provider.
- CI's runner-e2e "Prepare bundled Daytona plugin" step runs on a fresh
checkout per job, so it cannot see a stale `dist/`.
- The other sandbox providers are not used by these harnesses. The risk
is therefore limited to long-lived local worktrees running Daytona
waves, which this change covers.

## Verification

- `vitest run --config tests/restart-chaos/vitest.config.ts`: 121/121
pass, including the new staleness tests.
- `tsc -p tests/restart-chaos/tsconfig.json`: no errors in the changed
files.
- Manual: in the chaos worktree, `dist/plugin.js` (built 2026-11-06
11:16) lacked `runnerReconnectGraceMs`. After a rebuild at master
35d41f11bb it contains it.

## Risks

- This change touches the test harness only.
- The first wave after this change rebuilds the plugin once in each
worktree, because existing builds have no stamp. That takes a few
seconds.

> 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, in Claude Code with
tool use (shell, file edits, GitHub CLI). Default context window,
extended thinking.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 03:44:25 -06:00
DottaandClaude Opus 5.5 ef83d566ca fix(native): ask for the result once after a restart continuation ends without one (#19771)
## Thinking Path

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

## Linked Issues or Issue Description

Refs #19501, #19699, #19720

**What happened?**

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

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

**Expected behavior**

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

**Root cause**

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

## What Changed

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

## Verification

Red before the fix, green after:

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

Also run:

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

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

## Risks

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 03:22:37 -06:00
DottaandClaude Opus 5.5 fff70fe49e fix(runner): relaunch a provider whose runner died before admitting it (#19784)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs execute through `paperclip-runnerd`. When the control
plane restarts, recovery starts a new runnerd process, and that process
restores the provider (for example the Codex app server) from durable
state.
> - Each provider launch writes a durable startup fence before it spawns
the process. Admission clears the fence. The fence stops a second runner
from launching a provider while an unadmitted one may still run.
> - A restart chaos wave sent SIGTERM and then SIGKILL to the control
plane in quick succession. The second kill landed after the restored
Codex provider was spawned and before it was admitted.
> - The fence stayed in state with phase `spawned`. Every later runner
refused to restore. The recovery probe's `runner.drain` failed with
"provider startup ownership remains unadmitted", and the task needed a
manual Retry.
> - This pull request lets a new runner retire that fence when the
recorded provider process and its process group have provably exited. It
keeps every other fence closed.
> - The benefit is that a double restart during provider relaunch
continues automatically.

## Linked Issues or Issue Description

**What happened?**
A Codex run was active. A graceful restart (K2) suspended it, and
recovery started a new runnerd that began restoring the provider. That
runnerd spawned the Codex app server (pid recorded, phase `spawned`). A
hard restart (K1) 9 s later killed it before admission. At the next
boot, recovery chose `resume_dead_runner` and queued its `runner.drain`
probe. The new runnerd failed the probe with `command_execution_failed:
provider startup ownership remains unadmitted`. The run ended
`native_session_recovery_failed`, and the task needed a Retry. The kept
provider state had `lifecycle: "turn_active"` and a `startupAttempt`
with `phase: "spawned"`, `trigger: "restore"`, `processId` =
`processGroupId`, and no recorded exit. That process no longer existed.

**Steps to reproduce**
1. Start a Codex task on a local runner.
2. Restart the control plane gracefully during the turn.
3. Kill the control plane again while recovery relaunches the provider,
before the provider is admitted.

**Expected behavior**
Recovery relaunches the provider and continues the run.

**Actual behavior**
Every later runner refuses to restore, and the run fails.

## What Changed

- `provider_backend.rs`: after a runner loads durable provider state,
`retire_orphaned_startup_attempt` clears the startup fence only when all
of these hold:
  - the attempt is in phase `spawned` (neither admitted nor failed);
- the session is still restorable (`session_open`, `turn_active` or
`provider_exited`);
- signal 0 returns `ESRCH` for both the recorded process and its process
group.

It records a `provider_startup_retired` diagnostic event with the
attempt, persists the state, and lets the normal restore path relaunch
the provider.
- Intent-phase fences (OpenCode). Three OpenCode runs after a K6 crash
loop, and other runs on master, stopped at phase `intent` with no
recorded process. Under load, the runner spends seconds verifying launch
artifacts after it records intent. An intent alone could not show
whether a process was started.
  - A new intent now carries `beforeSpawn: true`.
- A new `SpawnStarting` observation clears the mark immediately before
every provider spawn site in `start_with_tools_observed`. That clear is
fsynced, but it emits no extra diagnostic event.
- An intent still marked `beforeSpawn` (in a restorable session) proves
that no process was started, so the fence is retired.
- An intent that reached the spawn, or one written before this mark
existed, stays fenced.
- A runner that predates the field rejects the state as unknown
(`deny_unknown_fields`), which fails closed.
- `validate()` only allows the mark on an intent or on a spawn-stage
failure.
- The fence stays closed for:
- an intent that reached the spawn, or one without the before-spawn
mark;
  - a failed startup (initialization or spawn);
  - a session that recovery closed;
  - a process or group that may still run;
  - a permission error or an invalid id.
- The existing test
`failed_authenticated_identity_commit_keeps_the_durable_startup_fence`
now uses a live process group instead of the fixed pid 123. That pid
might not exist on a CI host, so the test would no longer prove a
running provider keeps the fence.

## Verification

- Red before the fix: the new integration test
`restart_relaunches_a_provider_whose_runner_died_before_admitting_it`
(fake Codex app server; turn active; a `spawned` restore fence with an
exited process group; then the recovery `runner.drain` probe) failed
with `provider startup ownership remains unadmitted`, the incident
error. After the fix it passes. It shows one more `thread/resume`, the
retired diagnostic, and a cleared fence.
- Unit tests (unix-only module): retirement for each restorable
lifecycle, and for an intent still before the spawn. A `SpawnStarting`
outside its intent is rejected. The fence stays closed for:
  - an intent that reached the spawn;
  - an intent without the before-spawn mark;
- a real child that survives its killed leader (process-group probe, no
stubs);
  - a failed spawn;
  - a failed admission;
  - a closed session;
  - a live provider;
  - a surviving process group.
- `cargo test -p paperclip-runner-core --test codex_provider`: 119
passed. The startup event counts are unchanged, because the spawn-start
mark emits no event. The existing fence tests
(`failed_provider_startup_is_persistently_fenced_before_another_process_can_resume`,
`durable_recovery_closes_a_provider_that_reopens_a_settled_turn`) still
pass.
- `cargo test -p paperclip-runner-core --lib`: 570 passed. `cargo check
--workspace --tests` passes.
- `cargo fmt --check` passes.

## Risks

- Process ids can be reused. A reused id only makes a gone process look
alive, so the fence stays closed. Retirement needs `ESRCH` for both the
pid and the group.
- A provider process that moved to a new process group is outside this
check. Before admission the runner has sent it no turn, so it cannot
repeat provider work.
- Intent fences written by older runners, including the kept incident
states, have no before-spawn mark. They stay fenced and still need the
Retry. New launches carry the mark.
- A runner-side change takes effect on local runners after the runner
build. Daytona needs a new image.

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

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), used through Claude
Code with tool use (shell, file edits, test execution). Default context
window. Extended reasoning was available.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 03:12:13 -06:00
DottaandClaude Opus 5.5 9cc3760552 test(runner-e2e): allow controller outages up to ten minutes (#19797)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness and the runner-e2e recover-controller
cells stop the control plane with `replaceController`
(`tests/runner-e2e/controller-outage.ts`), hold it down, then start it
again
> - `parseControllerOutage` refuses holds longer than 300000 ms
> - Restart chaos needs longer holds. One example is proving that a
Daytona sandbox survives an outage longer than its old 5-minute
auto-stop but inside the 7-minute runner reconnect grace (RC-54)
> - With the cap, such a fault errors before the controller stops, so
the scenario cannot run
> - This pull request raises the test-harness cap to 600000 ms
> - The benefit is that chaos runs can now inject 5–10 minute outages;
production code and qualification contracts are unchanged

## Linked Issues or Issue Description

**Bug description**
A restart chaos replay with a 360000 ms K4 hold failed with "Controller
outage requires SIGTERM/SIGKILL and 0..300000 integer milliseconds". The
controller was never stopped.

**Expected behavior**
The test harness accepts outages of up to 10 minutes. The
recover-controller receipt check keeps its own readiness limit
(`readyAfterMs < 420000`).

Refs #19544

## What Changed

- `tests/runner-e2e/controller-outage.ts`: adds
`MAX_CONTROLLER_OUTAGE_MS = 600_000` and uses it in
`parseControllerOutage` and its error message.
`validControllerOutageReceipt` is unchanged and still requires readiness
inside the 7-minute grace.
- `tests/runner-e2e/controller-outage.test.ts`: the rejection cases now
use 600001. A new test accepts 360000 (SIGKILL) and 600000 (SIGTERM).
The existing five-minute contract test is unchanged.

## Verification

- `vitest run tests/runner-e2e/controller-outage.test.ts
tests/runner-e2e/server-restart-request.test.ts`: 52/52 pass.
- `server-restart-request.test.ts` now uses 600001 as its out-of-range
`controllerOutage`. The separate restart-request `controllerDowntimeMs`
limit (five minutes) is unchanged.

## Risks

- This change is test-only. No product or server code changes.
- A suite that sets `PAPERCLIP_RUNNER_E2E_CONTROLLER_OUTAGE_MS` above
300000 now runs instead of being refused. The recover-controller receipt
check still fails any receipt whose readiness is outside the grace.

> 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, in Claude Code with
tool use (shell, file edits, GitHub CLI). Default context window,
extended thinking.

## 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>
canary/v2026.1107.0-canary.9
2026-11-07 03:11:10 -06:00
DottaandClaude Opus 5.5 35d41f11bb fix(runner): keep Codex thread usage reported before a run's first turn as history (#19783)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A new Codex run can attach to a Codex thread that earlier runs used
> - In restart storm 9, a comment-wake run attached to that thread after
a restart. Before its first turn, it received the thread's usage from
the earlier run
> - The runnerd transport gave that usage an empty turn owner. The
driver rejected it as an unowned authoritative notification and failed
the run
> - The task then stopped with `native_event_replay_conflict` and needed
the next run to finish
> - This pull request keeps usage that arrives before a run's first turn
as history
> - The benefit is that a run is not lost to earlier work's usage, and
it is not billed for that usage

## Linked Issues or Issue Description

Refs #19718, #19722

**What happened?**

Restart storm 9 (seed 7111), task CHA-5 (Codex). Run A succeeded. A K2
restart fired. The comment-wake run B then started on a fresh runnerd
and attached to the same durable Codex session. Before run B started a
turn, runnerd committed a `usage.reported` event for the thread's
earlier usage. The transport rehydrated it as
`thread/tokenUsage/updated` with `turnId: ""`, because no turn was bound
yet. `classifyCodexNotification` classifies an empty id as
`invalid_authority`. The driver failed the run with "Provider
authoritative notification thread/tokenUsage/updated did not name its
execution owner." Recovery then stopped with
`native_event_replay_conflict`.

**Expected behavior**

Usage that arrives before a run's first turn does not fail the run. If
the session has no usage baseline yet, that usage is history, so the run
is not billed for it.

**Steps to reproduce**

1. Use fake Codex with `--usage-before-first-turn`. It reports thread
usage right after `thread/start`.
2. Open a driver session over the real runnerd transport. Wait until
runnerd commits the usage, then start a turn.
3. Before this change, the run fails with "did not name its execution
owner".

## What Changed

- `runnerd-codex-transport.ts` (`rehydrateRunnerdUsageNotification`):
the transport omits `turnId` when no turn is bound. It no longer sends
an empty owner.
- `codex-session-notifications.ts`: usage reported before the session's
first turn, with no baseline yet, sets the baseline (history). Usage
during or after a turn keeps its existing accounting.
- Fake Codex: new `--usage-before-first-turn` flag.
- New test `src/live/runnerd-codex-pre-turn-usage.test.ts` with real
runnerd, the real Codex driver and fake Codex.

## Verification

- Red, run twice before the fix: the test failed with
`NativeProviderTerminalFailure: Provider authoritative notification
thread/tokenUsage/updated did not name its execution owner.` This is the
live message.
- Green, run three times after the fix: the turn completes. The run's
usage `runDelta` is 7/2 (thread total 12/3 minus the 5/1 reported before
the turn).
- `npx vitest run src/drivers/codex src/live/runnerd-codex-transport
src/native-session-runtime`: 991 of 992 tests pass. One settlement test
timed out under parallel load. It passes alone (19/19), and this change
does not touch it.
- `pnpm run typecheck` in `packages/paperclip-runner` passes. `cargo
fmt` is clean.
- Not run: the live restart-chaos replay. It needs provider credentials,
which this session does not load.

## Risks

- Low risk. The accounting change applies only when no turn is active,
no turn is pending, the session has no settled turns and there is no
baseline. In that state, the usage cannot belong to this run's work.
- The test reproduces the failing shape (usage committed before the
first turn) with a fresh session. It does not reproduce the exact
restart and run-attach sequence from the storm.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code with tool use (shell, file editing). Default context window and
reasoning mode.

## Checklist

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

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

---------

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

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

## Linked Issues or Issue Description

Refs #19666

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

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

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

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

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 02:17:24 -06:00
af0fe8fe81 Keep personal MCP connections functional while idle tenants sleep (#19427)
## Thinking Path

> - Paperclip manages work for AI agents.
> - Personal MCP connections should remain enabled while an idle tenant
sleeps.
> - Unfinished OAuth, transfer and delivery work must retain its owner
and deadline.
> - Cloud can receive bounded metadata and incoming work while the
tenant remains the company-scoped authorizer.
> - Current identity, revision and delivery acknowledgments establish
when ownership has transferred.
> - This change preserves useful wake, retries and revocation while
making settled ownership inspectable without writes.

## Linked Issues or Issue Description

Refs: #19377 and #19750. Hosted companion:
[paperclip-cloud#1217](https://github.com/paperclipai/paperclip-cloud/pull/1217).

**What happened?** Enabled personal MCP grants and subscriptions could
keep an otherwise idle tenant awake. Ignoring those records would lose
discovery, authorization, delivery and revocation obligations.

**Expected behavior** Unconsumed authorization codes, device requests,
file handoffs and delivery work retain their original deadlines. Settled
connections remain enabled and become quiet after the current external
owner acknowledges responsibility. Incoming useful work resumes normal
authorization and delivery; revocation still takes effect for the
current company.

**Steps to reproduce** Complete normal PKCE/device consent and company
selection, synchronize client/device and revocation state, retain the
connection while idle, then exercise useful tools/events and revocation.
Sleep eligibility requires current incoming/deadline ownership
throughout.

## What Changed

- Add bounded signed client/device metadata and revocation
synchronization with exact revision, identity and ACK checks, startup
recovery and finite legacy-device cutover.
- Preserve company selection, authorization and operation identity.
Register grant work before its INSERT in the same transaction; retain
rollback, expiry and retry behavior.
- Require current managed identity/resource before hosted publication.
Receipt-cleared re-enable must obtain a fresh source ACK before device
creation.
- Reuse read-only settings and inspection budgets; connect MCP owners to
normal startup, drain and shutdown.
- Bound metadata pages and the actual signed request body while
preserving the default limits of other operations.
- Generate migration 0440 after the merged Legacy migration. Preserve
all 438 existing executable migrations and the journal prefix; retain
the normal five newest snapshots.
- Preserve the merged transaction-owner forwarding and regression
coverage from #19750 through normal ancestry integration.

## Verification

- Current own Shared/SDK, database, eval-kernel and Runner builds
passed, together with all seven normal application TypeScript programs.
Missing generated package exports/workspace links were restored through
normal setup; the original setup failures were retained. No source,
compiler option, test assertion or timeout was weakened.
- Normal migration generation changed only the five intended MCP tables;
its SQL is byte-identical to the reviewed MCP delta. Fresh reference
databases and both retained master/Muse histories upgraded through 0440,
preserving historical hashes, row fields, schema and idempotence. Direct
assertions cover the Legacy null default and MCP null/zero defaults. Two
selected history cases passed; the other 21 database cases were
unselected.
- All six current MCP lifecycle cases passed with zero skips: normal
PKCE approval intent, ACK recovery after outage, mutation during
publication, disable/re-enable with original expiry, company-deletion
rollback/commit and key rotation.
- All 81 checks in five current whole files passed with zero skips:
OpenAPI 34, read-only settings 2, diagnostic catalog 25, PostgreSQL
diagnostics 7 and dependency scheduling 13. PostgreSQL fixtures ran
normal migrations before test imports.
- Earlier canonical proof at App `cd1c8c6` / Cloud `9f67531` includes
real 60-second rate and three 15-second quiet windows, useful work and
three revokes. Later paired proof at App `43af514` / Cloud `914eb0f`
plus its reviewed candidate includes normal DCR, PostgreSQL metadata, a
144,017-byte signed publication, actual ACK, cap/default-413 controls
and a real 15-second quiet window. The relevant producer/signing/wire
inputs remain byte-identical in this integration; shared changes are
qualified above. These remain source-scoped observations, not a fresh
full canonical run on the integrated tree.
- All known test owners, process groups and OS sockets closed; an
independent final PostgreSQL audit found no remaining test sessions. One
dependency-test trace recorded 46 opens and 45 close events. Its cause
is unknown; this is not claimed as proof of graceful closure.
- Fresh exact-head remote checks passed: 59 successful and 4 skipped
contexts, with a complete current 5/5 Greptile review and all seven
review threads resolved. Targeted local checks do not constitute a new
whole-repository test run.

## Risks

App and Cloud must agree on identity, revision, namespace, deadlines and
revocation. Partial rollout, unavailable authority or missing ACK must
retain awake debt. Shared startup/shutdown and settings changed since
the earlier canonical profile, so historical observations carry only
within their stated source scope. No deployed Railway/Neon suspension,
Cloud alarm, savings or complete hosted Dot/Muse acceptance is claimed.

## Model Used

OpenAI `gpt-6.1-sol` produced code and verification, with a Codex
coordinator reviewing and integrating the changes. The coordinator's
exact runtime model ID and context-window sizes were not exposed.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Codex <noreply@openai.com>
2026-11-07 02:12:48 -06:00
DottaandPaperclip 909f4d1a33 fix(native): recover exact historical provider pack code (#19779)
## Thinking Path

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

## Linked Issues or Issue Description

Refs #19558, #19622, #19755.

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

---------

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

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

## Linked Issues or Issue Description

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-07 01:59:09 -06:00
DottaandClaude Opus 5.5 ee1e1a3994 fix(runner): adopt the turn a dying controller's turn.start already started (#19772)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A remote Runner hosts the provider (here Claude through ACPX) and
keeps working while the control plane restarts
> - A controller can die after the Runner accepts its `turn.start` but
before the controller checkpoints the accepted turn or emits
`turn.accepted`
> - The provider keeps running that turn. On reattach, recovery saw no
checkpointed active turn, and the Runner's replayed `turn/started`
failed the session with "Provider started an unexpected turn"
> - The run then failed with `native_event_replay_conflict`, replay was
blocked, and a finished issue moved from done to blocked
> - This pull request lets recovery adopt that turn when the Runner's
authenticated command result proves it is the controller's own turn
> - The benefit is that a controller crash loop no longer fails a run
that the provider is still doing correctly, and a foreign turn still
fails closed

## Linked Issues or Issue Description

**What happened?**
In a restart chaos run (Claude through ACPX on a remote Runner, crash
loop fault), a follow-up comment woke a new run. The controller sent
`turn.start` and the crash loop began about one second later. After the
restarts, the controller reattached to the same Runner and failed with
"Provider started an unexpected turn". The run failed with
`native_event_replay_conflict` and recovery required workspace repair.

**What did you expect?**
The new controller generation adopts the turn that the old generation
started, waits for its terminal, and finishes the run.

**Evidence**
The durable controller journal for the run shows command 4, `turn.start`
with `turnId: turn_lab_18ac…`, completed with `{ providerTurnId:
turn_lab_18ac…, status: accepted }`. The Runner emitted `turn.started`
for that turn 1.4 s after the command, after the old controller died.
The replacement's `session.snapshot` returned `activeProviderTurnId:
turn_lab_18ac…`. The controller's own checkpoint had no active turn and
the run had no `turn.accepted` event.

Related: #19711, #19691, #19718

## What Changed

- `codex-app-server-driver-impl.ts` `recoverSession`: when the
checkpoint has no active turn, no semantic result, and no autonomous
goal or disposition turn applies, adopt the single in-progress provider
turn if the transport proves that this run's latest completed
`turn.start` accepted that exact turn id. Any other active turn is not
adopted and still fails closed.
- `codex-session-state.ts`: a recovered session with an adopted turn
emits the missing `turn.accepted` before any of that turn's
notifications.
- `app-server-transport.ts`: new optional transport method
`controllerAcceptedTurnId()`.
- `runnerd-codex-transport.ts`: implement it from the durable command
journal (`controllerAcceptedTurnStartId`). The latest `turn.start` must
be completed and accepted, its result must name the requested turn id,
and no run attachment may come after it.
- `DURABLE_TRANSPORT.md`: document the adoption rule.

## Verification

- Red/green with the real `paperclip-runnerd` and the scripted ACPX
Claude sidecar: `npx vitest run
src/control-plane/acpx-controller-recovery.test.ts -t RC-59`. The old
controller sends `turn.start` and stops before the provider accepts it.
The provider accepts and starts the turn while no controller is
attached. A replacement controller adopts the live Runner through the
production transport and `CodexAppServerDriver.recoverSession`. Before
the fix: `activeTurnId` is null (the session then fails on
`turn/started`). After the fix: events are `turn.accepted`,
`turn.started`, `turn.completed` for the same turn, with one
`turn.start` to the provider.
- Driver tests: `npx vitest run
src/drivers/codex/codex-app-server-driver.recovery.test.ts -t
"controller that died"`. Adoption passes. With no accepted result, or a
result for a different turn, `startTurn` still fails with "Provider
started an unexpected turn."
- Journal unit tests: `npx vitest run
src/live/runnerd-codex-transport.test.ts -t
controllerAcceptedTurnStartId` (pending, failed, rejected, mismatched
turn, later attachment, later pending start all return null).
- Regression: `src/drivers/codex` (all files),
`src/live/runnerd-restart-continuation.test.ts`,
`src/live/runnerd-codex-transport-adoption.test.ts`,
`src/live/runnerd-codex-transport-release-resume.test.ts`,
`src/control-plane/acpx-controller-recovery.test.ts` pass. `pnpm run
typecheck:typescript` passes in `packages/paperclip-runner`.
- `pnpm verify` asks for broader coverage (exit 2). Required CI supplies
it.

## Risks

- Low to medium. The change only acts when the checkpoint has no active
turn and the Runner reports exactly one active turn. Adoption needs the
Runner's authenticated, run-scoped `turn.start` result for that exact
id. Without that proof, behavior does not change.
- If a `turn.start` result is still pending when the replacement reads
the thread, recovery does not adopt and fails closed as before.
- Not proven on Daytona. I did not run the paid replay.

> 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 runs).

## Checklist

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

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

---------

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

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

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

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

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-11-06 23:45:35 -08:00
DottaandClaude Opus 5.5 7475577cdb fix(daytona): keep a runner sandbox alive through the controller reconnect grace (#19754)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A remote agent run executes in a Daytona sandbox and keeps working
while the Paperclip server restarts
> - The server reattaches that runner within a 7-minute reconnect grace
> - Daytona stops an idle sandbox after its `autoStopInterval`, and it
counts only SDK and lifecycle calls as activity
> - With the server down, nothing touches the sandbox, so a 5-minute
auto-stop kills the runner before the server returns
> - This pull request makes the host tell the provider its reconnect
grace, so Daytona never auto-stops a runner sandbox inside that grace
> - The benefit is that a controller outage of up to 7 minutes no longer
turns a healthy remote run into a blocked task

## Linked Issues or Issue Description

Refs #19199 (the same floor for the warm runner idle window). Refs
#19741 (follow-up delivery through restarts; no overlap).

**What happened?**
A restart-chaos wave on Daytona killed the server for 4m51s while a
per-turn remote run was active. The environment used `autoStopInterval:
5`. The last sandbox activity was at 06:09:16. The server came back at
06:14:19 and then crashed again. At 06:15:21 recovery called the
sandbox. The plugin found it not started and started it again. The
runner identity marker was still on disk, but its process was gone:
`native.remote_runner_recovery_failed {stage: process_verification,
reason: process_not_running}`, then
`runner_remote_process_identity_unavailable`. The task went from done to
blocked. An earlier wave showed the same failure after a 4m45s outage.

**Expected behavior**
A sandbox that holds a live remote runner survives any controller outage
up to the reconnect grace. The server reattaches the runner and the run
continues.

**Steps to reproduce**
1. Create a Daytona environment with `autoStopInterval: 5` and a
`paperclip_runner` agent.
2. Start a remote run and kill the server for more than 5 minutes (less
than 7).
3. Restart the server. Recovery reports `process_not_running` and the
task becomes blocked.

## What Changed

- `server`: add `resolveRunnerReconnectGraceMsForRun`. It returns
`REMOTE_RUNNER_RECONNECT_GRACE_MS` for `paperclip_runner` on a sandbox
environment.
- `server`: forward `runnerReconnectGraceMs` to the provider worker on
lease acquisition and on the recovery reattach resume. It uses the same
provider-only path as `warmRunnerIdleTimeoutMs`. It is not lease
identity, lease metadata, or saved environment config.
- Daytona plugin: the effective auto-stop is at least the largest host
floor (warm idle window or reconnect grace) plus 5 minutes. With the
current grace this is 12 minutes. The plugin applies it at creation and
raises an older sandbox's shorter interval on resume (bounded, best
effort).
- Daytona plugin: keep the forwarded grace out of the normalized config.
Update the `autoStopInterval` manifest description.
- `doc/execution-semantics.md`: document the reconnect-grace floor next
to the warm floor.

## Verification

- Red/green Daytona plugin tests: `cd
packages/plugins/sandbox-providers/daytona && npx vitest run --config
vitest.config.ts`. The new cases failed before the fix (for example
`expected 120000 to be >= 720000`, and no `setAutostopInterval` call on
resume). All 605 tests pass after the fix.
- Server tests: `cd server && npx vitest run
src/__tests__/environment-run-orchestrator.test.ts
src/__tests__/environment-runtime.test.ts`. 247 pass. They prove
acquisition forwards the grace only for `paperclip_runner` on a sandbox,
that the reattach resume forwards it, and that it does not reach lease
metadata or saved config.
- `pnpm typecheck` for the plugin and `tsc --noEmit` for the server are
clean.
- `pnpm verify` asks for broad CI coverage because a doc changed.
Required CI supplies it.
- Evidence from the kept wave database: lease metadata shows
`autoStopInterval: 5`, `reuseLease: false`, `runnerLifecycleMode:
per_turn`. The run had no events between 06:09:16 and 06:14:19.

## Risks

- Cost: a runner sandbox that leaks (no cleanup) now auto-stops after 12
minutes instead of a shorter configured value. Cleanup still stops or
destroys released sandboxes. Explicit `0` and longer values are
unchanged. Auto-archive and auto-delete are unchanged.
- I did not inspect the sandbox state in Daytona directly. Cleanup
destroyed it before this investigation. The proof is the timing, the
lease config, and the `process_not_running` result after a restart that
kept the disk.
- No live Daytona replay ran.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 01:36:18 -06:00
DottaandPaperclip e2c7d81067 fix(server): keep reply recovery off web startup critical path (#19769)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The server restores retained native replies during startup.
> - Fair reply recovery can admit more due work while an earlier group
remains active.
> - Waiting for that whole pass can hold web readiness on unrelated
retained replies.
> - This pull request tracks reply recovery with the existing background
startup owner.
> - The server can become ready while idle and shutdown still account
for that work.

## Linked Issues or Issue Description

Refs: #19726.

**What happened?**

The startup path awaited the native response worker's first pass. That
pass can remain active while fair dispatch admits later replies. A
controlled `startServer` regression stays in recovery until the held
reply pass finishes. A deployed backlog supported investigating this
boundary, but no captured runtime stack proves it caused the observed
startup delay.

**Expected behavior**

Retained reply recovery must keep its authority checks, retry deadlines,
and fair dispatch. Its completion must not be a condition for unrelated
web readiness. Active recovery must still block idle and be joined at
shutdown.

**Steps to reproduce**

Run the startup regression with a held native response worker promise.
Before this change, `startServer` does not complete. After this change,
startup reports ready, idle work remains active, and shutdown waits
until the promise settles.

## What Changed

- Track the initial native response pass with the existing heartbeat
scheduler work owner.
- Extend the actual startup wiring test to cover readiness, idle
accounting, worker stop, and shutdown join.
- Document the response recovery boundary for idle and readiness.

## Verification

- Mandatory agent preflight passed on macOS arm64 with Node 26.4.0.
- True RED: the new startup case failed because `startServer` had not
completed while reply work was held. The same case passed after the
change.
- Final head `279e97587b` passed the full startup test file: 123/123
tests.
- The unchanged real PostgreSQL later-page case passed. It holds 100 old
publication rows, publishes the independent 101st reply once while the
first pass remains pending, then verifies completion and no recurring
idle SQL.
- Final-head CI passed: 60 successful checks and 3 skipped checks,
including `ci / verify`. Fresh Greptile is 5/5 with no open threads.
- Server TypeScript check passed: `node node_modules/typescript/bin/tsc
-p server/tsconfig.json --noEmit`.
- Initial test-launch and missing generated SDK setup failures were
resolved before the regression ran. They are not RED or pass evidence.
- No deployment or live recovery action was performed for this PR.

Focused commands, from `server/`:

```sh
node ../node_modules/vitest/vitest.mjs run src/__tests__/server-startup-feedback-export.test.ts --maxWorkers=1 --no-file-parallelism
node ../cli/node_modules/tsx/dist/cli.mjs ../packages/db/src/test-postgres-command.ts -- node ../node_modules/vitest/vitest.mjs run src/services/native-runtime/native-response-publication-retry.test.ts -t 'publishes a later-page reply while older publication groups remain contended' --maxWorkers=1 --no-file-parallelism --hookTimeout 90000
```

## Risks

Web readiness can complete before retained reply recovery finishes. The
existing scheduler still owns errors, retries, scope checks, and
delivery authority. The existing idle tracker and shutdown join retain
in-flight work. Other startup recovery gates stay in place.

## Model Used

OpenAI Codex, `gpt-6.1-sol`, with reasoning, tool use, and code
execution. The context window size is not exposed.

## Checklist

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

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-07 01:24:31 -06:00
Nicky LeachandPaperclip beb3e55072 Continue a released Runner after confirmed graceful eviction (#19760)
## Thinking Path

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

## Linked Issues or Issue Description

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

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 23:21:45 -08:00
github-actions[bot]andlockfile-bot e95dcadbd2 chore(lockfile): refresh pnpm lockfiles (#19773)
## Thinking Path

> - Paperclip uses a dependency lock for reproducible builds.
> - Dependency changes on master need matching lock entries.
> - Paid eval reports also pin a reviewed offline correction.
> - Reports must install before paid model work starts.
> - This automated PR keeps the lock and report proof together.

## Linked Issues or Issue Description

**What happened?**

Dependency manifests on master changed after the last committed lock
refresh.

**Expected behavior**

Frozen installs must match those manifests. Eval reports must retain
matching offline lock proof.

**Steps to reproduce**

Run a frozen install before and after this lock refresh.

## What Changed

- Refresh the dependency lock from the manifests on master.
- Refresh the sandbox provider lockfiles from their manifests on master.
- Include the matching offline report correction and hashes.

## Verification

- Pinned pnpm resolution completed with lifecycle scripts disabled.
- The report proof refresh accepted the reviewed committed baseline.
- The workflow rejected changes outside the lock and report proof files.

## Risks

The lockfile bot merges this PR without CI or review when every added
package version is at least 3 days old on npm. Otherwise the PR waits
for a maintainer, and the daily refresh tries again. The bot merges only
the exact commit this workflow generated from manifests already reviewed
on master. Unknown report lock baselines remain blocked. This update
changes no permissions or paid enable gates.

## Model Used

None — generated by pnpm and the deterministic report-lock updater.

## Checklist

- [x] Thinking path and problem description included
- [x] Generated lock and matching report proof included
- [x] Release-age gate decides between bot merge and maintainer review

Co-authored-by: lockfile-bot <lockfile-bot@users.noreply.github.com>
canary/v2026.1107.0-canary.8 nightly/v2026.1107.0-nightly.0
2026-11-07 07:14:22 +00:00
Nicky Leach 8be6d70538 fix(runner): select Claude effort again after a cold session resume (#19708) 2026-11-06 23:10:51 -08:00
DottaandClaude Opus 5.5 3895195f4b fix(native): report a retained provider pack the controller does not ship as incompatible (#19755)
## Thinking Path

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

## Linked Issues or Issue Description

Refs #19558, #19743

**What happened?**

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

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

**Expected behavior**

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

**Steps to reproduce**

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic. Agentic tool use with
shell, code editing, test execution and read-only authenticated browser
diagnostics.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 01:10:29 -06:00
Nicky Leach 167e39c4ef fix(runner): keep warm run.attach events on ordinary accounting (#19707) 2026-11-06 23:09:57 -08:00
DottaandClaude Opus 5.5 b03ea621c6 fix(environments): retry a teardown interrupted by server death at once (#19749)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - After a remote run, Paperclip destroys its provider sandbox. The
teardown first claims the lease with a 15-minute ownership fence, so no
other cleanup runs at the same time
> - #19696 made restart recovery end a fence when the claiming server
process is proven to have exited. It only does that for claims that
record the claimer
> - The run-release teardown and the stop-only teardown did not record
the claimer
> - When the server died during a run-release teardown, the sandbox
therefore stayed up until the 15-minute fence expired
> - This pull request records the claimer on those claims, and stops a
teardown attempt lost to a dead server from counting as a failed attempt
> - The benefit is that a restart during sandbox teardown destroys the
sandbox at the next startup sweep

## Linked Issues or Issue Description

**What happened?**
In a Daytona restart chaos wave, a run finished at 05:34:48. Its release
claimed the lease and called the provider, and a SIGKILL stopped the
server 2 seconds later. After restart the lease stayed `pending_cleanup`
with an in-flight claim. The sandbox was destroyed at 05:49:49, when the
15-minute claim fence expired. There was no provider error.

**Expected behavior**
A teardown interrupted by the server's death is retried by the next
startup sweep. It is not counted as a failed attempt. A real provider
error keeps the normal backoff.

**Steps to reproduce**
1. Release a run's sandbox lease with destroy, and hold the provider
call.
2. Stop the server during the provider call.
3. Restart. On master, the sweep skips the lease until the fence expires
15 minutes after the claim.

Refs #19696.

## What Changed

- `environment-runtime.ts`: the run-release teardown claim
(`destroyClaimedSandboxLease`) and the stop-only teardown claim save
`pendingCleanupController`, bound to the claim's attempt id, the same
way sweep claims do since #19696.
- `heartbeat/recovery.ts`: sweep claims mark that they counted an
attempt. When the sweep ends the fence of a lost claim, it gives that
attempt back, so a teardown lost to a dead server does not move toward
the cap or the long cooldown. Claims from other paths did not count an
attempt, so nothing changes for them.
- Workspace-repair and export-retry fences are unchanged. They protect
workspace work, not teardown.

## Verification

- Red, then green:
- `environment-runtime.test.ts` → "retries a run-release teardown at the
next sweep after the releasing server dies". The provider call is held
during `releaseRunLeases(..., "destroy")`, the claim is marked as from
an exited server, and a restarted sweep runs. On master the claim has no
controller record and the sweep skips the lease. With the fix, the sweep
destroys it at once and records attempt 1.
- `heartbeat-pending-cleanup-sweep.test.ts` → "does not count a sweep
attempt lost to a dead server as a failed attempt". On master the retry
is recorded as attempt 4; with the fix it is attempt 3 again.
- `environment-runtime`, `heartbeat-pending-cleanup-sweep`,
`sandbox-creation-operations` and `environment-routes` pass (367 tests).
`tsc --noEmit` for `server` passes.

## Risks

- The proof that a claimer exited is the same-PID-space proof from
#19696. A live, foreign or unidentified claimer keeps its fence.
- Giving back a counted attempt applies only after that proof. A
teardown that fails with a provider error still counts and backs off.
- No schema change. The new fields live in existing JSON metadata.

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

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with
tool use (shell, file editing, test execution). Extended reasoning
enabled.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 01:08:11 -06:00
DottaandPaperclip 33ca62f776 Keep external-agent feature reads on their admission transaction (#19750)
## Thinking Path

> - Paperclip manages work for AI agents.
> - External native agents must pass current feature and authority
checks.
> - The broker checks feature settings inside its admission transaction.
> - The old feature reader uses the root database pool.
> - Two active transactions can leave no connection for that read.
> - This change uses the admitted transaction for the same policy read.
> - It keeps the existing company, principal and controller checks.

## Linked Issues or Issue Description

Refs: #19340. That merged change fixes retained native Retry ownership.
This change leaves that recovery contract intact. Related: #18744 and
#17833 fix caller-owned transaction reads in other domains. Root
searched live GitHub for open externalBotEnabled changes and found no
duplicate.

**What happened?**

An external-agent admission can wait for another pool connection while
it holds a row lock. A second transaction can occupy the remaining
connection and wait for that lock. The feature read must use the
transaction that already owns admission.

**Expected behavior**

Dot, Grok and Muse read current feature settings on the admitted
transaction. Disabled providers remain disabled. Current authority
checks remain in force.

**Steps to reproduce**

1. Use a real PostgreSQL pool with two connections.
2. Lock the settings row in the admission transaction.
3. Occupy the second connection with a transaction that waits for that
row.
4. Read the existing provider feature policy through the admitted owner.
5. Require the read to finish before the competing lock is acquired.

**Paperclip version or commit**

The fix was tested at `d8dece250b75cffee67d3b7f4f0e669826282164`, based
on master `839d41aabbc6d042250548aedcaa5c50d369dca3`. It now includes
master `131b566ca4fa866c4348739c75cfe48c805b3398` through normal merge
`c2031b0fb3e5d1b34ebff1d5af8cc050d129bd9a`. All seven fix files retain
their tested blobs and modes.

## What Changed

- Pass the admitted select owner through the broker identity contract.
- Use that owner for Dot, Grok and Muse feature settings.
- Add a read-only settings projection that reuses the existing managed
and staging view. It does not create the settings row.
- Add six real PostgreSQL controls for enabled and disabled providers.
- Add three broker-path PostgreSQL regressions for exact transaction
forwarding through Dot, Grok and Muse.
- Keep all newer master broker changes and existing authority checks.

## Verification

Local controlled regression passed at tested revision
`d8dece250b75cffee67d3b7f4f0e669826282164`:

- Baseline: restore only the five original master production leaves.
Keep the same read-only settings projection and regression file. One
enabled-Dot case failed with the intended `57014`; it still read the
correct `true` policy. Five cases were unselected.
- The trace held the admission lock at 2.441 ms. The second connection
waited for that lock at 9.494 ms. The root-pool feature read finished at
2015.585 ms, after the competing statement timed out at 2014.982 ms.
- Candidate: all six enabled/disabled Dot, Grok and Muse controls
passed, with zero skips, in 14.528 s. Each read returned the configured
policy before the competing lock was acquired. Every competing
transaction committed without an error.
- Fresh normal candidate production declarations passed in 17.080 s. The
complete 615-root owning test program passed in 4.756 s. Own dependency
setup and TypeScript export builds had passed before these controls.
- Known worker groups and sockets closed. The single post-owner audit
found no other sessions in the test database. No forensic database was
changed.

An earlier selector attempt skipped all six cases and opened no test
database connection. It is not a regression pass or failure. Source
inspection proved that Vitest 5 filters names with ` > ` separators,
while its JSON reporter joins names with spaces. The selected control
then used the unique title-only pattern; its separate baseline failure
is preserved above.

The later normal master merge preserves all seven tested files. The 46
incoming paths change Native/workspace recovery, types and presentation.
They do not change the direct feature/settings read graph. The
direct-policy evidence therefore carries from its tested revision. It
was **not rerun at the merged revision**. Fresh merged-head checks at
`c2031b0fb3e5d1b34ebff1d5af8cc050d129bd9a` passed: the content-aware
Shared/SDK producer (1.325 s), normal Runner core and surfaces
TypeScript programs (0.936 s and 0.693 s), and the full normal App graph
(28.933 s). The App check included all production layers and all four
test programs: 616 normal test roots and 397/409/412 tree roots. Known
build owners and sockets closed. Database/Eval and provider asset
outputs remained unchanged. No install, Cargo, binary build, provider
execution or PG test repeated. Fresh final-head remote CI and review
remain **pending**.

The test entry is
`server/src/modules/external-agents/feature-reader-owner.postgres.test.ts`.
It requires a dedicated external
`PAPERCLIP_FEATURE_OWNER_TEST_DATABASE_URL` ending in `_test` and an
owned `PAPERCLIP_FEATURE_OWNER_TRACE` path. Its normal migration hook,
two-connection pool, case deadlines and assertions remain unchanged. No
provider or Runner binary is needed for these controls.

The review also identified a wiring coverage gap: the six contention
controls call the readers directly. A new whole-three file now traverses
the actual Dot/Grok/Muse broker factories, normal pending-pairing
producers, dispatch and real execution-admission transaction.
Pass-through observations compare the exact admission transaction with
the owner supplied to the normal readonly settings projection. Each
provider is disabled through normal settings and must return real 403,
read actual false policy, leave the queued run without Native authority,
and create no assignment. No grant, ready binding, policy result or
successful authorization is mocked.

With only the original five master production leaves restored, all three
new cases failed at the exact missing-owner assertion (`readOwners`
expected length 1, actual 0) after passing the real 403 and
admission-scope checks. The full baseline had0 pass / 3 fail / 0 skip in
12.691 s. With the exact candidate leaves restored, the unchanged
whole-three file passed 3/3 with zero skips in 10.101 s. Both variants
used actual Node 24 / Vitest 5 forks and a real PG pool of two; all
known owners/groups/sockets closed. The candidate's private temporary
directory was removed; the failed baseline database/home remain
retained. Root independently read all three raw baseline failures,
rehashed source and outputs, verified known owner/group closure, then
observed no remaining test sessions. The successful database was
normally removed; the original failing database and home remain
retained.

The new test entry is
`server/src/modules/external-agents/broker-feature-owner.postgres.test.ts`,
using dedicated `PAPERCLIP_BROKER_FEATURE_OWNER_TEST_DATABASE_URL`. The
complete normal 617-root owning test program passed in 4.883 s after a
normal exported config refresh; prior production declarations and
unrelated configs remained byte-identical. No compiler or
dependency/Rust work repeated for the PG variants. These results were
obtained with the seven production/source files at c203 plus the new
test; they are not claimed as a new published-head remote CI result. The
review repair is committed at
`d043860eb93a26c4416f184697a212f027d538e2`; it adds only this 117-line
test file to `c2031b0f`. Tracked production bytes are unchanged. Exact
published head `d043860eb93a26c4416f184697a212f027d538e2` now passes 56
checks with four expected skips. Greptile reviewed this exact head at
5/5 and confirms that the broker-test gap is addressed. The complete
review thread is resolved. Ponytail stopped without a verdict; one
normal retry was requested and no active turn or new finding was
observed. No earlier-head result is used as a new-head pass.

The controls prove feature policy and transaction ownership. They do not
prove deployed suspension, hosted Dot/Muse eligibility or full Native
task execution. The separate private useful-case pass does not supply
missing current-master or accounting evidence.

## Risks

The broker identity contract changes for all three providers. A missed
adapter could use the wrong owner. Current semantic, real-policy and
exact broker-owner identity tests cover that error.

The read-only projection reuses the normal settings view. It preserves
managed and staging rules. An absent row is projected without a write;
the existing initializing getter stays unchanged.

No schema, public API, queue, timer, rate limit or provider deadline
changes. No historical Retry or physical-stop proof is changed.

## Model Used

OpenAI `gpt-6.1-sol` assisted with source changes and tool use. An
OpenAI GPT-6 family coordinator reviewed the work; its exact runtime ID
was not exposed. Context-window sizes were unavailable.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-07 01:02:03 -06:00
DottaandClaude Opus 5.5 6d112c74cf fix(native): do not charge a restart resume that never started the provider (#19747)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs have an execution recovery budget of three provider
attempts, so a run that keeps failing stops retrying
> - #19720 stopped charging a crash restart when the previous restart
resume made progress
> - In a crash loop (several kills within seconds), the controller can
die again before the resumed execution starts its runner
> - The next claim then charged that resume, although it used no
provider work
> - This pull request does not charge a resume that never started; the
next claim replaces it
> - The benefit is that fast restart bursts do not exhaust a healthy
run's budget, and the generation cap still bounds real loops

## Linked Issues or Issue Description

Refs #19720, #19625

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

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

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

## What Changed

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

## Verification

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

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

## Risks

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

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 01:00:34 -06:00
DottaandPaperclip f673bd3cb5 fix(native): verify legacy harness filesystem independence (#19748)
## Thinking Path

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

## Linked Issues or Issue Description

Refs #19715, #19729, #19733.

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

## What Changed

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

## Verification

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

Focused command from `server/`:

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

## Risks

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

## Model Used

OpenAI GPT-6 through Codex, with reasoning, repository tools, and local
code/test execution. The exact runtime variant and context-window limit
are not exposed in this session.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-07 00:57:20 -06:00
96bc021edb fix(server): certify settled legacy processes for future retries (#19377)
## Thinking Path

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

## Linked Issues or Issue Description

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

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

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

## What Changed

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

## Verification

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

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

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

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

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

## Risks

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

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

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

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

## Model Used

OpenAI `gpt-6.1-sol` assisted with readers, fixtures, source review and
verification through Codex. A coordinating GPT-6-family agent
implemented and reviewed shared lifecycle wiring and integration. Tools
included Git, native TypeScript checks and controlled tests. Exact
served revisions and context limits were not exposed.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used and its available capability
details
- [x] I have checked ROADMAP.md and related work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have linked existing public work and described the issue
- [x] I have not included internal issue IDs or private links
- [x] My branch name describes the change
- [x] Required focused checks pass at their explicitly recorded
source/carry scopes above
- [x] I have added or updated meaningful tests
- [x] I have recorded the internal contract and migration requirements
- [x] I have documented risks and evidence limits
- [x] All required published exact-head Paperclip CI gates are green
- [x] Published exact-head Greptile is 5/5 with no open findings in this
focused diff; separate unchanged master observations are identified
above
- [x] I will address reviewer findings before requesting merge

---------

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

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

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

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

Refs: #19366

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <Paperclip-Paperclip@users.noreply.github.com>
2026-11-06 22:39:40 -08:00
Nicky LeachandClaude Opus 5.5 bf45f66619 docs(runner): separate the legacy-adapter paragraph in the Runner README (#19753)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The native Runner package has a README that explains its contracts
> - One section explains how a resumed run gets a fresh-conversation
handoff
> - The next text, about legacy adapters, has no blank line before it
> - Markdown renders the two texts as one paragraph, so the
legacy-adapter text looks like part of the native handoff text
> - This pull request adds the missing blank line
> - The benefit is a README that separates native and legacy behavior
clearly

## Linked Issues or Issue Description

**Issue type**
Formatting error in package documentation.

**Where is the issue?**
`packages/paperclip-runner/README.md`, the paragraph that starts "Legacy
adapters advertise `supportsToolRefreshOnResume`".

**What's wrong?**
The paragraph has no blank line before it. Markdown joins it to the
previous paragraph about the native fresh-conversation handoff.

**Suggested fix**
Add a blank line before "Legacy adapters advertise".

## What Changed

- Add one blank line in `packages/paperclip-runner/README.md` before the
legacy-adapter paragraph.

## Verification

- `git diff --stat`: one file, one line added.
- I read the changed section. The legacy-adapter text now starts its own
paragraph.
- No code changed, so no tests are necessary.

## Risks

- Low risk. This is a documentation-only change.
- The Runner build-input digest includes every file in
`packages/paperclip-runner`. This change therefore changes that digest,
and the next release builds a new Runner image. The Runner behavior does
not change.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with tool use
(shell, file 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-07 06:38:14 +00:00
Nicky Leach 08b049f381 Admit qualified Claude sources through the existing Core claim (#19196) canary/v2026.1107.0-canary.7 2026-11-06 22:25:22 -08:00
DottaandClaude Opus 5.5 e620ce3678 fix(heartbeat): deliver each accepted follow-up exactly once through restarts and admission holds (#19741)
## Thinking Path

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

## Linked Issues or Issue Description

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

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

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 00:24:09 -06:00
DottaandPaperclip 18b6b71d83 fix(native): refresh per-turn projections from the selected checkpoint (#19744)
## Thinking Path

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

## Linked Issues or Issue Description

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-07 00:14:17 -06:00
DottaandClaude Opus 5.5 24cbea136f fix(native): export OpenCode launch artifacts through a confined workspace copy (#19743)
## Thinking Path

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

## Linked Issues or Issue Description

Refs #19622

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

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

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

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

## Linked Issues or Issue Description

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

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

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

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

## What Changed

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

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

## Verification

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

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

## Risks

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 23:57:48 -06:00
DottaandClaude Opus 5.5 4dc5281f51 fix(ui): offer the owner-stop confirmation for retained saved-work recovery (#19742)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A native run saves the agent's files after its turn (copyback). One
controller owns copyback and records an owner receipt on the run
> - #19721 made retained saved-work recovery stop spending attempts when
a restart killed that owner. It records the receipt as
`workspaceOwnerRecovery.state = "operator_required"`, and a waiting
Retry reports `workspace_owner_stop_required`
> - #19717 added one task control, **Confirm the previous server
stopped**, for the direct finalization hold. That control did not cover
the retained hold, so the task told the board to confirm but offered
nothing to click
> - This pull request shows the same control for the retained hold, with
the recorded receipt token
> - The benefit is that every copyback-owner hold has exactly one
visible action, and the waiting Retry then continues by itself

## Linked Issues or Issue Description

Refs #19721 (retained hold, server side), #19717 (the confirmation
control).

**What happened?**
The retained recovery action named the stopped copyback owner, and the
task's Retry said "Retry is waiting for a board member to confirm that
the previous server stopped copying files". No control called the
confirmation route for this state.

**Steps to reproduce**
1. Retained saved-work recovery owns copyback for a finished run.
2. A restart kills the controller while it owns copyback, on another
host or without automatic stop proof.
3. Open the task.

**Expected behavior**
The task shows one **Confirm the previous server stopped** action to an
owner or administrator. It sends the recorded receipt token to
`resume-workspace-finalization`.

**Actual behavior**
The task shows only the retained file-recovery controls. The token is in
`evidence.workspaceOwnerRecovery.owner`, which the #19717 control did
not read.

## What Changed

- `WorkspaceExportRecovery`: when a retained export action has
`workspaceOwnerRecovery.state = "operator_required"` and no recorded
confirmation, render the existing confirmation control with that
receipt's token, in place of the retained file-recovery controls. After
the confirmation (state `resuming`), the ordinary retained controls
return.
- The confirmation control now takes the owner token as a prop, so the
direct hold (`evidence.owner`) and the retained hold
(`evidence.workspaceOwnerRecovery.owner`) share it.
- Docs: one sentence in `doc/native-workspace-finalization-recovery.md`.
- There are no server changes. The route already dispatches the retained
path (#19721).

## Verification

- Red then green: the new test "offers the same action for retained
saved-work recovery held by a stopped copyback owner" fails against
master's component and passes with this change. It checks that the
request body carries the retained receipt token, that the host name is
not shown, and that the retained controls return after `resuming`.
- `cd ui && npx vitest run src/components/WorkspaceExportRecovery` (35
passed).
- `pnpm check:token-gates` clean. UI typecheck clean.

## Risks

- Low. The change is UI only. The server rechecks the receipt, the hold
state and `runtime:confirm_owner_stop`.
- While the hold is active, the "Recover saved files" control is hidden.
It cannot release the receipt, so hiding it removes a futile action.

## Model Used

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

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 23:48:43 -06:00
131b566ca4 fix(workspaces): report safe Git restore diagnostics (#19736)
Return the restore stage and failure category with the existing HTTP409 rejection. Preserve the unsaved comment and editable draft. Keep nested saved-commit and clone-timeout diagnostics through preparation wrappers, and distinguish local Git file errors from authentication failures. Retain only safe process flags privately; no raw command cause is exposed.

All seven affected local suites pass (288 tests) and the layered server typecheck passes on supported Node25.6.1. Reviewed head 022eb7c2eb has Greptile Apex5/5, zero unresolved findings and all applicable CI passing (55 successful checks and4 intentional skips).

Co-Authored-By: Sentry Bot <sentry-bot@paperclip.invalid>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
canary/v2026.1107.0-canary.6
2026-11-06 21:33:59 -08:00
DottaandClaude Opus 5.5 464e6e16e0 fix(native): resume copyback after a restart kills its same-host owner; one board action otherwise (#19717)
## Thinking Path

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

## Linked Issues or Issue Description

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

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

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

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

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

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

---------

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

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

## Linked Issues or Issue Description

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 23:26:53 -06:00
DottaandClaude Opus 5.5 fc7e3a6503 fix(recovery): never replace a native session after a deterministic refusal (#19733)
## Thinking Path

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

## Linked Issues or Issue Description

Refs #19659, Refs #19516, Refs #19334

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

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

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

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

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

## What Changed

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

## Verification

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

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

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

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

## Risks

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 23:25:54 -06:00