Commit Graph
6388 Commits
Author SHA1 Message Date
ea6bd15e2a fix(native): record why finalization failed a run (#19063)
## Thinking Path

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

## Linked Issues or Issue Description

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

**Paperclip version or commit**

`master` at be20d4ad97.

## What Changed

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

## Verification

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

## Risks

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

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 18:07:35 -06:00
Nicky LeachandPaperclip a0c0851766 Runner: add private host control channel (#19129)
## Thinking Path

> - Paperclip coordinates AI agents and their execution.
> - A hosted Runner can remain alive between turns.
> - Its host needs to park, claim, or retire that process without a
persistent Core connection.
> - Those requests need a private channel with bounded input and safe
retries.
> - This PR adds the channel codec and socket handling as an isolated
foundation.
> - Later lifecycle changes will validate and execute the requests.

## Linked Issues or Issue Description

**Current behavior**
The Runner has no task-private host control channel. The earlier
combined lifecycle change was split for review.

**Proposed behavior**
Add a private Unix stream protocol for host lifecycle requests. This PR
does not wire it into Runner startup or advertise support. It can be
reviewed independently of stopped-process recovery.

**Reason and benefit**
Review frame parsing, credential handling, and retry behavior before
adding park and claim transitions. This replaces the transport portion
of #19060. The source-recovery work supersedes #19057 and #19058.

## What Changed

- Add `host_control.rs` and enable the existing rustix dependency's
socket APIs.
- Accept only a connected Unix stream. Set close-on-exec so provider
children do not inherit it.
- Bound request frames to 16 KiB. Reject unknown fields, malformed
requests, and EOF with a credential-free error.
- Add socket-pair tests for framing, deadlines, retries, conflicts, and
receipt-window reset in a separate test file.

### Transport contract

Each request is one newline-delimited JSON object. Each response is `{
"id": "request-id", "status": "..." }` followed by a newline.

| Request | Required binding | Intended effect in the later lifecycle
consumer |
| --- | --- | --- |
| `park` | Six Runner identity fields, last controller command sequence,
acknowledged source sequence | Retain the process and mounts after the
controller has settled that exact cut |
| `claim` | Park request ID and a fresh bootstrap ticket | Reconnect the
retained process through authenticated bootstrap |
| `retire` | Park request ID; optional claim ID and identity/sequence
cut | Retire only the matching parked or claimed process |

The six identity fields are Runner instance, environment lease, run,
normalized session, turn, and item IDs. This module only parses requests
and sends responses. The lifecycle consumer must validate these bindings
before performing an effect.

Before applying a request, the consumer checks the in-memory response
cache. An identical retry receives its prior status. Reusing a completed
request ID with different content returns `conflict`. Conflict responses
do not reserve an ID. A successful `parked` response starts a new cache
window; the consumer's identity, sequence, and park-ID checks must
reject requests from older windows. Credentials are neither persisted
nor included in errors.

Active reads are nonblocking. Parked reads block on socket input and may
have a caller-supplied receive deadline. Partial frames do not extend
that deadline. No idle polling is added.

## Verification

- `cargo test --offline --locked --manifest-path
packages/paperclip-runner/runner/Cargo.toml -p paperclip-runner-core
--lib host_control::` — 6 passed.
- Confirmed the repeated-chunk test fails when reads incorrectly reset
the original deadline. Restored production code and reran the targeted
suite.
- The initial policy gate did not recognize inline Rust tests. Moved the
same tests to the existing `src/tests/` convention.
- GitHub CI passed on `aaac996aba76`. Greptile is 5/5 on that commit,
with no unresolved review threads.
- A local merge-tree check against current master `1a7eb70a37bf` is
conflict-free.
- Reviewed changed lines for secrets, personal data, and private
deployment references before push.

## Risks

The module is dormant in this PR. It does not make a VM sleep, detach
Core, or evict a Runner. Integration must enforce the documented
identity and sequence fences before replying with success. The response
cache is process-local and does not provide recovery across process
death. Linux and Windows compilation are covered by CI; local socket
tests run on macOS.

## Model Used

OpenAI Codex with tool use and code execution. GPT 6.1 Sol with High
effort handles the parallel recovery work; this isolated channel change
is implemented by the coordinating Codex agent.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [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.1105.0-canary.0
2026-11-04 15:56:01 -08:00
DottaandPaperclip 818d2bb661 fix(native): preserve closed post-result failure diagnostics (#19126)
## Thinking Path

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

## Linked Issues or Issue Description

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

**Paperclip version or commit**

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

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

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

## Linked Issues or Issue Description

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

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

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

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

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

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 17:45:00 -06:00
DottaandClaude Opus 5.5 ac57b183f0 fix(evals): run campaign cells when TestOps selection is skipped (#19053)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The Runner Full-Stack E2E workflow runs the paid Product E2E cells,
including the Product regression TestOps plan
> - #18983 added an optional `testops_selection` job and made `catalog`
depend on it
> - That job is skipped in every ordinary manual or scheduled campaign
> - GitHub's implicit `success()` also fails when an ancestor job was
skipped, so every job below `catalog` was skipped
> - Since #18983 merged, ordinary campaigns run no cells: the merge
reports every selected cell as "No result artifact was uploaded"
> - This pull request gives each job below `catalog` an explicit status
condition
> - The benefit is that ordinary, nightly and branch-targeted campaigns
run their cells again, and TestOps campaigns are unchanged

## Linked Issues or Issue Description

Refs #18983, #19000

**What happened?**
Run https://github.com/paperclipai/paperclip/actions/runs/41914430034
selected 20 Product regression cells with the `id` input. `catalog`
passed. `daytona_image`, `build_runner_artifacts`,
`build_remote_provider_pack` and every matrix job were skipped. The
merge failed with 20 "No result artifact was uploaded" rows.

**Expected behavior**
An ordinary manual campaign runs its selected cells when
`testops_selection` is skipped, as it did before #18983.

**Steps to reproduce**
Dispatch `runner-full-stack-e2e.yml` on `master` without `testops_case`
or `ALLURE_JOB_RUN_ID`. The scheduled daily campaign has the same path.

## What Changed

- `daytona_image`, `build_runner_artifacts`,
`build_remote_provider_pack`, `test`, `test_recurring` and
`test_manual_private` now start with `!cancelled()`.
- Each of those jobs now requires `needs.<job>.result == 'success'` for
each of its real dependencies. A failed or skipped build still skips the
cells, as before. The existing schedule, cadence and evidence-route
conditions are unchanged.
- A new regression test parses the workflow. It requires every job below
`testops_selection` to use a status function and to require each real
dependency result.

## Verification

- `node --test tests/runner-e2e/product-regression-testops.test.mjs`:
15/15 pass.
- Negative control: the new test fails against the current `master`
workflow.
- `daytona-image.test.ts`, `workflow-target.test.ts` and
`workflow-timeout.test.ts` pass. `workflow-security.test.ts` has 5
failures. The same 5 fail on unchanged `master` (they concern
`pr-trusted.yml` and `chat-catalog-testops-case.yml`).
- A workflow change takes effect only from `master`, because paid
campaigns run only from the default branch. After merge, dispatch an
ordinary `id=` campaign and confirm the cells run.

## Risks

- Low risk. The conditions keep the previous failure propagation: a job
runs only when each direct dependency succeeded and the run is not
cancelled.
- `catalog` success already implies `authorize` and `target_lock`
success, so those are not repeated.

## Model Used

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

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 17:41:18 -06:00
DottaandClaude Opus 5.5 23cd123ff8 fix(e2e): retain the closed cause code of a journey cleanup failure (#19079)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The Product E2E journey harness runs the QA staging smoke plan and
reports each case to TestOps
> - Each case runs a journey, then releases its owned target resources
(cleanup)
> - Three recent cleanup failures had a passed journey and all checks
passed, but each result only says `target_cleanup_failed`
> - The adapter throws an error with a closed identifier code, and the
harness discards that code, so no one can find the failing cleanup step
> - This pull request keeps the identifier-shaped cause code as an extra
cleanup phase
> - The benefit is that the next cleanup failure is diagnosable from
TestOps without private runner files

## Linked Issues or Issue Description

**What happened?**
QA staging smoke cleanup failures show only `target_cleanup_failed`.
This occurred for `question-answer-resume` (launch 2390), and for
`company-access-isolation` and `create-skill-studio` (launch 2693). In
each case, the journey and all behavioral checks passed. The private
runner report is deleted with the runner, and the adapter error code is
not kept in the retained result. The cause is not known.

**Expected behavior**
A cleanup failure keeps a closed, identifier-shaped cause code (for
example `fixture_cleanup_not_confirmed`). The result never keeps the raw
error message.

**Steps to reproduce**
1. Run a Product E2E journey with a target session whose `release()`
throws an error that has a `code` property.
2. Read `normalized-results.json`.
3. Before this change, the cleanup phase only contains
`target_cleanup_failed`.

**Paperclip version or commit**
master at c99bcd89fe.

## What Changed

- `tests/runner-e2e/product-journeys/launch.ts`: when target release or
stop-only recovery fails, also record `cause.<code>` as a failed cleanup
phase. The code must match `^[A-Za-z][A-Za-z0-9_]{1,79}$`. Other values
are ignored. Timeouts keep their existing codes. `result.error` stays
`target_cleanup_failed`.
- `tests/runner-e2e/product-journeys/campaign.test.ts`: add a regression
test. It checks that an identifier code is kept, that the raw message is
not kept, and that a free-form code is not kept.

## Verification

- `npx vitest run --config tests/runner-e2e/vitest.config.ts
tests/runner-e2e/product-journeys`: 46 files, 1235 tests passed.
- Negative control: with the old `launch.ts`, the new test fails. With
the change, it passes.
- `tsc -p tests/runner-e2e/tsconfig.json` shows no errors in the changed
files. (This worktree has unbuilt workspace packages, so other unrelated
errors appear.)
- The Paperclip Cloud TestOps exporter already publishes phase codes
that match its closed code pattern as step messages. A `cause.*` code
matches that pattern.
- Not verified live: QA staging smoke uses a pinned harness revision
(`QA_SMOKE_HARNESS_SHA`). The new phase appears in TestOps only after
the pin moves to a revision that contains this change.

## Risks

- Low risk. The change only adds a phase entry when cleanup already
failed. Pass and fail grading does not change.
- An adapter could put private data into an error `code`. The strict
identifier pattern limits this to short identifier strings.
- Report consumers that count cleanup phases see one more failed cleanup
phase.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with
tool use (shell, browser, code editing). Extended reasoning was used.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 17:41:03 -06:00
DottaandClaude Opus 5.5 b1a71044af fix(evals): observe chat handoff replies and restarted task pages (#19073)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The Product regression TestOps plan runs 20 Product E2E cells
through `runner-full-stack-e2e.yml`
> - The full 20-case TestOps launch on `master` 4740d251 passed 13 of 20
> - Four failures came from the eval's observation, not from the product
> - Three Agent Chat handoff cases raced the product's completed-handoff
reply turn, and the run-count contract treated that reply as an
unexpected run
> - One continuation case waited for `DOMContentLoaded` after a server
restart, which the dev server can leave pending while the page is
interactive
> - This pull request makes the eval observe those two behaviors
explicitly
> - The benefit is that these cases measure the product again, and the
handoff cases now also prove that the result returns to the chat

## Linked Issues or Issue Description

Refs #14408, #19053

**What happened?**
TestOps launch https://paperclip.testops.cloud/launch/2694 (GitHub run
https://github.com/paperclipai/paperclip/actions/runs/41915965118)
failed these cells:
- `agent-chat` `multi-repository`, `clarify-reuse`, `plan-handoff`:
"Declared total provider-run bounds". Each final run ledger has exactly
one extra run. That run is the source chat's `chat_task_completed` turn,
with `chatCompletionUpdates` naming the finished task.
- `continuation` `completed-action-resume`: `page.goto: Timeout 30000ms
exceeded` waiting for `domcontentloaded` right after the deliberate
server restart. The server log shows the page loaded and called the API
during that wait.

**Expected behavior**
A completed handoff returns to the source chat (#14408). The eval must
observe that reply, require it, and count it. After a restart, the eval
must wait for the rendered task, not for a browser load event.

**Steps to reproduce**
Run the cells listed above on `master` with the Product regression
TestOps lane.

## What Changed

- `chat-flow.ts`: after the delegated task is `done`, the handoff branch
waits for the source chat's completion-reply turn
(`chat_task_completed`, correlated by `chatCompletionUpdates[].id`). It
requires exactly one such turn, `succeeded`, and exactly one agent
comment created by that turn.
- `chat-cases.ts`: the declared run counts include that one reply turn:
`multi-repository` 2 → 3, `clarify-reuse` 3 → 4, `plan-handoff` 4 → 5.
The minimum and maximum stay equal, so a missing reply or an extra run
still fails.
- `catalog.test.ts`: the catalog run-count total changes from 385 to 397
for these three cases across their profiles.
- `continuation-flow.ts`: `open()` uses `waitUntil: "commit"` with a 60
s navigation limit, then requires the rendered task thread
(`waitForTaskChatRendered`). This matches the existing restart handling
in `chat-flow.ts`.

## Verification

- `npx vitest run --config tests/runner-e2e/vitest.config.ts
tests/runner-e2e/chat-flow.test.ts tests/runner-e2e/continuation.test.ts
tests/runner-e2e/catalog.test.ts
tests/runner-e2e/kimi-qualification.test.ts
tests/runner-e2e/non-execution-wake.test.ts
tests/runner-e2e/product-regression-testops-catalog.test.ts
tests/runner-e2e/lifecycle-live.test.ts`: all pass.
- `tsc -p tests/runner-e2e/tsconfig.json`: no errors in
`tests/runner-e2e`.
- The retained ledgers from run 41915965118 show the reply turn:
`multi-repository` 3 runs, `clarify-reuse` 4 runs, `plan-handoff` 5
runs, each with one `chat_task_completed` run on the chat issue.
- Live single-case TestOps runs against this branch: see the PR
comments.

## Risks

- Changed eval conditions: the three handoff cases now also require the
completion reply. A product regression that drops the reply now fails
these cases, as intended.
- The continuation navigation allowance changes from 30 s
(`domcontentloaded`) to 60 s (`commit`) plus a rendered-thread check.
That is an observation deadline, not a product latency requirement. The
rendered-thread check is stronger than the old load event.
- Three other plan failures are not changed here. They are product
failures: the agent creates `human_only` confirmations, and
`resolve-from-comment` then refuses to record the user's explicit text
approval (`interaction_human_only`).

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), via 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-04 17:40:58 -06:00
2ccc7d37ef feat(ui): retain approved reads across outage reloads (#18825)
## Thinking Path

> - Paperclip lets people manage agent work through the board.
> - The board already retains loaded reads during a temporary server
failure.
> - Reloading the page removes that in-memory data.
> - A deploy can then leave a returning user without the shell or recent
tasks.
> - This pull request saves an explicit, limited set of reads in the
browser.
> - Cached identity supports display while fresh session checks protect
writes.

## Linked Issues or Issue Description

**What happened?**
A reload during an API outage loses the board's loaded reads. An
automatic worker update can also reload a hidden tab while a write is
pending.

**Expected behavior**
Show the cached shell and recent tasks during a temporary outage. Limit
saved content by user, company, age and build. Clear it on sign-out.
Wait for a fresh session before sending account-bound writes. Defer
automatic reloads while work is pending.

**Steps to reproduce**
Open a task and settings while online. Return 503 from the API and
reload. Restore the API. Sign out and confirm that saved query records
are gone. Discover a worker update while offline or during an autosave
and confirm that it waits.

Stacked on Refs #18822. Related durable-write foundation: Refs #17960.
The outage browser suite follows in a separate test PR.

## What Changed

- Persist an exact read allowlist through TanStack Query dehydration and
hydration. Save shell preferences, company/sidebar data, agent/project
lists, plugin UI metadata, the first inbox page, and 20 recent tasks.
Task UUID and identifier reads share one recent-task slot.
- Namespace records by user and company. Retain eligible reads across
normal memory garbage collection, expire each read after 24 hours, and
discard them after a build change. Unstamped builds disable hydration
and persistence while retaining sign-out cleanup. Use the existing
instance-wide plugin contribution query in the user's root record.
- Exclude session credentials and bootstrap data, embedded plan
documents, comments, preview work products, provider credentials and
environment values, browser capabilities, pairing data, other queries,
and mutations. Restore only display identity from session data. Preserve
safe agent provider/model display fields and configuration object
shapes. Apply the projection on save and restore.
- Clear saved reads on sign-out, cross-tab account changes, and
definitive session denial. Keep revocation working on plain HTTP without
crypto.randomUUID. Use per-user revocation revisions to reject delayed
writes and stale records. Preserve a newer account. Remove every
UUID/identifier projection and nested inbox copy of a denied task.
- Detach shared GETs when the account changes or signs out. Load
complete, freshly authorized agents before opening bulk settings from a
display-only roster. Retire a pending bulk-editor result or error when
row, select-all or clear changes the selection, including a
clear/reselect of the same agent.
- Require a fresh server lookup before account-bound writes, pending
task creation, or mounted autosaves resume. This includes automatic task
read receipts, inbox archives, workspace preferences and primary-agent
choices, with optimistic archive changes gated too. Cancel the old write
scope when the verified user changes. Release completed archives through
account-independent cleanup without restoring old cached rows; an old
operation cannot clear a newer hide flag. Retry unconfirmed read
receipts after a fresh session succeeds for the original account, and
remember confirmed receipts across refreshes. Track pending reads per
task so opening another task sends its receipt immediately; revisiting
reuses an outstanding receipt and its original account scope.
- Defer a hidden-tab worker reload until connectivity, mutations,
durable sends, and autosaves are safe. Synchronously count each
unresolved operation across online rate-limit retries. Release a
cancelled target’s unused dirty token when an editor changes tasks; keep
stored drafts, admitted saves and queued retries. Cover autosave
debounce, newer edits, navigation and undo.

## Verification

- Current head `fe4f682c879` is merge-ready: all 54 check runs completed
(52 successful, one skipped, one neutral), the legacy Snyk status is
successful, and both `ci / verify` and `ci / e2e` pass in run
`41752927822`. Greptile Apex reviewed this exact head at 5/5 with zero
unresolved threads; the branch has no merge conflicts. All three PRs now
meet the merge-ready bar; the stack remains unmerged pending operator
approval.
- Greptile Apex reviewed exact current head `fe4f682c879` at 5/5 with no
actionable findings and zero unresolved threads (summary updated
2026-11-04 at 01:24:40 UTC). Both settings-account and pending-selection
findings are fixed, replied to and resolved. The branch has no merge
conflicts. Current-head CI now passes in run `41752927822`.
- Seventeen new cases cover settings restored from actual saved query
records, matching/changed/signed-out identities, denied verification,
late crown responses without overwriting newer reads, and bulk-selection
changes during successful/failed reads. Sixteen fail the preceding
source and one preserves denial compatibility. Both settings paths reuse
the existing account-bound mutation guard; its optional explicit
QueryClient supports the workspace hook’s existing detached-host mode.
Primary-agent optimistic updates begin only after verification and
cleanup checks ownership and the cache revision.
- Earlier CI run `41749456480` on prior head `2cf7516f18c` has one real
server test failure: the owned-runtime case in
`idle-completed-history.test.ts` could not reach its HTTP child on port
34757 within 10 seconds (99 refused probes). Its other 1,829 cases pass
and three skip; the only other unsuccessful job is the downstream
`verify` gate. The exact selected case passes locally, and the entire
75-case file passes in 72.19 seconds on the exact `2cf7516f18c` source
tree with isolated PostgreSQL and a real child process on macOS
arm64/Node 25.6.1. The Linux CI startup cause remains unknown; these
local results are not a fix or CI acceptance. One failed-jobs retry with
debug logging was accepted on that unchanged head, then superseded by
the later settings fixes. Current-head CI remains required; no source
change was made for this failure.
- Current cache head `fe4f682c879`, based on read head `09d1db29dea` and
master `683fa4725e9`, passes all 1,198 focused tests across twenty-eight
files, UI typecheck, token gates and the enforcing query-error guard.
Dependent browser head `228c1b68d30` contains this exact application
source and passes standalone strict checking of all three browser specs.
Current-head CI now passes in run `41752927822`.
- Three new navigation regressions fail the preceding application source
and pass with the fix. They open three cached tasks before any receipt
resolves, leave the page before success or failure, revisit an
outstanding read without duplicating it, and recover an older failed
receipt after a fresh session succeeds while a later task remains
pending.
- The fixture repair passes all seven deadline integration cases and the
complete server typecheck, including the server test layer. The old
assertion deterministically fails at 23:59:06.888 UTC; its
one-second-old run also fails the first-second-of-day probe. The
corrected fixture passes noon and both midnight probes. The permanent
three-case coverage uses seeded 2099 clocks.
- Nine new cases cover archive success/failure after sign-out and
another account takes over, both mounted and unmounted; cleanup
ownership when a newer archive uses the same task; session-check 429
exhaustion followed by matching, changed or signed-out identity; and a
failed read receipt followed by a new session read. They verify no
old-cache restoration, toast or navigation, retained newer hide flags,
no failed-check loop and no duplicate confirmed receipt. Seven fail the
previous implementation; two compatibility cases retain account-change
suppression. Initial test error-constructor and query-notification
timing assumptions were corrected before the passing checks.
- The inherited skills/app error-priority fix passes all 60 affected
tests and UI typecheck on both application branches. Its twelve new
regression cases fail the preceding read implementation. The
persisted-cache suite includes 48 cases.
- Six new regressions fail against the preceding application source and
pass with the fix. Actual worker-update and mutation-cache listeners
cannot reload a hidden tab between online 429 retries; the original
create key survives, and the update reloads once after a receipt. Real
self-hosted and Cloud sign-out work without crypto.randomUUID, with
stamped and unstamped builds. Saved reads are removed and account write
scopes are invalidated. An initial test-fixture type annotation error
was corrected before the passing typecheck.
- The 48 cache tests cover actual five-minute memory GC, the 20-task
bound across saved and live reads, original 24-hour expiry, denial
before session verification, unstamped cleanup, cross-tab revocation and
delayed flushes. Private GETs cannot cross account transitions. Bulk
edits require complete current agent data. Undo releases the update
blocker while preserving the Saved indicator.
- All 23 built-UI outage/write/loading cases pass in 4.5 minutes on
exact integrated head `228c1b68d30`, containing current cache revision
`fe4f682c879`: ten outage, six durable-write and seven loading cases.
The run uses bundled Chromium, the built UI, a fresh local-trusted
server and isolated embedded PostgreSQL on macOS arm64/Node 25.6.1. All
three specs pass standalone strict TypeScript checking. Owned temporary
server and database stopped.
- Earlier browser runs exposed and fixed cached-header, native Storage
cleanup and replay/session defects. A prior run passed 21/22 because
real WebSockets contradicted the simulated HTTP outage; the fixture now
closes existing socket pairs and holds new sockets through the same
outage deadline. An older CI run exposed a disposed response in the
Inbox route callback; pre-navigation snapshots remove that lifetime, and
the current full run includes that repair. Earlier failures and their
diagnosis remain in the discussion. The current full run passes. Broader
historical workspace failures remain documented in Refs #18822; this
does not claim a clean full workspace suite.
- Current-head CI passes on `fe4f682c879`. Controlled local outages do
not complete hosted authentication or a real Cloud restart/deploy
acceptance.

Commands for the current cache change and inherited read fix (the
current focused run also includes src/lib/inboxArchiveCache.test.ts and
src/pages/Inbox.test.ts):

```sh
pnpm --filter @paperclipai/ui exec vitest run src/pages/CompanySkills.test.tsx src/pages/apps/AppNotConnected.test.tsx
pnpm --filter @paperclipai/ui exec vitest run src/hooks/useDurableSubmit.test.tsx src/hooks/useDurableAutosave.test.tsx src/hooks/useSignOut.test.tsx src/lib/persisted-query-cache.test.ts src/lib/service-worker-updates.test.ts src/lib/service-worker-cache-policy.test.ts src/lib/pending-send.test.ts src/lib/issue-execution-policy.test.ts src/components/InlineEditor.test.tsx src/components/MarkdownEditor.test.tsx src/components/NewIssueDialog.test.tsx
pnpm --filter @paperclipai/ui exec vitest run src/components/agents/AgentBulkRoster.test.tsx src/components/agents/AgentBulkSettingsSheet.test.tsx src/hooks/useAccountBoundMutation.test.tsx src/pages/IssueDetail.test.tsx src/components/TaskChatThread.test.tsx src/components/TaskChatThread.task-creation-order.test.tsx src/api/client.test.ts src/api/agents.test.ts
pnpm --filter @paperclipai/ui exec vitest run src/hooks/useCoordinatorWorkspaceEnabled.test.tsx src/context/PrimaryAgentProvider.test.tsx src/components/CoordinatorWorkspaceSetting.test.tsx src/components/primary-agent/PrimaryAgentPresentation.test.tsx src/components/MobileBottomNav.coordinator.test.tsx src/components/CoordinatorSidebar.test.tsx src/pages/Issues.coordinator-routing.test.tsx
pnpm --filter @paperclipai/ui typecheck
pnpm check:token-gates
node scripts/check-query-error-rendering.mjs --enforce --summary
```

## Risks

This deliberately saves task content on the local browser for up to 24
hours. The allowlist excludes credentials and ephemeral capabilities.
Local storage can be disabled or full; persistence then fails without
blocking the app. Restored data can be stale and never grants server
access. A user or access change clears cached account reads and
invalidates pending writes. The browser outage suite and real hosted
deploy acceptance remain release requirements. There are no migrations
or new dependencies.

## Model Used

OpenAI `gpt-6.1-sol` through Codex, with xhigh reasoning effort, 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
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have described the issue in-PR following the bug report template
- [x] I have not referenced internal/instance-local Paperclip issues or
links
- [x] My branch name describes the change and contains no internal
ticket id
- [x] The focused local tests for this change pass
- [x] I have added tests and updated relevant documentation
- [x] I have considered and documented 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: devinfoley <139239+devinfoley@users.noreply.github.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Bender (Fable) <bender@agents.paperclip.ing>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-04 15:40:30 -08:00
35d086213e fix(native): explain failed accepted results on the run (#19091)
## Thinking Path

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

## Linked Issues or Issue Description

Refs #19063.

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

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

Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 17:40:05 -06:00
Saurabh Sungh 66b0aded28 fix(cli): add KillMode=mixed to generated systemd unit (#18598)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The CLI subsystem provides background service management
(`paperclipai service install`) generating systemd user units on Linux.
> - When systemctl stops or restarts the service, systemd uses default
`KillMode=control-group` and sends SIGTERM simultaneously to all cgroup
processes, including embedded PostgreSQL.
> - This races the server's clean shutdown sequence and triggers a smart
shutdown in PostgreSQL instead of a fast shutdown, leading to ungraceful
exit.
> - This pull request adds `KillMode=mixed` to the `[Service]` section
of the generated systemd unit definition.
> - The benefit is that SIGTERM is delivered only to the main server
process, allowing the server to cleanly drain and shut down embedded
PostgreSQL in order.

## Linked Issues or Issue Description

Fixes #18595

## What Changed

- Added `KillMode=mixed` to `renderSystemdUnit` under `[Service]` in
`cli/src/services/service-manager.ts`.
- Added test assertion for `KillMode=mixed` in
`cli/src/__tests__/service-manager.test.ts`.

## Verification

- Automated test: `cli/src/__tests__/service-manager.test.ts` checks
that `renderSystemdUnit` generates a unit containing `KillMode=mixed`.
- Manual verification: Checked rendered systemd unit output to ensure
valid systemd syntax and correct section placement under `[Service]`.

## Risks

- Low risk: `KillMode=mixed` only applies to systemd user services on
Linux generated via `paperclipai service install`. If the main process
fails to exit within `TimeoutStopSec` (300s), systemd sends SIGKILL to
all remaining child processes.

## Model Used

- Google Antigravity (Gemini 3.8 Flash High) with chain-of-thought
reasoning and codebase tools.

## Checklist

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

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

## Linked Issues or Issue Description

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

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

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

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

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

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

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

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

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

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

## Linked Issues or Issue Description

Refs #18691

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

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

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 17:39:26 -06:00
Dotta 071b4dd5c3 fix(evals): preserve continuation restart and approval boundaries (#19076)
## Thinking Path

> - Paperclip manages work done by AI agents.
> - Product E2E journeys test that work through the UI and public APIs.
> - A continuation must retain completed work across a server restart.
> - A human-only plan request needs a recorded human decision.
> - The current fixtures retain an old Vite client and dismiss the plan
request before sending a comment.
> - This change uses the existing restart guard and approves the exact
current plan through its UI.

## Linked Issues or Issue Description

No separate issue exists. The failures are retained in [the original
complete Product
run](https://github.com/paperclipai/paperclip/actions/runs/41915965118).

**What happened?**

`completed-action-resume` reached its intended completed-child and
pending-question state. Its navigation then hit the original 30-second
limit after a server restart. The retained trace shows the old Vite
client polling during listener replacement. The existing chat restart
wrapper unloads that client, but the continuation callback did not use
it.

`revision-preserves-approval` saved the revised plan and final output.
Its exact current human-only confirmation remained pending. The fixture
had clicked Dismiss to enter a plain approval comment. A comment does
not resolve that card, so the final pending-work checks correctly
failed.

**Expected behavior**

Unload the old client before the existing restart. Use the real Confirm
action on the exact current plan revision. Require its saved human
acceptance before the normal continuation settles. Preserve all original
final checks and limits.

**Steps to reproduce**

Run those two existing continuation cases from source `4740d251`. The
original run preserves both failures. The focused controls below require
no provider credentials.

**Paperclip version or commit**

Original failure source: `4740d251b7032e714d33039ba66a56948238c38f`.

**Deployment mode**

Isolated local Product E2E servers in GitHub Actions.

## What Changed

- Route only the continuation restart callback through the existing
`restartChatServer` wrapper.
- Select the one pending confirmation for the exact saved plan revision.
Recheck it before the UI action.
- Click its scoped Confirm button. Require the same target, accepted
outcome, human resolver and timestamp in the saved receipt.
- Preserve the generic chat Dismiss behavior, ordinary reply cases,
original prompts, models, run bounds, grading and deadlines.
- Add focused unit and browser controls for the approval boundary.

## Verification

- The original full run retains these two failed grades. Cleanup passed
in both; that does not make either case pass.
- Focused continuation unit controls: 44 passed across four files. The
final new helper controls also passed 3/3.
- Three distinct controlled browser checks passed: the existing
old-client restart guard, the exact current human-only Confirm action,
and rejection of a changed revision without any approval POST.
- The browser controls use local HTTP fixtures. They do not prove a
fresh provider journey passed.
- All six files match the tested handoff hashes. `git diff --check`
passes.
- Local harness typecheck reached six diagnostics from stale cached
dependency declarations. The missing exports are in unchanged
adapter-utils and Runner code. No owned-file diagnostic remains. No
source or lockfile workaround was used. Fresh CI typecheck, build and
tests remain required.
- Fresh real Product acceptance has not run for this repair. The
original 20-case result remains 13 passed and 7 failed.


Current-head CI passes at `335683b741df5a3d0b76d035e105f0b44eda832d`:
required `ci / verify` and `ci / e2e` are green. [CI
run](https://github.com/paperclipai/paperclip/actions/runs/41922544434).
Greptile is 5/5 on this head. Ponytail is 4/5 with a ship
recommendation; its checkpoint warning was resolved with the dominating
awaited snapshot/push proof. No source changed after these checks. Real
changed-source journey acceptance remains pending.

## Risks

- The browser controls model the restart and approval boundaries; the
original real cases must still run after delivery.
- A changed or stale confirmation now fails before an approval. A
dismissed card or an agent-authored claim cannot satisfy human
acceptance.
- This fixture correction grants no new product permission and changes
no provider or scenario limit.

## Model Used

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

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` 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
2026-11-04 17:39:10 -06:00
3634c4a845 fix(chat): log the underlying error when a publication becomes delivery_unknown (#18875)
## Thinking Path

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

## Linked Issues or Issue Description

Refs #18874

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

## What Changed

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

## Verification

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

## Risks

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

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

## Model Used

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

## Checklist

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

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

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

## Issue

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

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

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

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

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

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

## What Changed

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

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

---

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 17:35:34 -06:00
Nicky LeachandClaude Opus 5.5 aa8372fd5d fix(accounting): post late usage to its owning run (#18991)
## Thinking Path

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

## Linked Issues or Issue Description

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

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

**Paperclip version or commit**

`master` at `04a24f4e06`.

**Deployment mode**

Any deployment that uses a native Runner with Codex.

## What Changed

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

## Verification

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

## Risks

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

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 15:30:55 -08:00
Devin FoleyandClaude Opus 5.5 15114bb9a0 fix(workspaces): keep durable seed copies above the free-space reserve (#18999)
persistDurableSeedArchive now checks the source file and applies the shared workspace free-space reserve before copying, so a large seed cannot fill the instance volume.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 15:29:55 -08:00
Nicky LeachandClaude Opus 5.5 2c39a2866b feat(plugins): let managed config grant worker variables to a plugin (#19049)
## Thinking Path

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

## Linked Issues or Issue Description

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

**What existing behavior does this improve?**

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

**Current behavior**

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

**Proposed behavior**

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

**Reason and benefit**

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

**Alternatives considered**

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

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

---------

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

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

## Linked Issues or Issue Description

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

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

The new worker test runs these steps against PostgreSQL.

**Paperclip version or commit**

`master` at `69dea878c0b`.

**Deployment mode**

Any server with native runs that use remote sandbox workspaces.

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 15:14:39 -08:00
7d1e9b3477 fix(recovery): retain exact warm native export sources (#17916)
## Thinking Path

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

## Linked Issues or Issue Description

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

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

## What Changed

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

## Verification

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


## Risks

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

## Model Used

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

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Devin Foley <devin@paperclip.ing>
canary/v2026.1104.0-canary.15
2026-11-04 14:47:42 -08:00
801d9548e3 test(server): cover competing recovery action inserts (#18773)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Recovery owners record one active action for each task source.
> - Two owners can read an empty source before either inserts its
action.
> - PostgreSQL rejects one insert, and Drizzle wraps that conflict.
> - The production fix for wrapped conflicts already landed in #18885.
> - This pull request adds a real database race test for that fix.

## Linked Issues or Issue Description

Refs #18885 and #18544. Related recovery work: #18402 and #18705.

**What happened?**

[CI run
41585908789](https://github.com/paperclipai/paperclip/actions/runs/41585908789/job/125752385968)
failed with `issue_recovery_actions_active_source_uq`. The recovery
worker did not handle the wrapped PostgreSQL conflict. #18885 now
provides the production fix.

**Expected behavior**

Both competing recovery owners return the same active action. The
existing bounded retry handles the exact wrapped unique constraint.

**Steps to reproduce**

1. Create a task in real PostgreSQL.
2. Hold both recovery inserts until both owners read an empty source.
3. Release both writes.
4. Check that the losing transaction records a wrapped `23505` conflict
and both service calls return one action.

## What Changed

- Retain the newer bounded cause-chain handling from #18885.
- Add a deterministic database race test. Check the actual wrapped
conflict, both returned action IDs, the saved action count, and the
attempt count.
- Keep the original real-clock settlement and idle-window assertions.

## Verification

- The two owning suites passed **98/98 tests** after resolving the
conflict: `pnpm exec vitest run
server/src/__tests__/issue-recovery-dependencies.test.ts
server/src/__tests__/issue-recovery-actions.test.ts`.
- Workspace typechecks passed across the root run and a successful
server rerun. The first server attempt could not resolve the local
plugin SDK from provider sources. Restoring that checkout-only SDK link
fixed resolution. The server typecheck rerun passed.
- Full local `pnpm test:run` has not run. The current local full build
stopped after the host exhausted disk space during native compilation.
Generated caches were removed from this checkout; no source or test
setting changed. Fresh CI is complete: 53 successful checks, two
expected skips and one neutral security scan. The branch is mergeable,
and the exact-head Greptile review is 5/5 with zero unresolved threads.
- Greptile scored current head
`dbaee78272a5b0f1db87ba33340a63571ef16948` **5/5** with no actionable
findings.
- Historical red proof: before wrapped-conflict handling, this race
returned one rejection. The losing transaction carried PostgreSQL code
`23505` and constraint `issue_recovery_actions_active_source_uq`.
- Preserve prior CI evidence: an [old serialized
job](https://github.com/paperclipai/paperclip/actions/runs/41649267044/job/125964435813)
hit an agent-audit setup timeout and a heartbeat wait failure. The exact
timing cause remained unproven. Its [single
rerun](https://github.com/paperclipai/paperclip/actions/runs/41649267044/job/125972675510)
passed. This PR changes no waits or assertions in those tests.

## Risks

- Low risk. The final diff adds one database regression test. It changes
no production code, schema, API, permissions, runner routing, or
workflows.

## Model Used

- OpenAI GPT-6.1 Sol (`gpt-6.1-sol`), xhigh reasoning, tool use, and
code execution.

## Checklist

- [x] I have included a thinking path and specified the model used.
- [x] I have searched for related work and linked the production fix.
- [x] I have included only public issue and PR references.
- [x] The changed test passes against real PostgreSQL.
- [x] I have documented verification limits and risks.
- [x] All Paperclip CI gates are green.
- [x] Greptile is 5/5 with no unresolved comments on the final head.
- [x] I will address review comments before requesting merge.

Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Devin Foley <devin@paperclip.ing>
2026-11-04 14:47:29 -08:00
Devin Foley 69dea878c0 fix(sentry): prove a native capacity retry from its successor run (#19051)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The server sends unexpected run failures to Sentry as run-failure
events
> - A native Codex run can fail because the model is at capacity.
Paperclip then schedules a bounded automatic retry
> - #18295 keeps that scheduled retry out of Sentry. It requires the
committed finalization fields on the failed run row
> - On staging, the failed run row can lose `finalizationReasonCode`
after the decision is committed. The retry run exists, but the report
still goes to Sentry
> - This pull request uses the successor retry run as the proof, and
stops reading the run's own finalization fields
> - The benefit is that a capacity failure that has a scheduled retry
does not make Sentry noise. Exhausted and held failures still report

## Linked Issues or Issue Description

Refs #18295

**What happened?**
A native Codex run failed with `native_provider_overloaded`. The status
decision scheduled the capacity retry, and the retry run was created.
The server still sent a run-failure event to Sentry. The server release
contained #18295. The failed run row had `finalizationPhase:
"committed"`, but it did not have `finalizationReasonCode`. The retry
run was created about two seconds before the failed run's terminal
write.

**Expected behavior**
When the committed capacity decision creates a retry run, the failure
stays local. The retry owns recovery. A later failure of the retry
reports its own outcome.

**Steps to reproduce**
1. Make a native Codex run fail with a typed `codex`/`server_overloaded`
diagnostic.
2. Let the status decision schedule the `native_provider_overloaded`
retry.
3. Let a late terminal write replace the run's `resultJson` with a
version that does not have `finalizationReasonCode`.
4. Report the run failure. Before this change, Sentry gets an event.

**Additional context**
Only the committed `schedule_retry` effect in
`status-decision-committer.ts` creates a successor with
`scheduledRetryReason = native_provider_overloaded` (through
`materializeNativeProviderCapacityRetry`). Thus the successor row is a
stronger proof than the run's own finalization fields.

## What Changed

- `server/src/services/run-failure-report.ts`:
`isScheduledNativeCapacityRetry` no longer requires `finalizationPhase
=== "committed"` or `finalizationReasonCode ===
"native_provider_overloaded"` on the failed run. It still requires a
failed native run with `errorCode = native_provider_overloaded`, the
typed `codex`/`server_overloaded` diagnostic, a native task, no signal,
and a phase that is not setup. It still requires the successor run for
the same task with `retryOfRunId = run.id` and `scheduledRetryReason =
native_provider_overloaded`. The successor lookup and its index use do
not change.
- `server/src/services/__tests__/run-failure-report.test.ts`: two new
cases. A scheduled retry stays local when the run row has no
finalization fields, and when the row has an earlier finalization phase.
The "finalization is not committed" must-report case changes to
"finalization has no fields and no retry run exists". That case still
reports.
- `doc/observability.md`: describes the successor-run proof.

## Verification

- `cd server && npx vitest run
src/services/__tests__/run-failure-report.test.ts`: 284 tests pass.
- Red check: with the `master` version of `run-failure-report.ts`, the
two new cases fail. Each one captures one Sentry event. All other 282
tests pass.
- `npx vitest run
src/services/native-runtime/native-provider-failure.test.ts
src/services/native-runtime/status-arbiter.test.ts`: 73 tests pass.
- I could not load `native-provider-overload.integration.test.ts` in my
local sandbox because of a missing environment setting. CI runs it.
- Exhausted, held, other-lane, other-task, no-task, legacy, setup,
signal and untyped-diagnostic failures still report. The existing
must-report cases prove this.

## Risks

Low risk. The change removes two field checks. The successor-run check
stays. Only the committed capacity decision creates a successor with
this retry reason, so a failure without a scheduled retry still reports.
No migration. No change to the retry behavior.

## 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)
2026-11-04 14:38:53 -08:00
Nicky LeachandClaude Opus 5.5 0173110ee3 fix(native): point failed run attachments at the docs (#19052)
## Thinking Path

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

## Linked Issues or Issue Description

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

## What Changed

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

## Verification

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

## Risks

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

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 14:36:17 -08:00
Devin FoleyandClaude Opus 5.5 72d80944a9 fix(skills): keep the skill cache lock transaction active (#18996)
Replaces #17907. That branch conflicted with `master` after #17918 added
preparation timing to the same function. This branch applies the same
fix on current `master` and keeps that timing.

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Before a run starts, the server prepares the agent's skills while
holding a per-company skill cache lock.
> - The lock is a Postgres advisory transaction lock on a dedicated
connection. The preparation work runs on the ordinary pool, so the lock
transaction sits idle.
> - Hosted Postgres (Neon) sets `idle_in_transaction_session_timeout`.
If preparation runs longer than that timeout, Postgres ends the lock
session.
> - The lock is then released early, and the commit fails with `write
CONNECTION_CLOSED`. The run fails during setup and the preparation work
is lost.
> - #16340 and #16956 fixed the same problem for the workspace source
lock and the lifecycle lock with `withWorkspaceLockKeepalive`.
> - This pull request applies that keepalive to the skill cache lock.
> - With it, slow skill preparation keeps its lock and the run no longer
fails.

## Linked Issues or Issue Description

Refs #16340
Refs #16956
Supersedes #17907

**What happened?**

A hosted staging run failed during setup with `write CONNECTION_CLOSED
<neon-pooler-host>:5432`. The run trace shows `skills.prepare` running
for 322 seconds before it failed. That is longer than Neon's 5-minute
`idle_in_transaction_session_timeout`. The stack ends in
`postgres/src/connection.js` (`TLSSocket.closed`).

**Expected behavior**

Skill preparation holds the skill cache lock until it finishes, and no
other preparation for the same company can enter the locked section
during that time. The run then continues to execution.

**Steps to reproduce**

1. Set `idle_in_transaction_session_timeout` on the database role to
`1s`.
2. Call `withSkillVersionCacheLock` with an action that waits longer
than 1 second.
3. Without this change, a second `tryOnly` call for the same company
gets the lock while the first call still holds it. The first call then
fails with `write CONNECTION_CLOSED` instead of returning its own result
or error.

## What Changed

- After `withSkillVersionCacheLock` gets the lock and records the
`skills.cache.acquire` phase, it reads
`idle_in_transaction_session_timeout`. It then runs the action inside
the existing `withWorkspaceLockKeepalive` helper. The helper sends
`select 1` on the lock transaction every third of the timeout, and at
least every 30 seconds. When the timeout is 0 (disabled), the helper
does nothing.
- The `.catch` from `master` that ends the timing phase on failure is
kept.
- Added `skill-version-cache-lock.test.ts`. It runs against real
embedded Postgres with a 1-second idle timeout, following the same
pattern as `isolated-remote-workspace-source-lock.test.ts`.
- The edited file no longer has an extra blank line at the end. This is
whitespace only.

## Verification

All checks below ran on this branch's tree, which is `master` at
afbc5a88 plus this change.

- **Red on current `master`:** both new tests fail against the unpatched
`master` source. The overlap test gets the lock a second time (`promise
resolved "'overlap'" instead of rejecting`). The failure test gets
`write CONNECTION_CLOSED 127.0.0.1:<port>` instead of the original
error, which is the same error as in production.
- **Green with the fix:** `skill-version-cache-lock.test.ts` and
`native-workspace-lock-keepalive.test.ts` pass (8 tests).
- **Related suites pass:** 13 files and 245 tests, covering
`execution-preparation-timing`, `isolated-remote-workspace-source-lock`,
`company-skills` (service, runtime concurrency and runtime selection),
every `skill-version-*` suite and `heartbeat-execution-configuration`.
- **Other checks:** `node scripts/check-module-boundaries.mjs` and `git
diff --check` pass.
- **Not run locally:** the full server `tsc`, because the sandbox has an
8 GB memory limit. CI has to confirm it.
- **Not done:** a live run on hosted staging. That will show after the
normal rollout.

On #17907, CI failed in `environment-cleanup-work.test.ts` (an
off-by-one call count) and `chat-channels.integration.test.ts` (reply
ordering). Neither test touches the skill cache lock. This branch runs
CI again on current `master`.

## Risks

- Low risk. The change runs only inside the existing lock transaction.
The work still uses the ordinary pool, so a one-slot pool cannot
deadlock.
- Each lock adds one `pg_settings` read. A long lock adds one `select 1`
at least every 30 seconds.
- If a keepalive query fails, the helper ignores the failure, and the
work's own result or error is unchanged. The other locks behave the same
way.
- This change does not explain why preparation took more than 5 minutes.
That is a separate performance question.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 14:36:01 -08:00
DottaandPaperclip 31266d7159 fix(logs): drain existing admissions before finalization (#19055)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Run logs retain execution output for operators.
> - Object storage uses bounded admission and asynchronous uploads.
> - A diagnostic can wait for capacity when its native execution ends.
> - Finalization closed the writer before that existing admission could
finish.
> - This pull request joins those admissions before it closes the log.
> - The benefit is complete accepted output with the existing storage
bounds.

## Linked Issues or Issue Description

Refs #18944 and #18327.

**What happened?**

A runner close diagnostic began object-log admission before its recorder
retired. The call waited at the 8 MiB pending-byte bound. Completion
then closed the writer. The waiting call failed with `Object run log
writer is closed`. A valid final log hash covered the stored prefix, but
the diagnostic was absent.

**Expected behavior**

Finalization must retain calls that started before closure. It must
reject new calls after closure. Cancellation and storage failures must
still settle without a success claim.

**Steps to reproduce**

1. Use the actual recorder and segmented object store with a controlled
conditional upload.
2. Fill the pending-byte bound. Start native warm-session retirement
while the diagnostic waits for capacity.
3. Complete the run through the real completion handler. Release the
upload.
4. Check the diagnostic bytes, final hash, source coverage, and warning
rows.

## What Changed

- Track active admission promises in each object-log writer.
- Reject new producers when finalization starts. Join existing bounded
admissions, then flush their tail before manifest closure.
- Add actual completion coverage and store controls for failure, Stop,
global capacity, and independent writers.
- Clarify the finalization contract in the storage documentation. Keep
feature maps unchanged for this routine fix.

## Verification

- Before the fix, the actual completion probe lost a pre-retirement
close diagnostic and emitted `object_log_persistence_failed`. Useful
canonical output, source coverage, and the final prefix hash remained
valid.
- After the fix, the exact probe retains the diagnostic and checks the
stored bytes and full hash. Ordinary completion and stalled local append
controls also pass.
- Focused store controls pass, including shared 64 MiB capacity,
new-producer rejection, storage failure, Stop, and independent writer
progress.
- Production and test source at
`bc03f8d53a721f01f80a12105a69d8583865e651`: 3 actual completion controls
and 53 store, recorder, journal, and diagnostic tests pass. Run with
`node node_modules/vitest/vitest.mjs run` from `server/`; select the
four safety files plus the completion file with `-t "retired recorder"`
in a separate invocation.
- Initial treatment repository typechecks and build pass. Current map
and module checks pass. A broader four-suite run had 99 passing
assertions but failed its existing 30-second completion teardown hook. A
later isolated completion run passed all 61 cases at the same limits.
The original teardown cause remains unresolved; no timeout was changed.
- Repository typechecks and build pass at that source commit. Current
head `6652adc7f3379ae250f7b726dad6c58c8482410f` removes only the
incidental map paragraph; production and test source are identical. All
current-head checks are terminal: 54 successful check runs, one
successful legacy status, and three skipped checks. Required `ci /
verify` and `ci / e2e` pass. Greptile reviewed exact current head at
5/5. Both review threads are resolved. GitHub reports the PR clean and
mergeable. Full repository tests stopped in chat shard 2: a GitHub
recovery fair-batch test expected no provider requests but observed `GET
/app/hook/config`. Its exact unchanged case passes in isolation. The
original cause remains unresolved; this full-suite run is red.
First-head remote server lane 2 failed a native-reassessment backlog
test: its quiet check observed two active work items instead of zero.
The exact unchanged case passes locally. That original CI cause remains
unresolved; no job retry or timeout change was made. Other first-head CI
lanes passed. The first native review rated the earlier source head 4/5
for the same removed map note. That is not a current-head assessment.
Current GitHub rules require zero approving reviews and only the two CI
checks above; a fresh native advisory re-review is optional and remains
unavailable while its earlier run has an unresolved workspace/accounting
obligation. That obligation is tracked separately and is not claimed
recovered.
- Removed the incidental map paragraph identified by Greptile and the
native review. The follow-up contains no runtime or test changes.
- This proves the local production-path mechanism. It does not attribute
any specific hosted warning to this cause. Hosted replay and served
revision checks remain separate acceptance work.

## Risks

Finalization now waits for existing bounded capacity admissions. Storage
cancellation and the existing operation deadlines still apply. Tests
check that failed uploads and Stop settle, and that a separate writer
can make progress. No capacity limit, timeout, or execution authority
changes.

## Model Used

OpenAI `gpt-6.1-sol`, through Codex, with reasoning and tool execution.
Context window size is not exposed by the host.

## 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 (56 focused checks; the
full-suite red remains documented above)
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 16:34:36 -06:00
DottaandClaude Opus 5.5 86afc2048b fix(native): admit follow-ups after an owner finished in this process (#19071)
## Thinking Path

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

## Linked Issues or Issue Description

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

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

**Paperclip version or commit**

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 16:29:25 -06:00
c19c4348c5 fix(server): record inspector provenance for dynamic import callback errors (#17844)
<!-- entire-trail-link-start -->
https://entire.com/gh/paperclipai/-/trails/49/changes/paperclip/49
<!-- entire-trail-link-end -->

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The server sends errors to Sentry when an operator sets a backend
DSN. An unhandled rejection is captured in `strict` mode, so the server
process exits after the capture.
> - A hosted server reported `TypeError
[ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING]: A dynamic import callback was
not specified.` two times, eight days apart, on two unrelated releases.
Each event stopped the process.
> - The stack has no first-party frame. An anonymous `setTimeout`
callback at `<anonymous>:1:N` calls `import()`. The column changed
between the two events, so the source text changed.
> - In local reproduction, only two paths make that frame:
`vm.compileFunction` without a filename, and an expression that a client
evaluates through the Node inspector. No shipped server code or
production dependency uses either path. The current events give no data
about the source.
> - This pull request adds bounded provenance to that one error: the
inspector state when the event is reported (never its URL), allow-listed
Node flag names, the process ids, the main-thread flag and the uptime.
> - The benefit is that the next event gives evidence about the source
of the anonymous code. A `closed` state rules out an expression from a
still-open inspector session. An `open` state needs other evidence
before a debugger is blamed.

## Linked Issues or Issue Description

No public issue exists. This section follows the bug report template.

**What happened?**

A hosted server process stopped two times with an unhandled rejection:
`TypeError [ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING]: A dynamic import
callback was not specified.` The stack is
`importModuleDynamicallyCallback (node:internal/modules/esm/utils:280)`
← `Timeout._onTimeout (<anonymous>:1:20)` (first event) or
`(<anonymous>:1:41)` (second event) ← `listOnTimeout`. The events came 3
minutes and 17 minutes after process start, on two releases that changed
only skill documents and UI files. The Sentry event has no data that
shows where the anonymous code came from.

**Expected behavior**

The error report gives enough data to investigate the source of the
anonymous code: whether an inspector session was open in the process,
and which inspector-related Node flags were set.

**Steps to reproduce**

1. `node -e "require('node:vm').compileFunction(\"setTimeout(() =>
import('node:fs'), 1)\", [])()"` gives the same error and the same
`Timeout._onTimeout (<anonymous>:1:N)` frame.
2. An expression such as `setTimeout(() => import('node:fs'), 1)`,
evaluated through the inspector (`Runtime.evaluate`), gives the same
error and frame.
3. `vm.Script` (`evalmachine.<anonymous>`), `eval` or `new Function` in
a `vm` context (`eval at …`), `new Function`, indirect `eval`, `node
-e`, `node -p` and stdin give a different frame or import successfully.

**Paperclip version or commit**

`master` at `0cb3fc7db591ba23e21fbbc70042bc44ec7d6f20`. The hosted
events came from builds `f96a1e64` and `3e7c3ee9`, on Node v24.22.0 and
v24.22.1.

**Relevant subsystem**

Server Sentry error monitoring (`server/src/sentry.ts`).

## What Changed

- New `server/src/sentry-dynamic-import-provenance.ts`:
`addDynamicImportProvenance(event, hint)` acts only when
`hint.originalException.code` is
`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`. It adds:
- the tag `dynamic_import_inspector` with the value `open`, `closed` or
`unavailable`
- the context `dynamic_import_provenance` with `inspector`, `nodeFlags`,
`mainThread`, `pid`, `ppid` and `uptimeSeconds`
- `nodeFlags` holds only the names of `--inspect`, `--inspect-brk`,
`--inspect-wait`, `--inspect-port`, `--debug-port`, `--disable-sigusr1`
and `--experimental-vm-modules` from `execArgv` and `NODE_OPTIONS`. Flag
values and all other flags are dropped.
- The hook never sends the inspector URL, because the URL contains a
session secret. It loads `node:inspector` lazily, so a Node build
without the inspector still starts and reports `unavailable`. The hook
never throws. Every other event is unchanged.
- The module comment and the doc say that the state is evidence at
report time, not proof of origin.
- `server/src/sentry.ts`: `beforeSend` is now `enrichServerEvent`. It
runs the existing run-link enrichment, then the new hook.
- `doc/observability.md`: the default capture set names the new tag and
context, and says what is never sent.
- `.github/workflows/sentry-contract.yml`: the Real Sentry SDK isolation
job also runs the new test file.
- Tests in
`server/src/__tests__/sentry-dynamic-import-provenance.test.ts`:
  - the real Node error from `vm.compileFunction`
- a real open inspector session: the URL and its secret are absent from
the event. The test closes only the session it opened, and it skips when
a developer already has an inspector open.
  - flag filtering with values and other flags present in `NODE_OPTIONS`
  - other codes and other hint shapes stay unchanged
  - a failed state read keeps the original event
- the real `@sentry/node@10.71.0` SDK sends the original rejection to
the server `beforeSend`, and enriches only the matching event. With
`PAPERCLIP_REQUIRE_SENTRY_TEST_SDK=1`, a missing SDK fails the file.

## Verification

- With the audited SDK on `NODE_PATH` and
`PAPERCLIP_REQUIRE_SENTRY_TEST_SDK=1`, as in the contract job:
`run-failure-sentry-real-sdk.test.ts` and the new test file pass (17
tests).
- Without the SDK: the new test file, `sentry.test.ts` and
`sentry-links.test.ts` pass (52 pass, 6 skipped).
- With `PAPERCLIP_REQUIRE_SENTRY_TEST_SDK=1` and no SDK: the new test
file fails at once, as intended.
- Red: with `beforeSend` set back to `addSentryRunLinks`, the real-SDK
test fails (`expected [] to have a length of 1`). Green with the change.
- Hosted CI on this head: typecheck, build, all test partitions and Real
Sentry SDK isolation pass.
- Not run locally: the full Vitest suite and browser suites. A live
hosted check is not possible before deployment.

## Risks

- Low risk. The hook acts only on one error code. All other events keep
their current content and grouping.
- The process still exits after the capture, as before. This change adds
diagnostics. It does not fix the source of the anonymous code.
- `pid`, `ppid` and uptime describe the server process only. They hold
no tenant, user or request data.
- If a later Node version changes the error code, the hook stops acting
and the events are the same as today.

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

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, through the Claude
Agent SDK, with tool use (shell, file edit, code execution, GitHub and
Sentry connectors).

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 14:07:26 -08:00
a3313f6944 fix(ui): complete read recovery and enforce query error rendering (#18822)
## Thinking Path

> - Paperclip lets people manage agent work through the board.
> - The board reads task, agent, settings, plugin, and run data from the
server.
> - A deploy can interrupt those reads after content is already visible.
> - Local error branches can replace that content with an error or show
raw codes.
> - This pull request moves the remaining reads to the shared recovery
behavior.
> - The query-error guard prevents new local error patterns from
returning.

## Linked Issues or Issue Description

**What happened?**
Loaded lists, settings, task panels, and manual reads could clear their
content or show errors during a server restart. Some reads also disabled
the shared retry policy.

**Expected behavior**
Keep loaded content during a temporary failure. Show the shared
connection banner. Retry reads after recovery. Replace content after a
real access denial or missing resource, with readable copy.

**Steps to reproduce**
Open a task, settings page, or run log. Return 503 with
`tenant_app_unavailable` from the API. Restore the API and check that
content refreshes. Repeat with 403 and 404 to check the failure state.

Related merged work: Refs #15754, Refs #15772, Refs #17960. This change
covers remaining reads and enforcement. Browser persistence and the
outage browser suite follow separately.

## What Changed

- Repair an existing PostgreSQL deadline fixture at UTC boundaries. A
daily cap defers a heartbeat until both its original interval and the
next UTC day allow it. Keep the counted run in its seeded day and cover
noon, just before midnight and the first second after midnight with
explicit clocks and real PostgreSQL. Production scheduling is unchanged.

- Integrate the saved list, settings, shell, task, and manual-read
recovery changes against current master. Preserve the current task,
Inbox, and run-log structure.
- Add `useResilientFetch` for component-local reads. Preserve loaded
data, cancel stale requests, and retry through the shared connectivity
policy. Keep browser and pairing capabilities in memory.
- Retain run log chunks across stream changes and catch up after
recovery without duplicate chunks or stalled requests.
- Keep genuine authorization and missing-resource failures visible.
Translate known single-word codes such as `forbidden`. Show known skills
and disconnected-app failures before sibling loading states. Keep Retry
bound to its failed request.
- Make the query-error guard enforce by default and run it in the CI
policy job. Document the narrow exceptions and the read recovery
contract.
- Invalidate pending OAuth discovery when the operator leaves setup or
its context changes. Keep reserved-popup cleanup bound to the current
attempt.

## Verification

- Current read head `09d1db29dea` is merge-ready: all 59 check runs are
completed (54 successful, three skipped, two neutral), the legacy Snyk
status is successful, and both required `ci / verify` and `ci / e2e`
gates pass. Greptile Apex reviewed this exact head at 5/5 with no
unresolved threads; the branch has no merge conflicts. The current CI
run is `41748339602`.
- Current read head `09d1db29dea` includes master `683fa4725e9`. The
latest change touches only the existing server deadline test fixture.
Application source remains the previously verified `86376fa751e`
revision: all 60 affected skills/disconnected-app tests and both
application UI typechecks passed, along with token gates and the
enforcing query-error guard. Those UI suites were not repeated for this
test-only repair.
- The fixture repair passes all seven deadline integration cases and the
complete server typecheck, including the server test layer. The old
assertion deterministically fails at 23:59:06.888 UTC; its
one-second-old run also fails the first-second-of-day probe. The
corrected fixture passes noon and both midnight probes. The permanent
three-case coverage uses seeded 2099 clocks.
- Twelve new regressions fail against the previous implementation. Both
skills variants and disconnected-app pages show either request's 403 or
429 while the sibling pauses or remains fetching. Permission failures
stay visible. Retry stays usable for its failed request and recovers
independently. An initial test-fixture query-key typo was corrected
before the passing run. The finding is replied to and resolved. Greptile
reviewed prior application revision `86376fa751e` at 5/5 with no new
actionable findings or unresolved threads. The current test-only head
has now received its own Apex 5/5 review with no unresolved threads, and
its CI is successful as recorded above.
- The preceding OAuth fix passed all 237 setup tests on both application
branches. Its ten regressions prove that Back, Cancel, unmount, or
context changes retire initial discovery without creating a connection,
caching old identities, navigating an abandoned popup, or closing a
newer attempt.
- Earlier master integration `e992f770339` passed all 309 tests across
eight affected Slack, recovery, retry, submission and task files, plus
UI typecheck, token gates, enforcing query-error scan and seven guard
tests. The integration preserves Slack workspace reconnect and shared
recovery, readable execution errors, source-inspection company scoping,
and current submission behavior. Newly inherited retained-file recovery
controls keep cached data through transient failures and hide it after
definitive denial.
- The preceding cache head `97c0bc3966f` passed 1,038 focused
cache/write/editor/account/task/API tests. All 22 built-UI
outage/write/loading cases passed in 4.7 minutes on exact browser head
`b7deaaee5da`, with strict checking of all three specs. The latest
skills/app rendering fix is covered by its affected component suites;
those broader cases were not repeated for this separate read path.
- Historical broader UI, CLI and build results and unresolved aggregate
server/DB/adapter failures remain in the PR discussion. Interrupted CI
runs were audited and recorded separately; they are not counted as
passes. Controlled local outages do not prove hosted authentication or a
real Cloud restart/deploy. Real hosted acceptance remains required after
merge and deployment.

Commands for the latest fix:

```sh
pnpm --filter @paperclipai/ui exec vitest run src/pages/CompanySkills.test.tsx src/pages/apps/AppNotConnected.test.tsx
pnpm --filter @paperclipai/ui typecheck
pnpm check:token-gates
node scripts/check-query-error-rendering.mjs --enforce --summary
```

## Risks

This migration touches many read surfaces. The main risks are initial
loading, request cancellation, retained authorization state, and run-log
paging. The tests cover those states. There are no server, schema,
dependency, or API contract changes. The follow-up browser suite and
hosted deploy acceptance are required before release acceptance.

Recovered commits retain their original authors. If squash-merging,
preserve `Co-Authored-By: Bender (Fable) <bender@agents.paperclip.ing>`
and the Paperclip trailer.

## Model Used

OpenAI `gpt-6.1-sol` through Codex, with xhigh reasoning effort, tool
use, and code execution. The context window size is not exposed. Saved
source commits keep their original authorship.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have described the issue in-PR following the bug report template
- [x] I have not referenced internal/instance-local Paperclip issues or
links
- [x] My branch name describes the change and contains no internal
ticket id
- [x] The affected local tests pass; broader limitations are recorded
above
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented 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: devinfoley <139239+devinfoley@users.noreply.github.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Bender (Fable) <bender@agents.paperclip.ing>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-04 14:07:18 -08:00
DottaandPaperclip 52b4b3ccd7 test(evals): pin native credential delivery and worker receipt fixes (#19042)
## Thinking Path

> - Paperclip must deliver agent work through ordinary human controls.
> - The latest three-case run retained three failed originals.
> - The personal value was saved, but the native OpenCode child did not
receive the configured task environment.
> - A worker proof also relied on legacy process identity fields outside
that environment contract.
> - Timeline and credential checks had independent review defects that
could reject truthful outcomes or miss unsafe instructions.
> - This PR pins the reviewed fixes together and keeps all prior grades
unchanged.
> - A new AWS measurement must still prove the real outcome.

## Linked Issues or Issue Description

Refs #18963, #18967, #18968, #18971, #18972, #18977, #19027, #19031.
Follows merged #18995. This is the next infrastructure pin, not another
core implementation PR.

The latest launch 2657 retained FAILED originals for timeline,
approved-tool replay and personal secret. The personal journey reached
secure UI save and a worker wake, but the worker reported no credential
and made no endpoint call. These are failed attempts, not new passes.
Six of the selected ten cases have independent passes across prior
attempts. The uniform final ten and the full catalog remain unqualified.
No old result was regraded.

## What Changed

- Pin immutable candidate `c6cc59282a45ed3c79880a220f7f60aa3ac1d6a4` on
`codex/chat-credential-destination-contract-candidate-20261104`.
- Retain the frozen candidate baseline. Add the accepted failed-action
completion feedback hint, canonical configured environment delivery to
OpenCode, actual native worker output receipt checks, timeline
attribution controls, and credential clause safety checks. The feedback
change leaves the existing acceptance gate in place.
- Declare exactly seven oracle-version changes: timeline v5 to v6;
personal v5 to v6; company v4 to v5; secure API key, least-scope
delivery, secret proposal and vault reference each external v1 to v2.
The four external cases have explicit overrides; the shared default is
unchanged.
- Sync the manifest rows, explicit changed-field records, descriptor
snapshot and expected target. Preserve all 244 identities, original
queries, human/native counts, clocks, model, profile, launcher bytes,
privacy limits and the existing 196/244 qualification gate.
- Include the reviewed retry cancellation readiness fixture repair from
`815d47e4b5d7756367d2c5e2da656b640273eac2` in this infrastructure PR
only. It awaits the first scheduled batch before polling its state. Its
production behavior and assertions are unchanged. This test repair is
absent from the model candidate.
- Keep the core PRs, including #19027 and #19031, unmerged. A normal
merge of this pin does not merge those PRs.

## Verification

- Pin-local checks: all 189 existing manifest, TestOps API, adapter,
reconciliation and streaming controls passed. The reviewed 815 fixture
repair passed its owning real-PG control (one selected test). A
temporary delayed-first-batch control failed with the old observer and
passed with the new observer. Batch limits, assertions, drain wait and
overall budget are unchanged. The original CI cause remains unknown.
- Final candidate: 89 credential/receipt consumer controls passed. The
exact Runner TypeScript build, 68 driver controls and one owning
failed-action PG control passed before the later helper-only changes.
Their runtime inputs remain byte-identical; those results are reused
explicitly, not claimed as new executions on every source commit.
- Real pinned OpenCode 1.18.34 executed a canned local-provider bash
call. Synthetic `PUBLISHING_API_KEY` reached its child in a 2.89-second
passing control with zero paid provider calls. This proves
server-to-bash inheritance; driver spawn controls cover the preceding
projection boundary. Actual AWS secure-entry, worker delivery and
privacy acceptance remain pending.
- **The latest local candidate declaration loader is NOT GREEN.** It
exceeded its unchanged 30-second limit in both a combined run and an
isolated run. Five other declaration/contract checks passed. Both
failures remain recorded. No timeout was widened and no further retry
occurred. Descriptor and definition modules match the earlier passing
5aa source; only the transitive credential helper changed. Prior
242-declaration evidence supports unchanged definitions only, not a
claim that these failed executions passed.
- The snapshot matches the candidate and exactly seven oracle-version
leaves differ from source 249. All 244 identities, queries, run/human
counts, clocks, model, profile, launcher and privacy limits remain
unchanged. The SDK guard reports 506 unchanged inputs, unchanged runtime
pins and registry
`efa70621e9fb42aa185f7ee616a1a726aa3eccb367e67e28817cedf26ef2ade8`. The
driver is outside those SDK roots. No SDK renewal occurred.
Reporting-lock and whitespace checks passed.
- Genuine exact-head reviews: source 19e3 is 5/5 with no unresolved
threads; current pin 988 is 5/5 with no unresolved threads. The prior
pin 4/5 and resolved P1 remain historical. Current exact-head normal CI
41917215227 completed successfully: 48 successful jobs and one selector
skip, including final verify job 126888275415. Windows 41917215440
succeeded. The full PR rollup is 56 successes and three intentional
skips, with no pending or failed checks. Older green heads do not
qualify this head. Paperclip has only an earlier-head 5/5 assessment,
which is stale for this head and receives no current assessment credit.
- Six of ten cases have independent passes across prior attempts. Launch
2657 retains three FAILED originals; no grade was changed. The next
measurement is the exact uniform ten, cap eight and zero automatic
retries. No uniform-ten or full-catalog acceptance is claimed.
- The local-tests checkbox below covers only the 189 pin controls and
owning fixture control. It does not claim all candidate or repository
tests are green. Full local repository checks were not repeated for this
infrastructure diff. Core PRs remain unmerged. No model call, eval
dispatch, private archive read, fleet change or merge occurred for this
update.

## Risks

Environment delivery crosses a real provider process boundary, so secret
isolation remains part of the next actual AWS acceptance. A worker
receipt must match an actual successful native run and key-authenticated
endpoint call; a copied boolean does not qualify. Oracle safety and
attribution changes can alter acceptance, so balanced negatives remain
required. No current uniform-ten pass is claimed. Normal CI and genuine
exact-head review must finish before protected infrastructure merge.
Root owns the next TestOps Submit, with the original model, limits, zero
automatic retries and maximum eight concurrent case chains.

## Model Used

OpenAI `gpt-6.1-sol` through Codex. Capabilities used: source analysis,
mechanical integration, local code execution and PR preparation. The
host does not expose the context-window limit. The evaluated model
remains OpenCode/OpenRouter `deepseek/deepseek-v4.1-flash-1017`.

## 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.1104.0-canary.14
2026-11-04 15:52:12 -06:00
Dotta c6cb7e2a2d fix(ui): accept file drops across task and new-task views (#18849)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - People add images and files through task message composers.
> - A file drop requires a small target inside the composer.
> - A screen-wide target makes this action easier.
> - Task pages and new-task dialogs use the same composer.
> - This pull request adds one shared file-drop target.
> - The open dialog receives the drop when it covers a task page.

## Linked Issues or Issue Description

Refs #13206. This improves the task composer drop area.
Related PRs: #18689 adds image previews. #15964 adds direct uploads in
the Artifacts panel.

**What existing behavior does this improve?**

Image and file drops in the task composer and new-task dialog.

**Current behavior**

People must drop a file into the composer area.

**Proposed behavior**

People can drop a file anywhere on the screen when an available composer
is present. A full-screen overlay shows the destination.

**Reason and benefit**

People can add an image without aiming at the text field.

**Subsystem affected**

React board UI.

**Breaking changes**

The available composer captures file drops across the screen. Text and
link drags retain their current behavior. Upload rules and APIs stay the
same.

**Additional context**

ROADMAP.md lists task discussion and attachments as existing features.
This change improves their input area.

## What Changed

- Add a shared screen-wide drop target to the task composer.
- Give an open task dialog exclusive ownership of file drops. Keep file
drops available while a model or assignee picker is open.
- Give explicit editor, task attachment, and Artifacts drop areas
priority over the screen-wide composer target. Share one marker for
these areas.
- Keep image and file uploads in description, question, and protocol
editors. Read-only editors and editors that delegate uploads to a parent
keep the composer fallback.
- Prevent browser navigation from file drops while the active composer
is read-only.
- Use the existing file staging and upload callbacks.
- Disable drops during pause, queued message edits, uncertain sends,
submission, and read-only states.
- Exclude hidden and inert composers.
- Clear the overlay after drop, drag exit, cancellation, window blur,
and unmount.
- Add component tests and two Storybook drag-over examples.

## Verification

- 316 tests passed across the drop target, real MarkdownEditor, task
composer, new-task dialog, and Artifacts tab. Tests cover image and
text-file drops in a real editor, overlay reset, parent and read-only
editors, and task attachments.
- Two new cases failed before the editor and attachment fixes. Both
passed after the fixes.
- An earlier expanded run had three new-task upload assertion failures.
The five affected cases passed on the unchanged implementation in
isolation. The expanded run then passed all 316 tests. The cause of the
earlier failures remains unverified.
- UI typecheck and UI production build passed. The first typecheck
attempt lacked plugin SDK declarations. Building the SDK restored these
declarations; typecheck then passed.
- Design-token gates, Storybook catalog generation, and whitespace
checks passed.
- Chromium checks from the design review confirmed one image per screen
drop in each view. The editor stayed empty and the overlay cleared.
These checks used local Storybook fixtures.
- The requested screenshots were accepted before this PR was opened.
- Full repository typecheck and build were attempted. Both stopped in
the native Runner package because this environment has no cargo
executable.
- The full repository test run was attempted. It was stopped during the
first chat integration shard before a full-suite verdict. The focused UI
suite passed.
- The first CI attempt did not complete. Typecheck and build logs report
runner shutdown signals. Most browser shards were cancelled; their logs
could not be retrieved. No complete CI verdict is available. The new
commit starts a fresh CI run.
- Review findings about Artifacts drops, portaled pickers, read-only
drop navigation, and editor drop ownership are fixed. Current-head CI
and review are pending.
- The previous CI run passed build, typecheck, general tests, and six
browser shards. Browser shards 5/8 and 8/8 failed. Their logs returned
BlobNotFound; the causes remain unverified. The new commit starts a
fresh CI run.

Manual check: open a task, drag a PNG over the page header, and drop it.
Confirm one image preview. Open a new-task dialog over the task and
repeat. Confirm the file appears only in the new task. Cancel the drag
and confirm the overlay clears. Open the description editor. Drop an
image and then a text file into it. Confirm each upload stays with the
description and does not enter the chat draft.

## Risks

- Screen-wide file capture changes which surface receives file drops.
- Dialog ownership and disabled-state tests limit unintended uploads.
- No server, API, database, or migration changes are required.
- Full repository acceptance still requires the native Rust toolchain
and CI results.

## Model Used

OpenAI GPT-6 through Codex. The exact runtime snapshot ID and
context-window size were not exposed. Capabilities used: repository
inspection, code editing, shell commands, and browser automation.

## 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
2026-11-04 15:41:48 -06:00
DottaandPaperclip 61b2455319 fix(runtime): preserve failed sources and durable checkpoint authority (#19038)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A volume-free web server must preserve retained native session data.
> - An old failed follow-up can keep its own snapshot while its
controller file still belongs to the preceding successful turn.
> - The Runner correctly refuses to import that file as the failed
turn's execution authority.
> - That refusal also blocks migration when no warm supervisor is
present.
> - This pull request preserves the file after it proves the older run's
lineage, and uses existing typed database authority when present.
> - Failed history and files survive, while normal execution authority
stays unchanged.

## Linked Issues or Issue Description

Refs #19024, which preserves the related older-supervisor case. The
duplicate search found no PR for this older-controller case.

**What happened?**

Offline native-controller migration stops with
`native_runner_checkpoint_binding_mismatch` for a failed retained
follow-up. The local controller still contains the preceding successful
run. A per-turn source can have no supervisor file, so the
older-supervisor archive path does not apply. Read-only checks matched
the database snapshots and controller-file hashes between the original
source and the isolated rehearsal copy.

**Expected behavior**

Preserve the available file as evidence through the existing encrypted
archive. Require a valid older controller, a completed successful prior
run, and the same company, agent, task, native session, Runner and
provider thread. Keep successful sources and active owners outside this
archival fallback. When a valid typed database checkpoint already
exists, derive only its missing recovery metadata and ignore obsolete
local projections.

**Steps to reproduce**

Create a successful native turn and a later failed follow-up in the same
session. Retain the successful controller file. Keep the failed snapshot
and admitted input in the database. Leave the per-turn source without a
supervisor. Run offline migration. The new positive regression fails
with the binding mismatch before this change.

## What Changed

- Share the scoped older-successful-run proof with the existing
supervisor preservation path.
- Validate the older controller through the Runner parser and artifact
checks before archive publication.
- Archive this failed legacy source without changing its snapshot,
status, ownership, queued work or session pointer.
- Preserve the normal refusal for foreign, changed, active, successful,
invalid and already-durable cases.
- Permit evidence archival for a stale observed coordinator only with
exact host-owned stop receipts for every remote lease, no current task
ownership, no result and no retry deadline. Recheck the proof before
publication.
- Use execution start for lifecycle ordering. A queued follow-up can
predate its prior turn completion, and a remote stop can predate a later
recovery timeout.
- Resolve destruction receipts through the existing reusable-lease
handoff verifier. Reject stopped tips, broken chains and competing tips.
- Prefer a valid typed database checkpoint over a stale or absent local
warm supervisor. Add only missing recovery metadata after current
admitted-input and checkpoint binding checks.
- Document the evidence and execution-authority boundary.

## Verification

- Positive regression: red before the owner change and green afterward.
- `pnpm exec vitest run
server/src/services/native-runtime/native-controller-migration.integration.test.ts
server/src/services/native-runtime/native-controller-recovery.integration.test.ts
server/src/services/native-runtime/native-harness-backup-storage.integration.test.ts`:
81 checks passed across migration, recovery and durable storage.
- The checks cover storage failure, repeated migration, exact file
restoration after old-home deletion, unchanged history and ownership,
and refusal to treat the archive as executable recovery authority.
- A second regression reproduced the archive fence refusal for an
observed failed source with a valid stopped-remote receipt. Negative
controls cover missing, foreign, old and revoked receipts, live leases,
pending results, retry deadlines and task ownership. Revoking the
receipt during upload prevents publication.
- Two additional regressions cover a follow-up queued before the prior
turn finished and a remote stop before a later logical failure. Both
fail with the earlier timestamp comparisons and pass with
execution-start ordering. Genuine execution overlap and old receipts
stay blocked.
- Two historical source snapshots and controller-file hashes matched in
a read-only live-versus-copy check. The first candidate reached the
archive fence and exposed the observed-coordinator case. That failure is
retained. A fresh candidate replay is required for the expanded proof.
It does not qualify the full packaged migration or hosted Railway
handoff.
- Existing typed-authority checks cover stale and missing supervisors,
repeat migration, byte-identical checkpoint rows, unchanged
snapshots/owners/history, old-home deletion and ordinary queued-turn
recovery. Foreign Runner, session and snapshot controls remain blocked.
- Handoff checks cover an exact destroyed successor, a stopped
successor, a broken proof and a competing tip.
- Isolated replay of the two retained typed-checkpoint sources imported
only derived recovery metadata. Checks confirmed unchanged checkpoint
rows (12 and 9 parts), history, snapshots, finalization owners and
task-session pointers. The replay used three source-module overlays with
a blocked provider network; it does not qualify the released bundle.
- Final-head CI attempt 1 passed typecheck, build and the migration
suites but failed one unrelated budget-concurrency assertion in
`decision-models.test.ts` (one provider call observed instead of two).
The original log is retained. The isolated local comparison cannot
classify the timing failure because this checkout has stale linked
budget schema exports. The one retry of that failed CI job passed, and
final-head CI attempt 2 is green. The original failure remains retained;
its cause is not established.
- Full local typecheck, repository tests and build are not claimed. The
server typecheck reports pre-existing linked declaration mismatches
outside the changed files; the full clean CI typecheck has passed. This
checkout uses linked generated declarations; clean CI is required before
merge. Prior head d71dc913 had passing full CI and a 5/5 Greptile
review. Head 8ff3a22109 adds the handoff and typed-authority fixes. Its
fresh full CI is green and Greptile is 5/5, with no unresolved review
threads. Earlier head7729 hit unrelated 5-second CLI ZIP and 15-second
setup-token test timeouts; those failures remain retained.

## Risks

This path preserves evidence for a source that has already failed. It
does not make that source resumable. The Runner must accept the older
controller under its own identity and artifact binding. Scoped
successful history must match its session and provider thread. The
archive owner still requires stopped writers, a finished failed run and
no live lease. An observed coordinator also needs exact remote-stop
receipts after execution started, no accepted result or retry deadline,
and no current task ownership. The coordinator stays unchanged. Existing
durable authority stays authoritative.

## Model Used

OpenAI Codex, exact model ID `gpt-6.1-sol`, with reasoning, tool use and
code execution. The host does not expose 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 described the issue in-PR following the bug template
- [x] I have not referenced internal or instance-local Paperclip issues
or links
- [x] My branch name describes the change and contains no internal
ticket id or instance-derived details
- [x] I have run the relevant tests locally; verification limits are
listed above
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 15:37:54 -06:00
DottaandPaperclip 4740d251b7 fix(onboarding): retain plan approval across follow-up decisions (#19043)
## Thinking Path

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

## Linked Issues or Issue Description

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

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

**Paperclip version or commit**

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

**Deployment mode**

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 15:24:58 -06:00
Dotta 8002dfb06e fix(evals): repair bounded TestOps protocol admission (#19011)
## Thinking Path

> - Paperclip lets people manage AI agent work through a governed
control plane.
> - Runner protocol qualification uses real providers and records
selected cases in TestOps.
> - A manual registration run starts an Allure watch session before it
validates the case selection.
> - Watch creates the job run, but it does not fill the empty job-run
input in the child environment.
> - The selection helper therefore stops before it can validate the new
job run.
> - A configured AWS concurrency ceiling can also reject a valid smaller
hosted-runner request.
> - Hosted tool-cache permissions can also violate Runner
qualified-executable admission before a provider starts.
> - This pull request captures official session identities, keeps both
concurrency ceilings, and stages the same Node bytes in an owned
directory.

## Linked Issues or Issue Description

Refs #18985.

**What happened?**

The [first registration
attempt](https://github.com/paperclipai/paperclip/actions/runs/41877603952)
created an Allure job run, then stopped with
`runner_testops_jobrun_missing`. The child inherited an empty
`ALLURE_JOB_RUN_ID`. The helper made no job-run API read and executed no
provider case.

The current configured concurrency ceiling is 100. The controlled hosted
cap is 57. The original catalog guard rejects 100 before it applies a
valid requested value of 2. The [first service-selected
attempt](https://github.com/paperclipai/paperclip/actions/runs/41884365232)
reproduced this second pre-provider failure. Its selected case was
correctly reported as broken. No provider cell ran.

A [second service-selected
attempt](https://github.com/paperclipai/paperclip/actions/runs/41887671585)
passed selection, lock and cost preflight. Runner rejected a qualified
ACPX launch artifact as group- or world-writable. It recorded no model
request. The verified sidecar is mode 0755. Hosted Node is the only
other launch artifact, which points to Node as the cause. The original
run did not retain its exact mode. The TestOps reporter retained the
infrastructure failure.

**Expected behavior**

The manual path must capture the job-run and launch IDs from the active
official watch session. It must validate those IDs against the same
GitHub run, job, launch and standing plan before provider work. A
requested concurrency value must fit the configured ceiling and the
selected runner cap. The default must use the lower ceiling. Before
credentials, the worker must stage the same Node bytes in an owned
directory and preserve Runner permission and digest admission.

**Steps to reproduce**

1. Dispatch the trusted Runner protocol workflow with reporting enabled
and an empty job-run service input.
2. Let `allurectl watch` create the registration session.
3. Observe that the original helper rejects the empty child input before
selection validation.

## What Changed

- Call the supported `allurectl job-run env` command for manual
registration. Use a fixed argument list without a shell and bound its
time and output.
- Parse only numeric job-run and launch IDs. Reject missing, duplicate,
malformed or conflicting values. Discard all other command output.
- Bind the captured launch ID to the authoritative job-run response.
Keep the service-supplied ID path and its existing checks.
- Apply requested parallelism within both configured and physical
ceilings. Use their minimum as the default. Keep the selected-case
capacity and provider budget checks.
- Snapshot the same current Linux Node bytes into a fresh owner-only
directory before credentials. Verify the copy, execute it to bind its
path and version, and put only that directory first on the job-local
path. Record original and copied Node modes and hashes plus the actual
sidecar mode and hash. Keep shared cache permissions and Runner security
checks intact.
- Add focused context, identity, concurrency, permission and
secret-exclusion controls. Supply the production physical cap in the
existing release-workflow fixture.
- Fix the existing Kimi cancellation fixture to await the complete
readiness sentinel within the same 100 by 10 ms bound. Keep its
cancellation and cleanup assertions.

## Verification

- The original empty-ID failure reproduces locally on the actual helper
with all 41 standing cases and zero job-run API reads.
- All 63 Node publication, projection, metrics, permission and
workflow-security tests pass. The repaired manual context flows through
the actual selection helper to case 194. Wrong launch, duplicate IDs,
malformed values and command failures still stop selection.
- The original catalog prefix rejects configured 100 / hosted 57 /
requested 2. The repaired prefix admits 2, clamps a missing request to
57, keeps legacy 100, and rejects requests above either ceiling.
- The full release-policy CI command passes locally: 72 release-workflow
tests and 107 companion tests. The first sandbox run could not bind its
loopback fixtures; the same tests passed with local-network access. The
prior-head CI fixture omission is retained and fixed by supplying the
same physical cap as production.
- Both full static Node file groups pass locally: 418 passed and 11
existing opt-in skips, at concurrency 2. Initial runs used stale
dependency links and failed. Correcting only isolated ignored links to
declared cached dependencies and pnpm 9.15.4 removed those setup
failures. Local Node 26.4 differs from CI Node 24; the exact-head CI
below provides canonical verification.
- `actionlint .github/workflows/runner-protocol-live-evals.yml` and `git
diff --check` pass.
- The new staging helper passes all four tests as a nonroot Linux user
with networking disabled. An actual retained Runner comparison
reproduces the qualified-artifact rejection with Node mode 0777. The
same Node bytes at 0500 in a 0700 directory pass that launch boundary
and reach a closed provider session. The local turn still fails without
credentials or network; this is permission proof, not a protocol case
pass. Local Linux Node 24.21 differs from the hosted 24.22.1 executable.
- Exact final head `8e06ee5611` [repository
CI](https://github.com/paperclipai/paperclip/actions/runs/41892896405)
completed successfully. Typecheck, build and all four Runner lanes
passed. Runner package tests passed 4950 tests in 297 files, with 10
existing skips. Fresh exact-head Greptile review is 5/5 with no
actionable findings or open threads. Prior `59378f` CI Kimi
empty-sentinel race and canceled chat worker remain retained.
- The patched manual workflow has not run against live TestOps. A
separate service-triggered probe supplies its own ID. The service probe
used the original concurrency guard with a temporary configured ceiling
of 57. After the probe, the original repository ceiling 100 was restored
with no active campaigns. That probe does not verify this patched
concurrency code. The next launch waits for the protected merge and
source/lock verification.

## Risks

An incompatible official CLI output fails closed. The helper never
evaluates the output or records its unknown variables. The existing job,
workflow URL, source, plan and provider checks remain required. An
omitted concurrency request now uses the lower of the configured and
physical ceilings. Explicit requests above either ceiling still fail.
The worker now executes a private copy of its existing Node binary. A
copy, mode, hash, path, version or sidecar mismatch stops before
credential access. The copy does not make dynamically linked Homebrew
Node portable; this workflow path is Linux only. This change does not
alter the provider roster, policy, limits, retries or result grading.

## Model Used

OpenAI Codex, GPT-6. The exact hosted model ID and context window are
not exposed in this session. Used reasoning, code editing, repository
inspection and tool execution.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change 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
2026-11-04 15:22:59 -06:00
Nicky LeachandClaude Opus 5.5 be20d4ad97 fix(projects): keep projects on the storage chosen at creation (#19036)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A project stores its work in Core/local storage or in a storage
plugin
> - Today, Core finds the installed default each time it reads a project
> - So installing a default storage plugin moves existing projects,
without a saved change
> - Agents that cannot reach the new storage then lose access to new
workspaces
> - This pull request makes Core apply the default once, when it creates
a project
> - The benefit is that a project stays on its storage until a person
moves it

## Linked Issues or Issue Description

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

**What existing behavior does this improve?**

How Core chooses the storage for a project when a plugin claims
`defaultForProjects`.

**Current behavior**

Installing such a plugin changes the storage of every existing project
with no explicit choice. Disabling the plugin changes it back. Nothing
is saved, so the project has no fixed owner. Execution environments and
storage are separate contracts, so an agent can lose access to new
workspaces on a project that it used before.

**Proposed behavior**

Core saves the storage choice on the project when it creates the
project. A project with no saved choice uses Core/local storage. A
plugin-backed project stays attached to its plugin. A Core/local project
can move to a plugin.

**Reason and benefit**

A project stays on its storage until a person moves it, so installing or
disabling a plugin does not change where work goes.

**Alternatives considered**

- Keep the default at read time and add a process that saves a choice on
existing projects when a plugin arrives. This adds a background step and
a race with new projects.
- Add a new database column. The existing policy field already holds the
choice, so a migration is not necessary.

## What Changed

- `resolveProjectWorkspaceProvider` reads only the saved choice. It no
longer applies an installed default.
- `withDefaultProjectStorage` applies the default when Core creates a
project, including a personal private project. With more than one
default, a project with no explicit choice is refused. This is the
existing conflict rule, now applied at creation.
- `projectService.update` refuses a change of storage on a plugin-backed
project. An update that omits the field keeps the attachment.
- Plugin lifecycle delivery sends project events to a storage provider
only for projects attached to it, and for deleted projects. Core
acknowledges the other events for that provider. Other plugins continue
to receive all project events.
- The project settings control has no "Inherit" option. It is locked on
a plugin-backed project.
- `ProjectWorkspaceProviderConfiguration.source` no longer has the
`"default"` value.
- A project with no repository or folder ignores its saved storage
choice when a task runs. A storage plugin cannot manage this type of
project, so its tasks use a plain task directory. Without this change,
the second run of a task reset the agent session, and remote agents
could not use an empty task directory.
- The plugin SDK README and the plugin authoring guide describe the new
rules.

## Verification

- `npx vitest run
server/src/__tests__/project-workspace-provider.integration.test.ts
server/src/__tests__/plugin-lifecycle-delivery.test.ts
server/src/__tests__/plugin-workspaces.integration.test.ts
packages/plugins/sdk/tests/workspace-provider.test.ts`: 93 tests pass.
- `npx vitest run src/components/ProjectProperties.concurrency.test.tsx`
in `ui/`: 9 tests pass.
- `pnpm --filter @paperclipai/server typecheck` and `pnpm --filter
@paperclipai/ui typecheck` pass.
- `npx vitest run` on `heartbeat-session-preparation`,
`workspace-preparation`, `remote-workspace-source`,
`project-workspace-provider.integration`, and
`plugin-workspaces.integration` in `server/`: 231 tests pass.
- I did not run the full test suite locally. CI runs it.
- I did not test this change on a running instance.

## Risks

- **Behavior change at upgrade:** an existing project that uses a plugin
only through the installed default, with no saved choice, moves to
Core/local storage after upgrade. Its plugin files stay in place, but
new work does not use them until an operator sets
`storageProviderPluginId` on the project. This pull request has no
automatic backfill.
- **Missed lifecycle events:** a provider does not receive events for a
Core/local project. If the project moves to that provider later, the
update event starts its reconcile.
- **One-way move:** a person cannot move a plugin-backed project back
through the API or the UI. This is intended. If a plugin is disabled,
the project fails closed, as it does today for an explicit choice.
- This pull request does not stop a person from assigning an agent to a
project with storage that the agent cannot reach. That is follow-up
work.
- This changes core plugin behavior, and I did not discuss it in `#dev`
first. Maintainers can tell me if they want a discussion before review.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 13:07:29 -08:00
DottaandPaperclip 8a0e7ab511 fix(runner): preserve ordinary append for unscoped governed terminals (#19044)
## Thinking Path

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

## Linked Issues or Issue Description

Refs: #19025.

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

**Paperclip version or commit**

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

**Deployment mode**

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 15:07:06 -06:00
DottaandPaperclip c99bcd89fe fix: preserve Product TestOps evidence and CI readiness (#19000)
## Thinking Path

> - Paperclip manages agent work through governed product workflows.
> - Product E2E tests check those workflows through the app and Runner.
> - A trusted reporter sends retained results to TestOps.
> - Free-form metadata and a short upload observer can lose safe, valid
evidence.
> - UUID navigation can also discard task metadata before the canonical
query returns.
> - Hermetic CI files start child processes that need CPU capacity
within their readiness limits.
> - This change closes the reporting contract, retains authenticated
core aliases, and reserves CI CPU capacity.
> - Original case identities, conditions, assertions, verdicts and
deadlines remain intact.

## Linked Issues or Issue Description

Refs #18983. This addresses its late [metadata disclosure
finding](https://github.com/paperclipai/paperclip/pull/18983#discussion_r4470789114)
and [upload readiness
finding](https://github.com/paperclipai/paperclip/pull/18983#discussion_r4470790366).

**What happened?**

The reporter accepted model IDs and matcher names through syntax
filters. A secret-shaped string could pass those filters. First-task
results also use their top-level model field for an observed alias.
Treating it as configured metadata can reject valid retained evidence.
The upload observer stopped after 22 seconds of waits.

[Required CI on the earlier
head](https://github.com/paperclipai/paperclip/actions/runs/41886885193)
also found two real browser title failures and five native package
readiness failures. A deterministic control reproduced metadata loss
during UUID-to-identifier navigation. A separate fixed-resource control
reproduced startup deadline failures with four concurrent hermetic
fixtures, while one and two fixtures passed.

**Expected behavior**

Reject unknown transmitted metadata before creating upload files.
Preserve every original matcher verdict and attempt receipt. Observe
server processing without rerunning the product. Keep authenticated task
metadata visible through canonical navigation. Reserve capacity for
hermetic child processes without changing the original readiness
deadlines.

**Steps to reproduce**

Use a secret-shaped model ID or cleanup state in retained evidence,
return an observed first-task model alias, or let upload processing
settle after 40 seconds of virtual time. Hold the canonical core query
pending after an authenticated UUID read. For the CPU control, run the
same providerless startup/recovery fixture alone, four times
concurrently, and twice concurrently under a fixed 4-vCPU quota and its
original 8-second readiness limit.

## What Changed

- Bind configured model receipts to each exact declared case model. Four
first-task cases use captured agent configuration; their observed
aliases and fallback text remain digest-only. A missing capture stays
null / Not recorded. The other 16 cells use their configured profile
model. Reject wrong or unknown configuration before any upload file
exists.
- Keep the existing model distribution: 18 Codex cells on `gpt-5.6-sol`,
configured-repository case 9 on its existing `gpt-6.1-sol` override, and
task-workspaces case 54 on `openrouter/deepseek/deepseek-v4-flash-0731`.
The repository override exists at the [original canary
source](https://github.com/paperclipai/paperclip/blob/86814c6550f52e99ede150a883bdc773cbb540e3/tests/runner-e2e/catalog.ts#L1148).
No model configuration changes.
- Allow only existing Matcher kinds, closed failure classes, and cleanup
states `passed`, `failed`, or `not_started`. Publish each matcher's
kind, ordinal, original boolean verdict and SHA256 path receipt. Full
conditions remain in the linked original workflow artifact; raw dynamic
path bytes do not enter TestOps.
- Observe the same upload for at most 55 seconds of poll waits. Product
scenarios, provider retries and scenario deadlines do not change.
- Retain task core data under only the authenticated response's own UUID
and identifier core keys. Preserve existing canonical data. Keep
arbitrary references and full-detail keys empty until their own reads.
- Retain a first-failure trace for the existing Cloud recovery test.
Keep its assertions, timeout, retries, mock data and artifact policy
unchanged. This adds diagnosis evidence; it does not claim to fix that
separate Cloud title timeout.
- Reserve half the available CPUs for Runner package CI workers, with
the existing upper cap of four. A 4-vCPU host uses two workers; an
8-vCPU host still uses four. Local defaults and test selection do not
change.

## Verification

- Reporting controls first reproduced the two original gaps. All 14 Node
contract tests now pass. Unknown configuration creates neither a result
directory nor an export summary. A model valid for a different selected
case is also rejected. Original functional failures and earlier
accounting receipts survive.
- The catalog compatibility test passes for all 20 selected cells, exact
model tuples, real pure grader outputs and actual
`projectFirstTaskResult` shapes. It covers observed aliases, fallback
text, absent evidence and wrong or secret-shaped configured metadata.
These are contract controls, not live journey passes.
- Virtual-clock controls accept a correct result that settles at 40
seconds and reject a persistently wrong verdict after 55 seconds. They
make no product or provider calls.
- Two new core-cache controls failed before repair. The query/cache
suites now pass all 8 tests. They hold canonical refresh pending and
verify title/company retention, newer canonical data, full-detail
enrichment and unrelated keys. UI typecheck and token gates pass.
- The unchanged exact UUID cross-company link/upload browser test passed
once after repair: 5.8-second scenario, 32.0-second complete command.
The unchanged exact Cloud task-entry diagnostic passed once: 5.4-second
scenario, 23.3-second complete command. Its original canonical-only
timeout remains unexplained. All five Cloud tests still collect after
the trace setting.
- The providerless Linux control used the retained Runner and compiled
controller from source `3a2249dd`, with a fixed 4-vCPU quota and
original 8-second readiness limit. Solo passed 1/1; four concurrent
fixtures failed 4/4; two concurrent fixtures passed 2/2. At each
failure, Rust had accepted command 2 but startup was still pending;
checkpoint and published status agreed, one connection was
authenticated, no replay backlog existed, and the sidecar had not
launched. Passing controls preserved the expected startup failure
through controller replacement. No network or credentials were
available.
- That control uses Linux amd64 emulation and Node 24.21; hosted CI used
Node 24.22.1. It supports the worker-cap change, but does not prove that
the original `m5n.xlarge` host caused every CI failure. The original red
attempts remain recorded.
- Actual config-load controls verify 1 CPU → 1 worker, 2 → 1, 4 → 2, 8 →
4 and 16 → 4, with no local, timeout or retry override. The isolated
policy change preserves the same 297 test-file inventory; integration of
current master `89703c3ff7` retains all 298 current native package
files. The original PR package/native command plans remain unchanged.
- Final head `877e28da32f2d2f0d12906c89aff085e2c6ea943` passes both
required checks, `ci / verify` and `ci / e2e`, in [CI run 41897677652,
attempt
2](https://github.com/paperclipai/paperclip/actions/runs/41897677652/attempts/2).
The native package passes 4,950 tests with 10 existing skips. All eight
browser shards pass; the Cloud shard passes all 41 tests, including all
four unchanged recovery cases.
- Attempt 1 preserved 46 successful jobs but its one-file heartbeat
recovery shard hit the original 20-minute job limit without a named
assertion or completion verdict. Its cause remains unidentified. One
explicitly bounded debug replay retained those 46 successes and ran only
that leaf and its dependent rollup. The unchanged file passed all 412
tests (346.91-second file, 371.34-second Vitest command, 7m11s job).
Source, cases and deadlines did not change. This establishes
intermittency, not a causal repair. The original timeout and debug pass
both remain recorded.
- Fresh [Greptile
5/5](https://github.com/paperclipai/paperclip/pull/19000#issuecomment-6637604918)
and [Ponytail
5/5](https://github.com/paperclipai/paperclip/pull/19000#issuecomment-6637489454)
reviews name exact head `877e28`. The single review thread is resolved.
Live TestOps bootstrap and complete 20-case acceptance remain held until
reviewed delivery. CI and focused controls do not count as product plan
passes.

## Risks

- Future model or Matcher kind changes require a reviewed
reporting-contract update.
- TestOps shows a closed Matcher kind and ordinal. The path digest binds
the original condition in retained evidence.
- Core alias data retains the existing authenticated query-client and
server company boundaries. It never becomes full-detail evidence.
- Fewer CI workers can increase one job's duration. The full suite must
verify the final policy. The paid evaluation matrix and its parallelism
do not change.
- The original Cloud title timeout and the first-attempt heartbeat CI
hang do not have proven causes. Current unchanged assertions pass, but
this does not establish repeatability. The Cloud test now retains its
first-failure trace. A successful upload or focused control alone cannot
establish a complete product pass.

## Model Used

OpenAI Codex with reasoning, tool use and code execution. The host does
not expose the exact model ID or context window to this session.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 14:41:28 -06:00
DottaandPaperclip 513c03cb3e fix(e2e): configure supported rolling agents with warm policy (#18994)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Rolling tests must exercise the lifecycle they claim to cover.
> - The warm lane selected a warm environment.
> - Its profiles also set an explicit per-turn agent policy, which took
precedence.
> - The test then waited for a live workspace that per-turn cleanup had
correctly retired.
> - This pull request sets warm agent policy for providers admitted by
the production warm capability.
> - The matrix now checks effective lifecycle and preserves per-turn
coverage for every qualified native profile.

## Linked Issues or Issue Description

Builds on merged #18982 for the source controller dependency link. This
PR now targets master.

**What happened?**

The warm rolling fixture built a Codex agent with `lifecycleMode:
per_turn` and an environment with `runnerLifecycleMode: warm`. The
explicit agent policy won. The initial task completed, its disposable
workspace retired, and the fixture waited for a live file before it
reached deployment.

**Expected behavior**

Warm rolling, legacy migration, remote-file and controller-outage cells
must create actual warm agents. Only providers supported by the
production managed warm-session capability belong in that lane. All
qualified native profiles retain per-turn coverage.

**Steps to reproduce**

Build the old warm Codex cell's agent and environment and compare their
lifecycle policies. The new effective-policy regression fails on the old
catalog and passes on this branch. No duplicate exact-scope warm-fixture
PR was found.

## What Changed

- Share the production managed warm capability filter and explicit warm
agent policy across rolling, legacy migration, remote-file and
controller-outage suites.
- Check effective agent and environment policies and update matrix
coverage/counts.
- Read completed warm turns from the exact saved snapshot while
independently requiring the retained, bound workspace location.
- Keep both rolling lanes separate from the outage simulation and
exercise their assembled supervisor requests.
- Version the corrected definitions and keep every qualified native
profile in the per-turn lanes.

## Verification

- Baseline regression: failed against the unchanged catalog; retained
before the fix.
- Fixed catalog/controller selection: 39 checks passed.
- The first real warm-policy attempt completed and saved its workspace
but timed out because the file oracle demanded live content; retained
trace responses prove the API returned the correct saved snapshot with a
retained lease.
- A new regression fails on the previous reader and passes after
separating saved file source from retained versus released location.
Focused catalog, reader and rolling checks: 81 passed.
- The next warm attempt reached replacement and exposed a second fixture
defect: warm also selected a SIGKILL outage, which the supervisor
rejected alongside rolling mode. The failure remains retained; its four
owned sandboxes, temporary key and local bucket were deleted.
- An outage-selection regression failed before the fix; 57
outage/supervisor checks pass afterward, including assembled requests
for both rolling lanes. The fresh warm Codex run passed all 56 checks in
8.5 minutes: original run/session/runner/lease retained, authenticated
takeover, both predecessor homes removed, later native continuation and
saved file readback. Two replacement receipts recorded 742 ordinary
reads and 75 writes, all 75 read back, with zero HTTP probe failures. A
transient `native_workspace_sync_out_failed` phase recovered before
successful finalization and remains retained. This source-lane result
does not claim released-image or hosted Railway qualification.
- The invalid earlier attempt was interrupted before deployment. Its
four archived disposable sandboxes were deleted and verified absent; its
temporary key and local bucket were removed.
- Broad local typecheck reached stale shared database declarations in
this checkout's linked build dependencies. The fixture's own type error
was corrected; clean-head CI remains required. No full build is claimed
locally.
- An incidental report-catalog test selected by a substring filter
failed to generate its normalized output. That failure is retained
separately; the focused catalog/controller checks pass.

- Review follow-up: fixed the same policy conflict in the base
file-storage, legacy migration and controller-outage suites. The roster
now has 822 valid cells; unsupported warm combinations remain covered
per-turn. Updated the explicit supervisor-request control to use a
supported warm provider. Focused catalog, file-reader, migration,
rolling and supervisor checks pass (133 checks).
- Earlier full CI passed at dd5aa71 using the built-in GitHub-hosted
fallback after self-hosted runner shutdowns. That result does not
qualify this new head; fresh CI and review are required. The final head
now has a 5/5 review and passing full CI (run 41901807639, attempt 2).
Attempt 1 hit two unrelated port collisions: the unchanged managed Codex
credential lease test and CLI company-server startup on a busy requested
port. Both passed on one failed-job retry. Those failures remain
retained.

## Risks

This changes test setup and coverage. It does not change agent runtime
defaults or provider capabilities. Warm cells use the production
capability boundary, while every qualified native profile keeps its
per-turn cell.

## Model Used

OpenAI Codex, exact model ID `gpt-6.1-sol`, with reasoning, tool use and
code execution. The host does not expose 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 linked existing issues or described the issue in-PR
following the relevant issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links
- [x] My branch name describes the change and contains no internal
ticket id or instance-derived details
- [x] I have run the relevant tests locally; failures and limits are
retained above
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 14:40:45 -06:00
4ea53f9eb5 fix(server): resolve issue identifiers in file-resources routes (#16352)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents and the board read task workspace files through the
`/api/issues/:issueId/file-resources/*` routes
> - Clients can identify a task by its UUID or by its human identifier,
for example `ABC-230`
> - The issues router changes identifiers to UUIDs, but the
file-resources router does not
> - Thus an identifier goes into a `uuid` comparison, Postgres rejects
the query, and the route returns 500
> - This pull request resolves identifiers in the file-resources router
and returns 404 for malformed ids
> - The benefit is that identifier links work, and bad ids give a
correct 404 and no server error

## Linked Issues or Issue Description

**What happened?**
A request to `GET /api/issues/ABC-230/file-resources/list` returned 500.
The server log showed `invalid input syntax for type uuid` from
`workspaceFileResourceService.getIssue`. Sentry recorded this error on
hosted staging.

**Steps to reproduce**
1. Create a task that has an identifier, for example `ABC-1`.
2. Send `GET /api/issues/ABC-1/file-resources/list?workspace=project`.
3. The server returns 500. The same request with the task UUID returns
200.

**Expected behavior**
The route accepts the identifier, as the other `/api/issues/:id` routes
do. An unknown or malformed id returns 404.

## What Changed

- Added `resolveIssueId` to `WorkspaceFileResourceService`. It changes a
task identifier to the task UUID. It does not check visibility.
`getIssue` still does the privacy-scoped read.
- Added a `router.param("issueId")` handler to the file-resources
router. It calls `resolveIssueId`, the same as the issues router does
for its routes.
- `getIssue` now returns 404 before the query when the id is not a UUID.
This stops the Postgres cast error for all callers.
- Added a regression test for identifier, lowercase identifier, private
task, unknown identifier, and malformed id on the `content`, `list`, and
`availability` routes.
- Updated the test service fakes with the new method.

## Verification

- `pnpm vitest run src/__tests__/file-resources.test.ts
src/__tests__/file-resources-git-scan-load.test.ts` in `server/`: 55
passed.
- Without the source change, the new test fails with `expected 200 "OK",
got 500 "Internal Server Error"`. This reproduces the production error.
- I did not run a full local `tsc` because the sandbox did not have
sufficient memory. CI runs the typecheck.

## Risks

- Low risk. The change adds one indexed lookup on `issues.identifier`
(unique index) only when the path value has the identifier format.
- The identifier lookup does not check visibility. `getIssue` then
applies the privacy-scoped read, so a private or unknown task gives the
same 404 as before. The route does not show if a task exists.

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

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code with tool use (shell, file edit, 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: devinfoley <139239+devinfoley@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
canary/v2026.1104.0-canary.13
2026-11-04 12:30:26 -08:00
3cdb367369 fix(acpx): report how the sandbox agent process session ended (#16366)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Hosted `claude_local` runs use the ACPX engine. The agent process
runs in a sandbox, and a host-side process session bridge carries the
ACP stream.
> - Some hosted turns fail with only "The connection to Claude was
lost." (`acpx_turn_failed`, terminal session category `connection`). The
failure report shows one pending `execute` tool. It does not show why
the agent process went away.
> - The bridge already records how the session ended: the remote exit
code and signal, or a host proxy close. But it writes that evidence only
to the run log. The run failure report never receives it.
> - Without it, we cannot tell an OOM kill (`SIGKILL`) or a provider
process exit from a bridge or proxy close. Each cause needs a different
fix, so we must not guess.
> - This pull request keeps the first terminal evidence on the bridge
handle and copies it, as closed values, into the run failure report's
execution context.
> - The benefit is that the next recurrence names the cause, and the
correct fix can follow.

## Linked Issues or Issue Description

**What happened?**

Hosted `claude_local` runs on staging and production failed with "The
connection to Claude was lost." One run failed about 4 minutes in, 7–10
seconds after its last ACP event. Another failed after about 3.5 hours
with no ACP events for most of that time. Both had one pending `execute`
tool. The report had no information about how the sandbox agent process
ended.

**Expected behavior**

The run failure report says how the sandbox agent process session ended.
Examples: the remote process exited with a code, the remote process was
killed by a signal, or the host proxy closed.

**Steps to reproduce**

1. Start a hosted ACP `claude_local` run on a sandbox target.
2. Kill the agent process in the sandbox while a tool runs, for example
with `kill -9`.
3. See that the run log has an `ACP process session terminal:` line, but
the run failure report has no matching field.

**Paperclip version or commit**

`master` at 1b58ac6.

## What Changed

- `startAdapterExecutionTargetProcessSessionBridge` keeps the first
terminal evidence it records (`source`, `outcome`, `exitCode`,
`signal`). The new optional handle method `readTerminalEvidence()`
returns it. The run log line does not change.
- The closed source and signal labels move to
`acpx-engine/constants.ts`. The bridge and the server reader use the
same list.
- The ACPX engine adds `acpProcessTerminalSource`,
`acpProcessTerminalOutcome`, `acpProcessTerminalExitCode` and
`acpProcessTerminalSignal` to the turn activity diagnostics in
`resultJson`. It leaves out an unknown exit code or signal.
- `collectRunFailureDiagnostics` copies these fields into the execution
context. It accepts only closed labels and an integer exit code from 0
to 255. If the source is not a closed label, it drops all the fields.
- `doc/observability.md` documents the new fields next to the other ACP
activity fields.
- Tests: bridge evidence for a remote exit, a missing command, and an
authenticated proxy close; the engine summary helper; and the server
reader with valid and invalid values.

## Verification

- `vitest run src/acpx-engine/ src/execution-target-sandbox.test.ts` in
`packages/adapter-utils`: 22 files, 579 tests pass.
- `vitest run src/services/__tests__/run-failure-diagnostics.test.ts` in
`server`: 121 tests pass.
- `tsc --noEmit -p packages/adapter-utils`: passes.
- I type-checked the changed server files with a narrow config. The only
errors came from workspace packages that were not built locally
(`@paperclipai/plugin-sdk`, `@paperclipai/paperclip-runner`). A full
server `tsc` ran out of memory in my sandbox. CI runs the full check.

## Risks

- This change adds diagnostics only. It does not change retry, recovery,
or classification behavior.
- Every new field is a closed label or a bounded integer. No message,
command, path, payload, or provider text goes into the report.
- The host records a `proxy_close` from a socket close event. If the
turn finalizes before that event, the fields are absent for that run.
When the fields are absent, the report has the same data as it has
today.
- `readTerminalEvidence` is optional on the handle type, so other handle
implementations and test doubles still compile.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`) through Claude Code, with tool use
and code execution. It read the repository, traced the failure path,
edited the code, and ran the tests.

## 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

Related: #16360 bounds hung MCP tool calls (ACP kind `other`). This
change is separate. It covers the sandbox agent process ending while a
tool runs.

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

---------

Co-authored-by: devinfoley <devinfoley@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: devinfoley <139239+devinfoley@users.noreply.github.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 12:15:43 -08:00
53735db7de fix(retry): resume verified preparation failures (#19004)
## Thinking Path

> - Paperclip must preserve saved agent work while proving earlier
execution stopped.
> - A deliberate task Retry can observe a newer failure while an older
preparation still holds execution.
> - That older preparation can also have a finalized empty object log
despite never reaching the provider.
> - Binding only the latest run and requiring every log field to be
absent keeps valid recovery deferred.
> - This change follows the exact persisted predecessor chain and binds
current Retry authority to eligible holds.
> - It recognizes only an exact finalized empty-log receipt within the
existing strict preparation proof.
> - One fresh user turn can then start with saved work and the existing
ownership, stop and accounting checks.

## Linked Issues or Issue Description

Refs #18989, #18860 and #18997. The first waits for predecessor cleanup
during automatic dispatch. The second selects the committed native
session owner. The third retains admitted Retry authority when no source
recovery action exists. None passes current board Retry intent to a held
failed predecessor. A search of open repairs found no duplicate.
ROADMAP.md was checked; this is a recovery bug fix.

**What happened?**

A failed native preparation retains a legacy run row and an inherited
session. Its remote workspace is later saved and verified. An automatic
successor also fails during preparation. Board Retry observes that newer
run, but the effective hold belongs to its predecessor. The Retry does
not bind the predecessor's recovery generation. The native
historical-source guard then keeps the request deferred.

**Expected behavior**

Carry the deliberate Retry through its verified same-company, same-task
and same-agent persisted predecessor chain. Start one fresh user turn
after every source passes current execution, stop and saved-work checks.
Keep incomplete or unrelated proof held. Preserve prior work and
recorded action outcomes.

**Steps to reproduce**

1. Fail native preparation with an inherited session and a remote
workspace.
2. Finish its real workspace recovery and stop receipts.
3. Save a newer failed automatic successor with `retryOfRunId` bound to
that source.
4. Submit ordinary task Retry for the newer run.
5. Before the change, no successor is admitted. The request remains
deferred behind the older hold.

**Paperclip version or commit**

The regression fails on master
`039e0dad2c51c80251e1302a19c5566e039c357a`, which includes #18989.

**Deployment mode**

Source checkout on macOS arm64 with real PostgreSQL, HTTP task Retry and
the assembled heartbeat service. The remote filesystem boundary uses
temporary files. Hosted rollout and an original-task Retry are not yet
verified.

This PR incorporates the reviewed empty-log follow-up #19021, preserving
its commits and regression controls. Both parts are required for hosted
preparation recovery, so the combined revision is qualified and deployed
together.

## What Changed

- Accept a finalized zero-byte `s3_segments` log only when its reference
binds the exact company, agent and run, its digest matches empty
content, compression is false, and no output progress or nonempty
excerpt exists. Add a real-recorder HTTP regression and 14 negative
controls. Unknown or nonempty logs stay held.

- Read a complete persisted Retry chain in one recursive query, limited
to 32 runs. Reject missing, cyclic, newer, unfinished, cross-company,
cross-task and cross-agent sources. Transcript hints grant no authority.
- Bind eligible historical recovery records to the current Retry intent.
Keep complete or running recovery budgets. Leave successful ancestors
and mismatched recovery-source records unchanged.
- Recheck that same chain when selecting an ancestor's fresh-user
strategy.
- Admit only a held ancestor through the shared native preparation
proof. Keep the closed preparation failure codes, no-provider state,
profile, input, result, output and accounting checks. Treat inherited
session input as prior context and accept the exact bounded host start
marker. Verify current ownership, saved work and stop receipts before
forcing a fresh session.
- Add HTTP and PostgreSQL regressions and document the rule.
- Give the HTTP Retry nudge and explicit test worker the same real
heartbeat facade and provider boundary. Establish the revoked catalog
receipt before either can dispatch.

## Verification

- Combined integration head `91f021f233151192f69dd7a035d083abcc67c47c`
requires fresh complete CI and review; qualification is in progress.

- Repair validation: all **six** recovered preparation provider-boundary
cases pass. The catalog cases still require the exact access refusal,
zero provider inputs and zero provider turns. The success case still
checks preserved file bytes and one native turn.
- [Previous exact-head
CI](https://github.com/paperclipai/paperclip/actions/runs/41883421120/job/126785508136)
failed the catalog case with `remote_workspace_transport_unsupported`.
The route created a default heartbeat that could race the injected
worker and allocate outside the controlled transport. The repaired test
shares its real heartbeat across both dispatchers and holds the agent
start lock until the revoked catalog receipt is committed. It tracks and
drains both Retry nudges during teardown. It changes no production
access check or expected refusal.
- The first broad local reproduction hit a full local disk. It passed
201 cases and failed 86, including explicit manifest free-space errors.
Generated build caches were removed from these isolated checkouts. The
owning run passed 670/671 cases. Its one failure was the saved-Files
loader refusing insufficient temporary disk space. After cleanup, that
unchanged test passes in isolation. The final shared-facade and
start-lock version passes all six dispatch cases. Both legacy
continuation suites pass all 29 cases. The final fixture teardown also
retires deferred input before removing recovery proof, so global queue
sweeps cannot revisit earlier cases. All seven dispatch and saved-Files
controls pass together after that change. No disk reserve, assertion or
timeout changed. The initial local typecheck also stopped on `ENOSPC`
while materializing provider assets. Server typecheck now passes after
cleanup. No test timeout or reserve was lowered.
- Current-head CI is complete: 52 successful checks, three expected
skips, and a successful security status. Both required gates (`ci / e2e`
and `ci / verify`) passed on `0adc2e38103533dd45bcc56f957ac019642c57b1`.
[Full CI
evidence](https://github.com/paperclipai/paperclip/actions/runs/41898305521).
Greptile scored final head `0adc2e38103533dd45bcc56f957ac019642c57b1`
5/5 with no blocking findings or unresolved threads. The earlier
verification below describes the previous head.

- RED on unchanged master `039e0dad2c`: the certified-preparation
sibling passes, while the inherited-session case admits zero successors
instead of one. A second isolated RED on latest base `8f06bd4e17`,
including #18997, uses the exact final three-node test fixture. The HTTP
route returns 202, then the test observes zero successors instead of
one. All baseline production files remain unchanged. The initial
dependency/setup failures are separate from these behavioral failures.
- The positive cases cover the failed-failed-successful chain. They keep
the completed result unchanged, verify the original saved file and
validate fresh-session continuation authority. Repeated HTTP input and
concurrent resume calls keep one successor.
- PostgreSQL controls accept a complete 32-run chain. A 33-run chain and
a newer-created predecessor return no usable ancestry and do not bind
the older recovery record.
- Safety controls cover company, task and agent scope, conflicting
canonical bindings, missing and cyclic edges, transcript hints, live
execution and controllers, unknown stop, failed saving, unsafe writer
state, Stop, actor and generation changes, independent holds and
mismatched recovery records.
- Ancestor controls retain provider-event, native-input,
session-checkpoint, controller-recovery, result, process, runtime,
accounting and unknown-result guards. Host start and warning prefixes
use exact origins, sequence and payloads. A fifth extra event is
rejected.
- The first complete owning run found 22 introduced admission failures.
The initial historical-native allowance also applied to exact-source
Retry. It admitted contradictory preparation evidence. The fix now
requires a canonical ancestor and the shared strict startup proof. The
original direct-source assertions remain unchanged.
- The next focused run passed 82 cases, including both catalogue-warning
controls. One new result fixture lacked the database's required owner
tuple. Two existing controls reached their original 30-second limit. The
fixture now uses the real native task and completion-contract owner
tuple. A controlled nine-case run passes the result case, both unchanged
timed-out controls, three failure-code controls and all three positives.
The earlier timeout causes remain undetermined. All failed-run logs are
retained.
- Complete owning suites passed on previous head
`137a8eb3febf41052e112fc3c4ffe1abb92bcab7`: all 675 tests across
workspace recovery, task Retry, explicit continuation and legacy
continuation. The command exits successfully with normal teardown. This
includes every earlier introduced admission failure and the two
unchanged timeout controls. Its base includes the merged #18997
authority repair and #18982 rolling-copy packaging repair. The rebase
retained both test imports at their one adjacent-import conflict.
- Node 24.19.0 and pnpm 9.15.4. Module boundaries and `git diff --check`
pass. The initial PostgreSQL start lacked package library links after
install with scripts disabled. The documented hydration restored the
isolated dependency; that setup failure was not a behavioral red.
- Previous-head [full
typecheck](https://github.com/paperclipai/paperclip/actions/runs/41883421120/job/126785506215)
and [full
build](https://github.com/paperclipai/paperclip/actions/runs/41883421120/job/126785506094)
passed. Earlier attempts received synchronized runner shutdown signals
and a lost-runner annotation. The shutdown initiator remains unknown. A
CI owner used the documented hosted-runner failover for attempt three
and restored the original routing value after capture. That attempt
exposed the catalog failure above. Source workflows and check
requirements remain unchanged. The current head now has green CI,
Greptile 5/5, no unresolved review threads, and no merge conflicts.
Hosted rollout and the original task are not yet verified.


- Combined recovery proof: 24 of 24 cases passed on this exact head in
43.13 seconds, with normal teardown and unchanged assertions and limits.
They cover the real recorder, all 14 empty-output rejection controls,
ancestor and direct Retry, saved messages, the configured native
provider, and verified stop. The targeted TypeScript graph also passed;
the preceding integration step passed 95 safety/provider/message cases
and all 49 task Retry cases.
- The incorporated #19021 was independently red/green with the real
recorder: unchanged predecessor code returned HTTP 202 but admitted zero
successors. Its 18 focused cases, all 690 owning recovery cases, full
typecheck/build, 1,904 local chat cases, complete CI and Greptile 5/5
passed before integration. The unchanged CLI startup lane passed one
justified retry; no hosted acceptance is inferred from those results.
- Additional local qualification of the unchanged workspace fixture
alongside the native bootstrap fixture passed 288 of 289 cases. One
manual-admission HTTP case ended with `socket hang up` after 451 ms,
without an assertion or HTTP status. That unchanged case passed once in
isolation at the original 40-second limit, with normal teardown, and
passed in exact `0adc` CI in 17.2 seconds. The earlier disconnect cause
remains unknown; no assertion or timeout changed.
- An earlier CI attempt failed the newly merged native ingress fixture
because its shared Node command failed strict permission qualification.
The current base includes #19020's owned executable fixture repair; no
production permission check was relaxed.
- Integration with current master had one documentation conflict. An
automatic test merge also duplicated an app declaration; that syntax
failure was retained and fixed by reusing the configured real HTTP app.
Both new message-state assertions remain intact. Workflows and lockfile
retain master's contents.

## Risks

- A damaged or overlong chain remains held. The helper does not truncate
it and grant partial authority.
- The new query runs only for a deliberate Retry or its ancestor
strategy check. It adds no background polling, table, credential or
provider policy.
- A fresh intent is not stop or file-recovery proof. The existing owners
retain those checks. Prior effects remain fenced, and no old result is
replayed.
- Local tests do not prove hosted rollout. Live acceptance must use the
served fixed revision and the task's original saved work.

## Model Used

OpenAI GPT-6 through Codex for the original change. OpenAI GPT-6.1 Sol
(`gpt-6.1-sol`), xhigh reasoning, for the merge and CI repair. Both used
repository tools and PostgreSQL verification. Context-window limits were
not exposed by the harness.

## Checklist

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

---------

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

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

## Linked Issues or Issue Description

**What happened?**

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

**What did you expect to happen?**

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

**Steps to reproduce**

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

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 14:11:03 -06:00
c3715e74a6 perf(ci): let the public-install registry reuse warmed scoped metadata (#16838)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Every pull request runs the `ci / Build` check. This check was the
slowest check in recent green runs: 432s in run 39254079691, 480s in run
39251304886 and 472s in run 39245233967.
> - The slowest step in Build is "Verify built-in Grok from a clean
public npm install" (162s and 207s). Its consumer `npm install` took 69s
and 75s, even though a background step fills the npm cache first.
> - The loopback registry in `scripts/public-install-registry.mjs` sends
packages that it does not serve to npmjs with a 302 redirect. It escaped
scoped names with `encodeURIComponent` (`%40scope%2Fname`). npm requests
and caches the same metadata as `@scope%2fname`, and npm keys cached
metadata by URL.
> - Thus the consumer install could not use the scoped metadata that the
warm-up had just put in the cache. It downloaded and parsed all 291
scoped packuments again (about 285 MB of full packument JSON) inside the
4-vCPU container.
> - This pull request makes the redirect use the same escaping as npm.
> - The benefit is a shorter Build check on every pull request. The
installed bytes do not change.

## Linked Issues or Issue Description

**Describe the bug**
The consumer install in the public-install verifier does not use cached
scoped package metadata. The warm-up step
(`scripts/warm-public-install-npm-cache.mjs`) caches this metadata
through direct npm requests.

**Steps to reproduce**
1. Run `PAPERCLIP_PUBLIC_INSTALL_NPM_CACHE=/tmp/c node
scripts/warm-public-install-npm-cache.mjs`.
2. Install the same dependency set through the loopback registry with
`npm_config_cache=/tmp/c` and `--loglevel http`.
3. Count the `(cache miss)` lines.

**Expected behavior**
No cache misses for metadata that the warm-up already fetched.

**Actual behavior**
291 cache misses. All of them are scoped packages that are requested as
`https://registry.npmjs.org/%40scope%2Fname`. The cache holds them as
`https://registry.npmjs.org/@scope%2fname`.

Related: #16513 changes when the verifier waits for the warm-up. This
pull request does not overlap with it, and the two changes add together.

## What Changed

- `scripts/public-install-registry.mjs`: the 302 redirect for packages
that the registry does not serve now uses npm's own escaping
(`name.replace('/', '%2f')`, the same as npm-package-arg `escapedName`).
The `packageName` check already allows only characters that need no
other escaping.
- `scripts/__tests__/public-install-registry.test.mjs`: the test now
expects `@other%2fpkg` for a scoped redirect. A new assertion covers an
unscoped redirect.

## Verification

- `node --test scripts/__tests__/public-install-registry.test.mjs`: 3 of
3 pass. This includes the real npm install test that compares installed
bytes from file installs and registry installs.
- Local replay (Linux, npm 11.12.1). I filled a fresh cache with the
warm-up script, then installed the same 62-dependency set (627 packages)
through the registry:
  - Before: 291 cache misses, 28.3s.
  - After: 0 cache misses, 19.8s.
- I also ran the real `scripts/public-install-registry.mjs` with a
packed root package that depends on the full public dependency set.
Result: 2 misses (only the loopback package and its tarball, as
expected) and 1438 cache hits.
- To compare on CI, look at `consumer_install_ms` in the
`paperclip.public-npm-install.timing.v1` line of the Build log.
Baseline: 68822 ms (run 39254079691) and 75175 ms (run 39251304886).

## Risks

- Low risk. The redirect target is the same npmjs document. Only the URL
escaping changes. npm still checks every tarball against the integrity
in the packument.
- If the npm cache holds stale scoped metadata, npm revalidates it the
same way as it does for direct requests (`cache-control: max-age=300`).
- The expected time saving comes from a local measurement. The real
Build saving depends on the network and CPU of the runner. I expect
about 10–20s per Build run. The CI timing line will confirm the number.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code as a Paperclip agent with tool use (shell, file edit). I did the
root-cause analysis, the local replays and the code change with this
model.

## Checklist

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

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

---------

Co-authored-by: devinfoley <139239+devinfoley@users.noreply.github.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 12:10:00 -08:00
ff7e7b4963 fix(claude-local): make sandbox tool commands the first OOM victims (#16844)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Hosted `claude_local` runs execute in a sandbox. The sandbox has a
fixed memory limit (4 GiB in the observed sandboxes).
> - In that sandbox, every process has the same `oom_score_adj`. When a
Bash tool command uses most of the memory, the kernel kills the largest
process. That can be the Claude Code process.
> - When Claude Code is killed, `claude-agent-acp` reports "The
connection to Claude was lost." and the whole run fails. Server logs
show `Claude Agent process died: Claude Code process terminated by
signal SIGKILL` for each failure.
> - A failed tool command is recoverable. A dead agent is not.
> - This pull request makes Claude's tool commands the first
out-of-memory victims in managed sandboxes.
> - The benefit is that a memory-heavy command fails with exit 137, and
the agent continues the run.

## Linked Issues or Issue Description

Refs #16366 (diagnostic PR for the same failure). Related: #14509
protects the host control plane from local agent processes. That PR does
not cover a sandboxed agent and its own tool commands.

**What happened?**

Hosted `claude_local` runs fail with `acpx_turn_failed` and "The
connection to Claude was lost." while one `execute` (Bash) tool call is
pending. The pending commands are memory-heavy, for example `tsc
--noEmit` with a 3.3 GB heap, a monorepo `pnpm install`, or a type check
plus `vitest`. At the same second, the server log shows `Claude Agent
process died: Claude Code process terminated by signal SIGKILL`. Over 10
days, all 12 such lines on one staging stack were `SIGKILL`, and they
match the failed runs. The sandbox memory cgroup has `memory.max` 4 GiB
and `memory.oom.group` 0, so the kernel kills one process. Every process
has `oom_score_adj` 0, so the kernel picks the largest one.

**Expected behavior**

Under memory pressure, the kernel kills the tool command. Claude gets a
failed command (exit 137) and continues the run.

**Steps to reproduce**

1. Start a `claude_local` run on a sandbox target with a 4 GiB memory
limit.
2. Ask the agent to run a memory-heavy command in a large repository,
for example a full `tsc --noEmit` and a test run together.
3. The kernel kills the Claude Code process, and the run fails with "The
connection to Claude was lost."

**Agent adapter(s) involved**

Claude Code (`claude_local`, ACP lane and CLI lane, managed sandbox
targets).

## What Changed

- `claude-config.ts`: add `CLAUDE_TOOL_SHELL_PREFIX_SCRIPT`. Claude Code
calls a `CLAUDE_CODE_SHELL_PREFIX` with one argument, the full command
string. The script raises its own `oom_score_adj` to 1000 (no privilege
is necessary to raise it). Then it runs the command string in the shell
that Claude Code selects: `CLAUDE_CODE_SHELL`, then `SHELL`, when either
is an executable bash or zsh, else zsh or bash from `PATH` (bash first
when `SHELL` names bash).
- `buildRemoteClaudeConfigMaterializationCommand` can install the script
(umask 077, write to `.partial`, `chmod 700`, `mv`). The install runs
last in the existing materialization command, so it adds no sandbox
round trip. It prints a marker only when the file is in place.
- `materializeRemoteClaudeConfig` returns `toolShellPrefixInstalled`. It
is true only when the command exits 0 and prints the marker.
- ACP lane (`acp.ts`) and CLI lane (`execute.ts`): set
`CLAUDE_CODE_SHELL_PREFIX` to the installed script only when the install
is confirmed. A prefix path that does not exist would fail every Bash
tool call, so a failed install keeps the previous behavior. An operator
`CLAUDE_CODE_SHELL_PREFIX` in the adapter env is kept, and no script is
installed.
- `docs/adapters/claude-local.md`: add "Sandbox memory limits".

## Verification

- `npx vitest run src/server/claude-config.test.ts` in
`packages/adapters/claude-local`: 10 pass, 6 new. The new tests install
the script with the real materialization command through `/bin/sh` and
check:
- the file content, mode 0700, the marker, and that no `.partial` file
remains;
- the command runs with `oom_score_adj` 1000 while the caller keeps its
own score (Linux only);
- shell selection: a valid bash `SHELL`, an invalid `CLAUDE_CODE_SHELL`
that falls back to `SHELL`, and a `SHELL` of `/bin/sh` that falls back
to bash from `PATH`;
  - quoting and the exit status of the command are kept;
  - an operator prefix wins, and no marker means no install.
- `acp.test.ts`: the managed-config test now checks that
`CLAUDE_CODE_SHELL_PREFIX` names an executable 0700 file next to the
config dir. A new test checks that an operator prefix is kept and no
script is installed. Both use the local sandbox runner, which runs the
setup commands for real.
- `tsc --noEmit -p packages/adapters/claude-local` is clean.
- Two tests fail in my sandbox both with and without this change: `uses
the same default model in ACP startup and session identity` and
`reselects the full assignment and bootstrap guidance after a failed
resume`. They also fail on clean `master` in the same environment, so I
think they depend on that environment. CI covers them.
- Real Claude Code check (Claude Code 2.1.286): with
`CLAUDE_CODE_SHELL_PREFIX` set to the script, a Bash tool call printed
`oom_score_adj` 1000 and ran in zsh (the selected shell), and its parent
was the `claude` process at score 0. Without a prefix file that exists,
every Bash tool call fails with exit 127. This is why the env is set
only after a confirmed install.
- Real OOM check in a 4 GiB Daytona sandbox (`memory.oom.group` 0). A
stand-in "agent" process held 1.2 GB at score 0, and a tool command
started 14 processes of 250 MB each:
- without the prefix, the kernel killed the stand-in agent (SIGKILL,
137; `oom_kill` +1);
- through the installed prefix, the stand-in agent stayed alive, and the
kernel killed 5 tool processes (`oom_kill` +5).
- Assumption that could invalidate the design: Claude Code passes one
argument (the command string) to the prefix and honors the variable
through `claude-agent-acp`. I checked the argument shape with a logging
prefix, and `claude-agent-acp` 0.73.0 spawns the CLI with `{
...process.env }`.

## Risks

- Every Bash tool command and hook in a managed sandbox now runs through
one more `sh` process and an `exec`. The cost is a few milliseconds for
each command.
- If a future Claude Code release changes how it calls
`CLAUDE_CODE_SHELL_PREFIX`, Bash tool calls can fail. The tests above
and the documented variable limit this risk. Removing the variable
restores the old behavior.
- The inner shell is not a login shell. Claude Code sources its own
shell snapshot inside the command string, so the environment stays the
same. If Claude Code runs without a snapshot, it uses a login shell.
Only exported variables then pass through to the inner shell.
- If the selected shell differs from the one that Claude Code chose (for
example, a custom `CLAUDE_CODE_SHELL` that is not bash or zsh), the
script uses the same fallback order as Claude Code.
- This does not help when the Claude Code process itself uses most of
the memory. The local (non-sandbox) lanes do not change.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`) through Claude Code, with tool use
and code execution. It read the repository and the Claude Code CLI
behavior, ran the OOM experiment, edited the code, and ran the tests.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [ ] I have run tests locally and they pass (two tests fail on clean
`master` in my sandbox too; see Verification)
- [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: devinfoley <139239+devinfoley@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 12:09:54 -08:00
Nicky LeachandClaude Opus 5.5 e107a75755 fix(native): decide session settlement from live state (#18992)
## Thinking Path

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

## Linked Issues or Issue Description

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

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

**Paperclip version or commit**

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

**Deployment mode**

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

## What Changed

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

## Verification

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

## Risks

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

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 12:04:29 -08:00
DottaandPaperclip 25b5a072cc fix(recovery): let messages continue recovered preparation (#19026)
## Thinking Path

> - Paperclip must preserve work while allowing people to continue
failed tasks.
> - A preparation failure can retain an ID from an earlier provider
session.
> - Workspace recovery and host settlement already prove that the failed
attempt never dispatched a provider.
> - The preparation classifier also required that inherited session ID
to be absent.
> - That extra condition kept new messages, saved messages, and Retry
behind a resolved recovery hold.
> - Classify execution from the existing execution evidence and admit
one fresh turn through normal gates.

## Linked Issues or Issue Description

Refs #18908, #19004, and #19021. The latter two address historical Retry
ancestry and empty object logs. This repair addresses a current source
with an inherited session and ordinary user messages; it needs neither
ancestry nor an object log. ROADMAP.md and related open PRs were
checked.

**What happened?**

Sending a message to a failed task saved it in the queue, but did not
start another run. The failed native preparation had recovered files and
durable host settlement, plus `sessionIdBefore` from prior work. The
classifier treated that input as execution evidence and kept the
recovery hold.

**Expected behavior**

Admit the new user turn once the existing saved-work and stop checks
pass. Preserve previous failures and files. Show the current run instead
of the old error, and consume messages when execution starts.

**Steps to reproduce**

Fail Runner preparation before runtime selection. Preserve its remote
workspace. Retain an inherited `sessionIdBefore`. Send an ordinary
message or click Retry. The three new HTTP regressions accept the
request but admit zero successors before the repair.

**Paperclip version or commit**

Base `3bd52a9fa0`.

**Deployment mode**

Authenticated hosted Runner. Regression verification uses macOS arm64,
Node 26.4.0, real PostgreSQL, HTTP routes, and real temporary workspace
archives. The external sandbox transport and native provider boundary
are controlled in the execution fixture.

## What Changed

- Stop requiring the inherited session input to be absent from otherwise
verified failed preparation.
- Cover new messages, previously saved messages, Retry, request replay,
one successor, recovered files, and current task presentation.
- Exercise the existing preparation safety controls with inherited
context, including a new output-session refusal.
- Exercise native execution and finalization with the inherited session
input.
- Document the continuation rule.

## Verification

- RED: all three new HTTP cases admitted zero successors before the
production change.
- All three final HTTP cases pass. The 41 preparation safety cases,
including the new output-session refusal, pass with inherited context.
- The native execution fixture passes with inherited context. It reads
restored files, writes the continuation result, runs one provider turn,
persists the accepted result, empties the queue, and resolves the
notice.
- Initial positive assertions expected an empty queue while the fixture
deliberately held the agent's slot, then expected a null optional wait
field. The final assertions use the actual queue contract and retain
execution and presentation checks.
- `pnpm build`, `node scripts/typecheck-low-memory.mjs`, module
boundaries, and `git diff --check` pass locally. The final rebased head
`56d2ef26` passes the four HTTP/native journey cases. All 634 tests in
the four complete owning recovery, Retry and continuation suites pass.
Current-head full CI passes: 53 successful checks and three expected
skips, with no failed or pending checks. The full local command has the
documented timeout below.
- The full local command passed its first three chat shards, then
stopped in shard four on a 15-second timeout in `answers each finding
thread in its own run instead of one unattestable batch` (267 other
shard cases passed). The exact unchanged test passes alone (about nine
seconds of test work) and all three full CI chat groups pass on this
head. It uses a custom adapter, so the changed native-preparation proof
is inapplicable. The timeout cause remains unknown; the full local
command is not claimed green and its later groups were not reached.
- Initial CI attempt: all other checks passed, but server shard 2/12
failed on the unchanged `idle-completed-history.test.ts` owned HTTP
runtime test. The ten-second readiness probe returned ECONNREFUSED. The
source and runtime have no diff from the base and this test does not use
failed-preparation admission. The exact local case passes; the
underlying readiness cause remains unknown. Logs and annotations are
retained at [the failed
job](https://github.com/paperclipai/paperclip/actions/runs/41897539206/job/126811385017).
The single failed-job retry passed on the same head, including the
dependent verify aggregate. This proves recovery of that check; the
original readiness cause remains unknown.
- Greptile reviewed the exact head `56d2ef26` at 5/5 with no actionable
findings and zero unresolved threads. Hosted recovery remains unverified
until the repair reaches the serving revision.

## Risks

The risk is mistaking earlier context for a stopped current execution.
Current provider session, process, controller, native
input/result/output, accounting, exact lease stop, workspace
epoch/generation/writer, actor, assignment, pause and approval checks
remain enforced. No migration, new polling, API, credentials or
execution policy is added.

## Model Used

OpenAI GPT-6 through Codex, using reasoning, code editing, shell tools,
and browser diagnostics. The exact serving snapshot 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 (634 owning-suite cases and
final-head journey cases; full-local timeout is documented above)
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 13:54:38 -06:00
Devin FoleyandPaperclip e65c68726b Require published migrators before publishing Core images (#19023)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Its release pipeline publishes Core images and exact-source database
migrators.
> - A migrator run can pass admission but skip all build and publication
work.
> - The image observer could stop checking after provisional build
admission.
> - The final image gate checked source proof without checking migrator
publication.
> - This pull request requires the exact migrator publisher to finish
before image publication.
> - Consumers can then use the published image with its matching
migration artifact.

## Linked Issues or Issue Description

**What happened?**

The [migrator run for
e968131](https://github.com/paperclipai/paperclip/actions/runs/41868122685)
passed its admission job but skipped build and publish. Its [Docker
run](https://github.com/paperclipai/paperclip/actions/runs/41868122627)
still published an image. The exact-source migrator artifact was absent.

**Expected behavior**

An image consumer restarts a strictly deferred producer. It waits for
the exact migrator publication before it publishes the image. A
successful admission job alone does not prove publication.

**Steps to reproduce**

1. Let a migrator run defer work while the image consumer starts.
2. Make the producer appear admitted before its build later becomes
skipped.
3. Complete the source verification job.
4. Observe that the old image gate can accept source proof without a
successful migrator publisher.

The regression fixtures model this state. The real producer records
establish the missing publication; the exact transient API response seen
by the original observer is not retained.

**Paperclip version or commit**

The observed source was `e968131efa7e63d321f3bcca8d47a62513c2efb4`. This
patch is based on current master.

No duplicate fix was found in open migrator PRs. PR #18406 concerns npm
canary scheduling and does not provide this image publication gate.

## What Changed

- Require the successful canonical migrator `publish` job for the exact
source and latest attempt. Check complete job pages and re-read the run
after the jobs.
- Continue observing deferred producers until publication, even after a
build appeared admitted. Only a physical runner proves build admission.
- Check migrator publication again in the final read-only
`--check-proof` image gate. Failed producers still require inspection
and are not retried automatically.
- Add regression coverage for deferred recovery, skipped and stale
publication, attempt races, and the final CLI gate. Update release
documentation.

## Verification

- All 57 current-head check contexts are successful or expected skips on
`daed1c756a9fc02b8c15128b96af4da86d447e3f`; both required checks (`ci /
verify`, `ci / e2e`) passed. Greptile is 5/5 with no review threads, and
the branch has no merge conflicts. GitHub master rules require CODEOWNER
review before merging.
- The first CI attempt failed serialized server shard 2/9:
`heartbeat-process-recovery.test.ts` timed out after three seconds
waiting for an adapter execution to start (1 failed, 411 passed).
[Failed
job](https://github.com/paperclipai/paperclip/actions/runs/41894941958/job/126802368555).
This PR changes no server or package files. The exact timeout cause is
not established; this failure is retained and is not described as
infrastructure noise.
- The same first attempt also failed general server shard 6/12:
`heartbeat-stale-queue-invalidation.test.ts` waited five seconds for an
adapter execution after native-owner completion; the run remained
running with no error (1 failed, 2,627 passed, 3 skipped). [Failed
job](https://github.com/paperclipai/paperclip/actions/runs/41894941958/job/126802369329).
Its cause is also not established. The first-attempt failures remain
part of this record.
- The single controlled failed-jobs rerun completed successfully on
attempt 2 at the identical commit. Both previously failed heartbeat
shards and the aggregate verifier passed. This demonstrates recovery on
rerun; the underlying first-attempt failure causes are not established.
[Successful
rerun](https://github.com/paperclipai/paperclip/actions/runs/41894941958/attempts/2).
- Node 24: `node --test scripts/cloud-source-verification.test.mjs
scripts/standard-image-contract.test.mjs` passed: 62 tests, 0 failures,
after rebasing on current master.
- An explicitly gated read-only live probe rejected the real skipped
producer for `e968131` and accepted the published producer for
`afbc5a885b52e96df10c1c6e643bea31b51caac8`.
- `git diff --check` and Gitleaks passed before push.
- A broader run that also included
`scripts/__tests__/release-verify-workflow.test.mjs` failed when local
fixture creation and package installation hit `ENOSPC`. Those failures
remain unverified locally; the focused source and image tests passed
separately, and CI Typecheck + Release Registry subsequently passed.
- Full workspace typecheck, Vitest and app builds were not run locally.
Disk capacity does not permit installing the full workspace in this
separate checkout. No application TypeScript or package manifest
changed. The complete CI run subsequently passed, including typecheck,
build, general and serialized tests, release checks and e2e.
- No live Slack posts, workflow dispatches, producer reruns, deployments
or provider mutations.

## Risks

- Image publication now waits for the exact migrator publisher. A failed
publisher blocks the image and reports a publication failure. Existing
source failure cancellation still applies.
- This adds bounded Actions reads to the existing source observer. The
observer keeps its existing deadline. The final publication runner only
reads evidence and cannot restart producers.
- The publisher job name is part of this gate. A workflow rename must
update the gate.
- No database schema, migration content, image contents, credentials or
application behavior change. Revert this PR to restore the prior
publication gate.

## Model Used

- OpenAI `gpt-6.1-sol`, xhigh reasoning effort, through Codex in T3
Code. Source inspection, tool use and code execution. Context window
size is not exposed by the harness.

## Checklist

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

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 11:51:42 -08:00
Devin FoleyandClaude Opus 5.5 09f9d691cf fix(server): keep the root failure code when the execution deadline fails a run (#19028)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The execution-control watchdog fails a run when its control deadline
passes and no owner finished it.
> - The watchdog replaces the run's error code with
`execution_finalization_deadline_exceeded`.
> - The run-failure report reads the root cause from
`resultJson.originalFailureCode`. The watchdog did not write that field.
> - So every watchdog failure goes to Sentry with the same fingerprint,
whatever its first failure was.
> - This pull request keeps the earlier failure code as
`originalFailureCode`, as the native finalizer already does.
> - The benefit is that Sentry groups watchdog failures by their real
first failure, so each cause can be found and fixed.

## Linked Issues or Issue Description

**What happened?**

One hosted Sentry group collects all
`execution_finalization_deadline_exceeded` run failures. It has 17
events from many drivers (Codex app server, OpenCode server, ACPX
runtime). In one recent event, the run first failed with
`native_execution_ownership_unverified`. The watchdog then failed it at
the deadline. The recovery action evidence kept `originalFailureCode:
native_execution_ownership_unverified`, but the run row and the Sentry
event did not. The event had no `root_error_code` tag, and its
fingerprint was only `[execution_finalization_deadline_exceeded,
paperclip_runner]`.

**Expected behavior**

When the watchdog replaces an earlier, more specific error code, the run
keeps that code as `resultJson.originalFailureCode`. The run-failure
report then sends `root_error_code` and adds it to the fingerprint
(`server/src/sentry.ts`).

**Steps to reproduce**

1. Create a running run with `errorCode:
native_execution_ownership_unverified` and an expired
`executionControlDeadlineAt`.
2. Run `reconcileAbandonedExecutionControl`.
3. The run fails with `execution_finalization_deadline_exceeded`, and
the earlier code is lost. The captured event has no
`diagnostics.execution.originalFailureCode`.

**Paperclip version or commit**

`master` at `89703c3ff7de`.

Refs #14110, a similar fix for the finalization sweep. A search found no
other open PR for this path. Draft #18672 changes legacy deferred wakes,
which is a different path.

## What Changed

- `execution-control-reconciliation.ts`: when the watchdog fails a run,
it writes the earlier code (`coordinator.failureCode ?? run.errorCode`)
to `resultJson.originalFailureCode`. This is the same value that it
already writes to the recovery action evidence.
- The watchdog keeps the other `resultJson` fields.
- The watchdog does not replace a root cause that is already recorded.
- The watchdog does not record the deadline code as its own root cause.
- Two regression tests in `execution-control-reconciliation.test.ts`.

## Verification

- The new test "keeps the run's prior failure code as the reported root
cause" fails on unchanged `master`. The run's `resultJson` has no
`originalFailureCode`. With the fix, the test passes.
- `pnpm exec vitest run
src/services/execution-control-reconciliation.test.ts
src/__tests__/execution-control-work.test.ts
src/__tests__/run-failure-sentry.test.ts src/__tests__/sentry.test.ts`:
111 passed, 5 skipped.
- `pnpm exec vitest run src/services/task-retry.test.ts
src/services/heartbeat/recovery.test.ts
src/services/native-runtime/native-safe-replacement.test.ts
src/services/workspace-recovery.test.ts`: 437 passed. These suites use
this watchdog cause.
- `node scripts/typecheck-layers.mjs` in `server/`: passed.
- Not run locally: the full `pnpm -r typecheck` (the sandbox has no Rust
toolchain for the runner crate), `pnpm test:run` and `pnpm build`. CI
runs these.

## Risks

- Low risk. The change adds one `resultJson` key only when the watchdog
moves a run to `failed`, and only when a different earlier code exists.
- Other readers of `resultJson.originalFailureCode`
(`native-run-projection.ts`, `idle-sleep-safety.ts`) only check
committed or succeeded runs. A watchdog failure is neither, so their
results do not change.
- `isPendingNativeRetry` reads only `nativePhase`, so reporting is not
suppressed.
- Sentry groups will split by root cause after deployment. New events
will go to new groups, and the existing group will get fewer events.
This is the intended change.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), with tool use and code execution
in a Paperclip agent sandbox.

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 11:51:36 -08:00
DottaandPaperclip 3a283fbc9b fix(runtime): preserve failed sources with older warm supervisors (#19024)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A volume-free web server needs durable recovery data for retained
sessions.
> - An old failed follow-up can retain its snapshot while the warm
supervisor still holds the preceding successful turn.
> - The migration refuses to treat those different snapshots as the same
execution authority.
> - That refusal also blocks the tenant even when the failed source can
be preserved as an archive.
> - This pull request archives that narrow case after it verifies the
older successful snapshot and all scope bindings.
> - The failed run keeps its history and files while valid controller
recovery keeps its existing proof requirements.

## Linked Issues or Issue Description

Refs #18959, which added the existing failed-controller archive path. No
duplicate PR for this case was found.

**What happened?**

Offline controller migration stops with `stale_supervisor_snapshot` for
an old failed warm follow-up. Its local supervisor still contains the
prior successful turn. Read-only checks found the same database snapshot
and supervisor bytes in the live source and the isolated copy. The
mismatch predates the rehearsal.

**Expected behavior**

Preserve the failed run's database state and available local files in
the existing durable archive. Only archive when the older supervisor
matches a completed successful run in the same company, agent, task,
native session and Runner instance. Keep current execution and
controller authority checks intact.

**Steps to reproduce**

Create a successful native turn and a later failed warm follow-up. Keep
the successful snapshot in the supervisor file and the failed snapshot
in the database. Give the failed source local controller bytes but no
durable controller checkpoint. Run the offline migration. The new
regression fails with `stale_supervisor_snapshot` before this change.

## What Changed

- Verify the older supervisor against scoped successful history and the
same provider thread.
- Use the existing encrypted archive owner for that failed legacy
source.
- Preserve snapshots, run status, ownership, queued work and the
session's last-run pointer.
- Keep foreign, changed, active, successful and already-durable sources
outside this path.
- Document the preservation and recovery boundary.

## Verification

- New positive regression: red on the original owner, green after the
change.
- Native migration and controller recovery: 27 checks passed. Final
migration checks also cover refusal to replace a durable checkpoint.
- Storage failure blocks publication. Repeated migration is safe. The
archive restores the exact controller and supervisor bytes after the old
home is deleted.
- The archived failed source does not gain executable recovery
authority.
- Local server typecheck is blocked by reused workspace declarations for
accounting, bootstrap and ingress contracts. No diagnostic names either
modified owner or test file. Full local typecheck, repo-wide tests and
build are not claimed; clean CI is required before merge. The exact-head
CI has now passed typecheck, build, policy, Runner checks, browser E2E,
and all server test shards.
- Rehearsal verification against the copied historical source remains
required. Full tenant migration and hosted deployment qualification
remain separate gates.

## Risks

The archive preserves evidence for an already failed source. It does not
certify that source as resumable. A prior snapshot must match exactly,
and the normal archive owner checks the terminal failed binding before
it publishes. Existing durable checkpoints remain authoritative. A
missing or changed proof still blocks migration.

## Model Used

OpenAI Codex, exact model ID `gpt-6.1-sol`, with reasoning, tool use and
code execution. The host does not expose 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 described the issue in-PR following the bug template
- [x] I have not referenced internal or instance-local Paperclip issues
or links
- [x] My branch name describes the change and contains no internal
ticket id or instance-derived details
- [x] I have run the relevant tests locally; retained failures and
limits are listed above
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented 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.1104.0-canary.12
2026-11-04 13:40:53 -06:00
Devin FoleyandPaperclip cf0ab4f7f4 chore: enable boxd plugin release publishing (#19020)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Sandbox provider plugins let those agents run in remote
environments.
> - The boxd provider exists, but its release entry excludes it from CI
publishing.
> - npm now has the `@paperclipai/plugin-boxd@0.0.0` bootstrap
placeholder.
> - This pull request enables CI publishing and documents npm
installation.
> - A pre-existing OpenCode fixture blocked CI because it qualified a
group-writable host Node binary.
> - This also gives that test its own Linux Node copy with strict
permissions.
> - Operators can install a real release after CI publishes it.

## Linked Issues or Issue Description

Related implementation:
https://github.com/paperclipai/paperclip/pull/17284.
No matching public issue or duplicate release-enrollment PR was found.

**What existing behavior does this improve?**

Publishing and installing the existing boxd sandbox provider.

**Subsystem affected**

`packages/plugins` and the npm release manifest.

**Current behavior**

`publishFromCi` is false. npm has only the `0.0.0` placeholder. The
README says the package is not published and requires a local-path
install.

**Proposed behavior**

Include boxd in CI releases. Document stable installation, explicit
published versions, and the placeholder limit.

**Reason and benefit**

Operators can install the provider through the existing npm plugin
installer after a real release ships.

**Breaking changes**

None. This changes release enrollment and documentation only.

**Additional context**

This follows the existing plugin and sandbox roadmap items. It adds no
new runtime capability.

## What Changed

- Set `publishFromCi: true` for `@paperclipai/plugin-boxd` in the
release manifest.
- Replace the README's unpublished-package notice with npm installation
guidance.
- Explain that the `0.0.0` placeholder has no plugin functionality and
that `latest` can point to it until the first stable release.
- Keep the local development instructions available.
- Fix the existing ingress-retry test to use an owned Linux Node copy
and explicit executable permissions. This follows the existing OpenCode
transport fixture. It does not change production launch checks.

## Verification

- Passed: `node --test scripts/release-package-map.test.mjs
scripts/check-release-package-bootstrap.test.mjs
scripts/bootstrap-npm-package.test.mjs` (31 tests).
- Passed: `node scripts/release-package-map.mjs check`.
- Passed: `PAPERCLIP_RELEASE_BOOTSTRAP_BASE_SHA=origin/master node
scripts/check-release-package-bootstrap.mjs
scripts/release-package-manifest.json
packages/plugins/sandbox-providers/boxd/README.md`.
- Passed: `git diff --check`.
- Confirmed that npm resolves `@paperclipai/plugin-boxd` to version
`0.0.0`.
- Full workspace typecheck, test suite, and build were not run locally.
CI checks remain required before merge.
- Initial CI failure: [native Runner
job](https://github.com/paperclipai/paperclip/actions/runs/41889969053/job/126785636239),
OpenCode ingress retry, `qualified OpenCode proxy command must not be
group- or world-writable`. The failing fixture is byte-identical to the
base commit `3a2249dd576`. It passed `process.execPath` to
qualification, while other OpenCode tests already use a private Linux
Node copy.
- Local focused run: `pnpm exec vitest run
server/src/services/native-runtime/native-ingress-bootstrap-retry.integration.test.ts`
built the real Rust Runner and passed the Codex case. The OpenCode case
stopped at `runner_remote_provider_artifact_incompatible: OpenCode proxy
path belongs to the controller host` on macOS (`/Users/...`). This does
not exercise the new Linux-only copy.
- An unchanged-base OpenCode comparison was attempted with `-t
"reacquires opencode ingress"`, but PostgreSQL setup failed with
`ENOSPC` before the case ran. The local baseline comparison is therefore
inconclusive. Task-owned Rust build outputs were cleaned to recover disk
space.
- At this stage Linux verification was pending. The successful final run
is recorded below.
- No new tests were needed. The existing release-tool tests cover
manifest enrollment, dependency ordering, and bootstrap checks.

- Current-head CI interruption: [workspaces-a shard
1/2](https://github.com/paperclipai/paperclip/actions/runs/41893808561/job/126799012853)
was cancelled after the runner received a shutdown signal. The log shows
no assertion failure. The job rerun request was rejected with HTTP 403
because the workflow was still running. The subsequent fixture
correction starts a new full CI run and supersedes that pending rerun.

- CI at `7fffa315`: [native Runner
job](https://github.com/paperclipai/paperclip/actions/runs/41893808561/job/126799011711)
passed executable permission checks, then failed because Node tried to
resolve `/memfd:paperclip-verified-launch (deleted)`. The private
executable was named `opencode-node`, which bypassed Runner's Node
interpreter recognition. The fixture now names its copy `node`, matching
the existing transport fixture and selecting the CommonJS descriptor
loader.
- Passed a local smoke test of the production CommonJS descriptor loader
using an open, unlinked script file. This is macOS evidence only; the
successful final Linux run below provides the complete ingress retry
evidence.

- Final verification on `bcacac542aa0a1b641b26c099c1e9fc4a1653ec4`: all
54 reported checks passed; three unrelated checks were skipped. [Native
Runner integration
job](https://github.com/paperclipai/paperclip/actions/runs/41895787712/job/126805844773)
passed, including the OpenCode ingress retry regression. The fresh full
run also passed the workspace shard that was interrupted on the previous
attempt. Greptile reports 5/5 for this exact head with no unresolved
review threads. Earlier failures and local verification limits remain
recorded above.

## Risks

- The maintainer confirmed that npm trusted publishing is configured for
this package. The first real CI publish will verify OIDC authentication.
- Merging enables publication in the next successful canary run. An
installation that uses `latest` can still resolve to the placeholder
until the first stable release includes this change.

## Model Used

OpenAI Codex, exact model ID `gpt-6.1-sol`, with xhigh reasoning effort,
repository tool use, and shell execution. The runtime does not expose
the context window size.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-04 11:36:04 -08:00
Dotta 89703c3ff7 fix(ui): place pinned sidebar pages above More (#18915)
## Thinking Path

> - Paperclip helps people manage AI agents and their work.
> - The Coordinator sidebar gives people access to workspace pages.
> - People can pin pages from the More menu.
> - The sidebar puts these links below More in a separate Pinned
section.
> - This change puts pinned links above More in the main navigation.
> - People can reach their saved pages without an extra section heading.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

The placement of pinned pages in the Coordinator sidebar.

**Subsystem affected**

ui/ — React board UI.

**Current behavior**

Pinned pages appear below More with a Pinned heading and extra space.

**Proposed behavior**

Pinned pages appear after Needs you and before More. They use the same
row spacing as the main links.

**Reason and benefit**

Saved pages belong with the main navigation. This placement keeps them
visible above the menu that contains the other pages.

**Breaking changes**

The Pinned section and its heading are removed. Saved pin data, link
order, routes, and pin controls use the existing code.

## What Changed

- Move pinned links into the primary navigation above More.
- Remove the separate section, heading, and extra space.
- Correct the feature guide to state that pinned pages appear above
More.
- Update sidebar tests to check placement, saved order, account changes,
restore, and pin removal.

## Verification

- `pnpm exec vitest run ui/src/components/CoordinatorSidebar.test.tsx`:
15 tests pass.
- `pnpm check:token-gates`: pass.
- `node scripts/check-maps.mjs`: pass after the guide correction.
- `pnpm --filter @paperclipai/ui typecheck`: pass.
- Browser checks pass in Chromium against the production Coordinator
sidebar in Storybook with fixture data. Screenshots cover saved pins and
pinning from More. No page errors occurred. This does not test a live
API.
- The full workspace typecheck and build stop because this host has no
Rust `cargo` command.
- The first UI build was killed and the browser crashed while checks ran
together. The host recorded OOM kills at its 8 GB limit. The browser
check then passed after the full-suite process was stopped. The UI
production build passed after the Storybook server was stopped.
- `NODE_OPTIONS=--max-old-space-size=4096 pnpm --filter @paperclipai/ui
build`: pass.
- `pnpm test:run`: stopped during the first chat integration shard after
memory exhaustion affected other checks. It has no full-suite result.
The CI run passed the test lanes on the latest head.

- [CI
run](https://github.com/paperclipai/paperclip/actions/runs/41768881396)
passed on `a836fc1320c363e0fbb5591312c6726026b6f151`. The required `ci /
verify` and `ci / e2e` checks pass. Build, typecheck, Rust checks, test
lanes, and all eight browser shards pass.
- Greptile: 5/5 on this head. The documentation finding is fixed and its
thread is resolved.
- The additional Paperclip review is paused and queued. It is not a
required branch check.

## Risks

Low risk. This change moves existing links. It does not change storage
or API behavior. The tests check expanded and collapsed rendering, the
empty state, stored order, unknown routes, user changes, and changes
from another browser tab.

## Model Used

OpenAI GPT-6 through the Codex agent. The session supports reasoning,
tool use, code edits, and command execution. The runtime does not expose
a more specific model ID or context window size.

## Checklist

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