3444 Commits
Author SHA1 Message Date
DottaandClaude Opus 5.5 630411af53 fix(recovery): require a 1 s start-time gap and treat EPERM as alive in owner proofs (#19848)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - After a restart, recovery code decides whether the process that owns
an in-progress run is gone
> - Two proofs compare process start times to the millisecond, but `ps
-o lstart` on macOS and BSD reports whole seconds
> - So a live owner can look like a reused PID, and recovery can release
it while it still runs
> - Signal-0 handling also disagrees: one check treats EPERM as dead,
while all others treat it as alive
> - This pull request puts one start-time rule and one EPERM rule in
`controller-process-identity.ts` and uses them at every site the Phase 0
ownership inventory named
> - The benefit is that no proof releases a live owner because of clock
precision or a permission error

## Linked Issues or Issue Description

**Bug description**
The Phase 0 ownership inventory (finding 1) found inconsistent "owner
gone" proofs. `native-failed-run-owner-release.ts`
(`recordedProcessMayBeLive`, `runProcessMayBeLive`) and
`native-session-executor.ts` (`verifiedRecoveryProcessIsAlive`) treat
any start-time difference of 1 ms or more as a reused PID.
`native-restart-recovery.ts` and `controllerProcessExited` require a gap
of at least 1 s. `local-service-supervisor.ts` (`isPidAlive`,
`isProcessGroupAlive`) treats EPERM as dead.

**Steps to reproduce**
1. On macOS, record a live process start time with millisecond
precision.
2. Run the abandoned-owner release pass, or the recovered-runner
liveness check, against that live PID.
3. The pass releases the owner, or the check reports the runner as gone.

**Expected behavior**
A sub-second start-time gap is ambiguous and keeps the owner. EPERM
means the process exists.

Refs #19846

## What Changed

- `controller-process-identity.ts`: export `processExists` (EPERM =
alive) and `processStartsDefinitelyDiffer` (gap >= 1 s; a missing time
proves nothing). `controllerProcessExited` uses the new helper.
- `native-failed-run-owner-release.ts`: the controller and run-process
checks use the shared helpers instead of exact millisecond equality.
- `native-session-executor.ts`: `verifiedRecoveryProcessIsAlive` accepts
a start time within 1 s as the same runner. Its signal path uses the
same check.
- `native-restart-recovery.ts`: `definitivelyDifferentProcessStart`
delegates to the shared helper. No behavior change.
- `local-service-supervisor.ts`: `isPidAlive` and `isProcessGroupAlive`
treat EPERM as alive.

## Verification

- New `controller-process-identity.test.ts`: the 1 s boundary, EPERM and
ESRCH for every signal-0 check, and a live spawned runner whose recorded
start is 500 ms off.
- `native-failed-run-owner-release.test.ts`: two new cases (controller
and run process) with a live child and a recorded start 500 ms off. The
owner stays `observed`.
- Red before the fix: with the old call sites, all 4 new cases fail
(`releaseAbandonedNativeOwners` returns 1 and releases the live owner;
EPERM returns false; the live runner reads as gone). Green after the
fix.
- `npx vitest run src/services/controller-process-identity.test.ts
src/services/native-runtime/native-failed-run-owner-release.test.ts
src/services/native-runtime/native-restart-recovery.test.ts
src/__tests__/local-service-supervisor.test.ts`: 59 passed.

## Risks

- Low risk. The change only makes proofs stricter: a sub-second gap or
EPERM now keeps an owner that the old code released. Such a row waits
for the existing lease, TTL, or real process exit.
- A recycled PID whose new process started within 1 s of the recorded
one now stays held until it exits. That is the same rule the takeover
path already used.
- Previous-release story: no persisted shape changes. Rows saved by the
previous release are evaluated with the stricter rule.

## Model Used

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

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-07 10:06:35 -06:00
DottaandClaude Opus 5.5 e2d490c61b feat(observability): anchor native time to first token at the accepted wake (#19838)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - When someone messages an agent, the reply must start fast. The
durable ownership plan (#19831) makes time to first token (TTFT) a hard
invariant.
> - Phase 0 of that plan records a TTFT baseline per adapter and
environment, measured from the moment the message or wake is accepted.
> - Today `task.time_to_first_token` starts at the wake comment, the
answered question, or the run row's creation. A wake request that was
accepted earlier (deferred, coalesced or retried) is not part of the
measurement, and the span does not say which start it used.
> - This pull request moves the start back to the earliest accepted
input and names it on the span.
> - The benefit is that the baseline and later budgets measure what the
user waits for, with no new work before `turn.start`.

## Linked Issues or Issue Description

Refs #19831

**What is the problem?**
`task.time_to_first_token` does not include the time between an accepted
wake request and the creation of its run row. It also does not record
which input it started from.

**What is the expected outcome?**
The span starts at the earliest accepted input and carries an `anchor`
attribute: `comment`, `question_response`, `wake_request` or
`run_created`.

**Why does it matter?**
Phase 0 of the durable ownership plan sets TTFT budgets from this
baseline. A start that skips queue time hides the latency that deferred
wakes add.

## What Changed

- `resolveNativeTimeToFirstTokenStart` (pure) in `native-run-trace.ts`
picks the start and the anchor. `anchor` joins the closed span-attribute
allowlist.
- `readNativeRunWakeRequestedAtMs` reads
`agent_wakeup_requests.requested_at` for the run. It never throws.
- `native-session-executor.ts` calls it only after the first deliverable
text, so it adds no wait before `turn.start`. The span now ends at the
text commit time, not after the trace write.
- Updated `doc/run-log-events.md` and `doc/observability.md`.

## Verification

- `cd server && npx vitest run
src/services/native-runtime/native-run-trace.test.ts
src/services/native-runtime/native-run-wake-anchor.test.ts` (26 pass).
- `cd server && npx vitest run
src/services/native-runtime/native-session-executor.test.ts -t first`
(41 pass). The existing TTFT test now asserts the anchor for each
ingress.

## Risks

- Low risk. This changes only the run-log and OTLP span. It is not
Telemetry (AGENTS.md §5.7), and it does not change runtime behavior.
- One indexed primary-key read per native dispatch, after the first
text. A read failure drops the anchor and keeps the old start.
- TTFT values can grow for deferred wakes. That is the intended
correction. `startOffsetMs` can be negative when the wake request comes
before the trace start.

## 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
- [ ] 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 <[email protected]>
2026-11-07 09:57:30 -06:00
DottaandClaude Opus 5.5 84908d176b fix(recovery): name the failed check in native_remote_recovery_lease_mismatch (#19839)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Remote native runs execute in a sandbox, and recovery can reattach a
run to its original sandbox runner
> - Reattachment checks the saved lease, workspace, and runner identity
in five places. Each failed check throws the same bare
`native_remote_recovery_lease_mismatch`
> - On staging, 34 OpenCode runs on 31 tasks failed with this bare error
in one evening. The run records could not show which check failed, so
the cause could not be confirmed from the run alone
> - This pull request makes every producer name the failed step and
field with closed values
> - The benefit is that an operator and the next investigation can see
which reattachment check failed, without new logging or identifiers in
the error

## Linked Issues or Issue Description

Refs #19399, Refs #19733, Refs #19755, Refs #19779

**What happened?**
Remote same-run retries failed with
`native_remote_recovery_lease_mismatch` and the task was blocked for
reconciliation. The error is thrown by environment acquire, environment
resume, workspace preparation, workspace sync, and remote runner
verification. All of them used the same bare string. The run's `error`
field and its run events did not show which check failed.

**Steps to reproduce**
1. Start reattachment for a remote native run whose saved lease,
workspace reference, or runner identity no longer matches.
2. Read the run's `error`.
3. It shows only `native_remote_recovery_lease_mismatch`.

**Expected behavior**
The error names the step and the field that differed, with no
identifiers, and existing consumers of the code keep working.

## What Changed

- New
`server/src/services/native-runtime/remote-recovery-lease-mismatch.ts`:
the code constant, a closed `stage` and `field` vocabulary, and
`remoteRecoveryLeaseMismatch(stage, field)`. It builds
`native_remote_recovery_lease_mismatch: stage=<stage> field=<field>`.
- `environment-run-orchestrator.ts` (stages `environment_acquire` and
`environment_resume`), `heartbeat/workspace-preparation.ts`
(`workspace_preparation`), `native-workspace-sync.ts`
(`workspace_sync`), and `verifyRemoteRunnerReattachment` in
`native-session-executor.ts` (`runner_verification`) now throw the
detailed form. Each check keeps the same condition and order. Only the
message changes.
- `restart-interrupted-turn.ts` uses the shared constant. It already
matches the code exactly or as the `code: ` prefix, so automatic
continuation after a reattachment loss is unchanged. No other consumer
matches the code.
- `doc/execution-semantics.md` documents the detail format.

## Previous-release state

This change adds no persisted state and changes no saved identity. Runs
that a previous release failed keep the bare
`native_remote_recovery_lease_mismatch` in `error`. Runs failed by this
release store the detailed form. The only consumer (`isReattachmentLoss`
in `restart-interrupted-turn.ts`) accepts both forms, and
`workspace-recovery-retry-copyback.test.ts` covers both.

## Verification

- Exact-detail assertions in the producer tests:
- `server/src/__tests__/environment-run-orchestrator.test.ts`: every
scope, status, expiry, provider, lease-ID, cwd, plugin-task, and resume
case asserts its stage and field. Against the previous producer, 21 of
these cases fail (the message was the bare code). With this change, all
57 tests pass.
- `native-session-executor.test.ts`: a replaced sandbox reports
`runner_verification`/`provider_lease_id`. A missing durable runner
identity reports `identity_run_id` before any sandbox read.
- `native-workspace-sync.test.ts`,
`heartbeat/workspace-preparation.test.ts`,
`native-controller-recovery.integration.test.ts` assert their exact
detail.
- End to end on embedded PostgreSQL:
`heartbeat-native-remote-failure-retention.test.ts` records
`native_remote_recovery_lease_mismatch: stage=environment_resume
field=lease` on the run when the provider no longer resumes the lease,
and it still requests the user's Retry once.
`workspace-recovery-retry-copyback.test.ts` adds a detailed-form row
that still continues once.
- The affected server test files pass locally. `pnpm --filter
@paperclipai/server typecheck` passes.

## Risks

- Low risk. Only the error text changes. Conditions, ordering, and
fail-closed behavior are unchanged.
- Logs and run errors show a longer message. The detail has closed
values only, so it adds no identifiers or secrets.
- A first version of this PR also changed remote retry selection. Review
showed that a plain resume in a new sandbox hits the remote workspace's
unsaved-work hold, and that a `destroy` disposition does not prove the
old sandbox is gone. That change is removed. It needs a separate design.

## Model Used

- Claude Opus 5.5 (Anthropic), model ID `claude-opus-5-5`, run in Claude
Code with tool use (shell, file editing, read-only browser access to
staging).

## Checklist

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

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native agents use tools to read reusable company skills.
> - The existing native skill tools read only skills assigned to the
agent.
> - An agent can stop when a task needs another skill that is already in
the company library.
> - This pull request adds tools to search and read accessible company
skills on demand.
> - Agents can use the saved instructions without a persistent
assignment change.

## Linked Issues or Issue Description

**Problem or motivation**

An agent cannot use an unassigned company skill through the standard
native skill tools. A generic API workaround needs extra discovery and
can return the full library.

**Proposed solution**

Add `search_skills` and `load_skill` to the native tool catalog. Search
returns small pages of stored metadata. Load returns text from a saved
version and lists its files. Both tools use the current run identity and
existing company and project access checks.

**Alternatives considered**

Assign each skill before a read, or use generic API discovery. Both add
work to a read that the company already permits.

**Roadmap alignment**

This extends the existing skills discovery path. It does not add a
store, permission model or installation flow. Related: #15409 adds an
assigned-skill loader to a different adapter.

## What Changed

- Add bounded company skill search. Search does not synchronize sources
or fetch repositories.
- Add versioned text reads for SKILL.md and supporting files. Page text
and file lists. Reject unsafe paths and binary files.
- Recheck access after storage reads. Keep the existing closed Muse,
skill-test and completion-review tool scopes.
- Add integration tests with isolated PostgreSQL and real local package
storage. Update the native tool count assertion and skill documentation.
- Limit text files to 16 MiB before body reads. Accept saved file paths
up to 4,096 characters plus the repository prefix. Return the response
digest only on the first page.
- Add optional byte limits to the package readers. Select only the
requested legacy file in PostgreSQL. Cast the size comparison to
`bigint` so existing callers can omit the limit.
- Cache decoded text and its digest for one run. Limit the cache to 64
MiB and 64 files. Check access on each page and clear the cache when the
run closes.
- Read repository source files and wrapper inputs through verified warm
reads or isolated recovery. These reads do not hold the company startup
lock.

## Verification

- Local verification tested tree
`7ba5828387ca0a9c2ee99655818a28de1235a854`, published as commit
`c9d04cba601541cca506f5b72aa6f7272a25379f`. The published tree matches
that tested checkout. Current head
`33fa3d3d4c7988288685ce7c6499d064e0cb9079` (tree
`8daaa2f3264d1cd65a9b6d85dbb9d3729ac4a29d`) removes six comment lines
from three source files; its executable code is unchanged from the
locally tested revision.
- Four native skill and authority Vitest suites: 72 passed and 7
existing sandbox checks skipped. All 20 skill-library integration tests
passed.
- Skill-library and package-store suites: 65 passed. This includes the
45 package-store cases and the same 20 integration cases. Ran with umask
`022` for executable-file tests.
- Server source and test typecheck layers passed in low-memory mode. The
affected source cycle also passed a final separate check.
- `git diff --check` and `node scripts/check-module-boundaries.mjs`
passed.
- Red/green evidence: the new default inline-read case reproduced
PostgreSQL error `22003`. Two max-size paging cases reproduced extra
body reads. Warm and cold repository reads timed out while another
connection held the company startup lock. All five cases pass after the
fixes.
- Both 16 MiB paging cases check that the second page performs no
additional body read. They also check that source-access revocation
blocks a cached page. Warm and cold lock cases load both the generated
wrapper and a supporting file while the startup lock remains held.
- The first cold-read test attempt had an incorrect helper import.
Corrected the test import, reproduced the lock timeout, then verified
the fix.
- [CI on the previous
revision](https://github.com/paperclipai/paperclip/actions/runs/42319652795)
passed typecheck, build and policy. Serialized shard 2 failed in the
unchanged test `schedules bounded retries for failed accepted
interaction continuation wakes`. The same case passed locally: 1 passed;
416 cases were excluded by the filter. The cause of the earlier CI
failure remains unknown. [CI for
c9d04cba](https://github.com/paperclipai/paperclip/actions/runs/42323260426)
passed: 52 successful jobs and one skipped selector, including
typecheck, build, all serialized server shards, native server tests,
Runner/Rust, and e2e. This establishes a later pass; the earlier
heartbeat failure's cause remains unknown. [CI for current head
33fa3d3d](https://github.com/paperclipai/paperclip/actions/runs/42325148224)
passed: all 52 applicable workflow jobs succeeded, with one skipped
selector. The head has 58 successful check runs and three skipped
checks; Snyk also passed.
- Earlier failures remain recorded. Preflight cannot build Rust because
Cargo is absent. The first catalog assertion expected 51 tools instead
of 53. Test startup needed the plugin SDK build. An earlier typecheck
hit the 8 GiB memory limit; low-memory checks passed. The previous
package test failed under umask `077`; it passed under `022`, and this
run's complete package suite passes with `022`.
- Greptile reviewed current head `33fa3d3d` at 2026-11-07 01:40 UTC: 5/5
with no outstanding findings. The three older Ponytail threads are
resolved after checking the size/cache, long-path, and bigint fixes and
regression tests. Ponytail's supplemental refreshed assessment stopped
before completing and posted no new findings. Its earlier findings are
fixed and all seven review threads are resolved. The completed Greptile
assessment covers current head.
- No hosted deployment or live provider run was performed.

## Risks

- Package reads can recover stored packages through the existing storage
layer. Search reads only stored database metadata.
- Load is a read. It does not install files into the provider workspace
or change runtime bundles. It does not grant tool access or authorize
actions in the skill text.
- Callers must retain the returned version ID for later pages. A missing
version returns an error.
- Files above 16 MiB return a size-limit error. The run cache retains at
most 64 MiB of decoded text and 64 entries. A cache miss or eviction
requires a full verified file read. Each page still checks current
access. The SHA-256 response field is null after the first text or
file-list page.
- No database migration or UI change is required.

## Model Used

- OpenAI Codex, GPT-6 family, with reasoning, repository tools and code
execution. The exact model ID and context window are not exposed to this
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
2026-11-07 09:23:14 -06:00
DottaandPaperclip b8e02fc2b4 fix(native): preserve bounded larger web inventories (#19832)
## Thinking Path

> - Paperclip manages AI agents and preserves their work across server
changes.
> - Native harness preservation stores retained web-server files in
authenticated archives.
> - A retained skill can contain hundreds of valid local files.
> - The WEB evidence owner rejected any directory or inventory above 128
entries.
> - This blocked preservation of two 462-file skills even though their
payload fit the existing byte limits.
> - This pull request shares finite inventory bounds between capture and
archive validation.
> - It preserves every file and keeps the execution, identity, writer,
and publication guards.

## Linked Issues or Issue Description

Refs: #19814

**What happened?**

WEB evidence preservation rejected a retained source with 927 local
skill files. The source had settled execution ownership. The files fit
the existing payload limits. The directory enumeration failed before
archive publication because its entry limit was 128.

**Expected behavior**

Preserve a valid finite inventory within explicit file, payload, path,
and metadata limits. Keep remote recovery obligations unchanged.

**Steps to reproduce**

1. Retain two 462-file skills and one three-file skill under a settled
native source.
2. Request canonical WEB evidence preservation.
3. The old owner rejects directory enumeration. The added integration
test reproduces this refusal with real files and PostgreSQL.

## What Changed

- Share the WEB limit through the existing controller archive owner:
10,000 total entries, 4 MiB of manifest metadata, and 4,096 bytes per
path.
- Apply the limits to filesystem capture, published binding validation,
and archive descriptor validation.
- Keep the existing 16 MiB per-file and 64 MiB payload bounds. Keep
authority history at 128 entries and 32 epochs.
- Add real archive and cold-restore coverage for 927 files, legacy
manifest compatibility, and bound refusals. Document the limits.

## Verification

- Genuine baseline RED: the new 927-file integration case reaches the
original `captureWebEvidence.directoryNames` limit and fails before
publication.
- The initial setup attempt failed before test collection because the
generated plugin SDK was absent. The required SDK build fixed setup; it
was not counted as a regression result.
- The first affected three-suite run passed 93 cases. The large
roundtrip exceeded the generic 15-second case deadline. That failure is
retained. The isolated large case passed with a 60-second case limit,
but full CI later exceeded that limit while the other 4,577 tests in its
shard passed. Both timeout failures are retained. The final fixture
keeps all 927 files and 18.5 MB, uses bounded 16-way independent
comparisons, and has a 120-second whole-case limit. The canonical
preservation signal remains 60 seconds. No production deadline or
assertion changed.
- The 927-file case then passed: encrypted archive, authenticated cold
restore, all bytes/hashes/modes, unchanged remote leases, and
database-only validation with source volume absent and storage reads
forbidden.
- The final metadata repair adds a genuine RED case: an entry list below
4 MiB previously emitted a larger complete manifest without rejection.
The owner now checks the complete serialized manifest. All ten focused
cases passed after that repair. The final bounded-I/O fixture passed the
same ten cases in 37.26 seconds, including the full 927-file roundtrip.
All 93 other affected cases passed in the preceding three-suite run.
Fresh required CI supplies broad final-head coverage.
- Server TypeScript, module boundaries, and whitespace checks pass.

Run the focused PostgreSQL checks from `server/` with the repository
wrapper:

```sh
node ../cli/node_modules/tsx/dist/cli.mjs ../packages/db/src/test-postgres-command.ts -- node ../node_modules/vitest/vitest.mjs run src/services/native-runtime/native-harness-filesystem-independence.integration.test.ts --testNamePattern 'preserves all 927|existing 128-entry WEB|WEB manifest beyond|oversized WEB filesystem' --hookTimeout 90000 --maxWorkers 1 --fsModuleCache
```

## Risks

The owner can hold more entries in memory. The fixed 64 MiB payload, 4
MiB metadata, path, and entry bounds constrain that cost. The archive
format and old receipt bytes do not change. Older builds still have the
128-entry limit and cannot qualify a larger new receipt. This change
does not settle remote recovery, grant execution authority, alter
finalizer locks, or change publication CAS rules. No hosted preservation
or deployment was performed for this PR.

## Model Used

OpenAI `gpt-6.1-sol`, with code execution and repository tools. 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 <[email protected]>
2026-11-07 09:11:00 -06:00
DottaandPaperclip 061c84d280 fix: preserve local copies behind settled remote recovery holds (#19830)
## Thinking Path

> - Paperclip manages AI agents and the work they save.
> - Online migration must save legacy local files while other remote
work continues.
> - A failed native run can retain a remote recovery hold after its
local writer has settled.
> - The current source proof recognizes only some of those recovery
producers.
> - This blocks stable local files and their shared Git aliases behind
an interrupted session or exhausted export recovery.
> - This change admits two exact producer shapes under the existing
physical and task locks.
> - It preserves the failed run, accepted result, remote hold, retained
allocations, and recovery history.

## Linked Issues or Issue Description

Refs #19642, #19688, #19810. A public search found these related
changes. This is a narrow correction to their local preservation proof.

**What happened?**

Online workspace import rejects a stable local source with
`online_workspace_source_scope_unproven` when its prepared native source
has an ambiguous session interruption hold or an exhausted file recovery
hold. Shared Git aliases inherit the refusal.

**Expected behavior**

Save the stable local copy under the existing source locks. Keep remote
recovery blocked until its own recovery owner supplies the required
evidence.

**Steps to reproduce**

1. Create a failed native run with an unowned terminal finalizer and an
exact prepared allocation.
2. Keep either its board-owned interrupted-session failure block or its
exhausted system recovery and blocked file recovery with an accepted
result.
3. Import the local Git source or a shared alias through
`migrateRemoteWorkspace`.
4. Before this change, both actual importer tests reject the source.
With this change, both save and restore its files while retaining the
hold.

**Paperclip version or commit**

Reproduced on master at `b2af45f0f1` with local PostgreSQL 18.1 on macOS
arm64 and Node 26.4.0.

## What Changed

- Reuse the exact prepared-source failure proof for ambiguous native
session interruptions.
- Admit an exhausted export hold only with its exact run, blocked task,
unowned finalizer, accepted result, original allocation, and current
recovery receipts.
- Bind the complete accepted result into the publication fingerprint.
Keep the existing source, alias, Git, finalizer, and task locks.
- Add real importer, authenticated restore, refusal, and
publication-race controls. Document the separate local and remote
obligations.

## Verification

- Baseline: the exhausted-export importer case failed at
`remote_workspace_migration_blocked:online_workspace_source_scope_unproven`.
- Review found that the first interruption fixture used the
artifact-failure resume mode. The corrected fixture reads
`nativeProviderRecoveryEvidence` and `nativeSessionFailureDisposition`.
Its canonical ambiguous interruption case failed at the source guard
before the cause-specific mode repair.
- Fresh recorded metadata then showed that an interrupted source can
have a boolean `providerSessionEstablished: false`. The final fixture
derives that exact false+ambiguous state from a checkpoint without a
provider identity or provider events. It failed against the intermediate
shared-true guard. The final cause-specific proof passes it. The
artifact-failure case still requires true+exact mode.
- Both importer cases remove the originals and restore saved files
through the real encrypted snapshot owner. They keep the failed run
status, native source reference, result bytes, tasks, recovery actions,
retained allocations, and unrelated active remote work unchanged. The
existing migration owner adds only its local source recipe.
- Final affected PostgreSQL suites: **107/107 passed**, 74.44 seconds,
canonical shared PostgreSQL wrapper with one worker and cleanup
complete. Controls reject the wrong interruption mode, a string session
flag, unreleased owners, active controllers, stale fingerprints, changed
allocations, changed recovery state, and changes during upload. Command
from `server`: `node ../cli/node_modules/tsx/dist/cli.mjs
../packages/db/src/test-postgres-command.ts -- node
../node_modules/vitest/vitest.mjs run
src/services/remote-workspace-online-source.test.ts
src/services/online-legacy-workspace-recovery.test.ts --fsModuleCache
--maxWorkers=1 --hookTimeout=90000`.
- Final server source and all test typecheck layers passed. Module
boundary check and `git diff --check` passed.
- A first setup attempt could not load the unbuilt plugin SDK and ran
zero tests. Building the SDK corrected the setup. An unchanged-baseline
run repeated source refusals after a local edit used the wrong path. The
first 105-case and 106-case passes predate the final producer correction
and are superseded by the 107-case pass. Earlier failures and passes are
retained separately.
- A read-only probe loaded the exact committed proposed inspection code
against three current stored holds in one repeatable-read snapshot. All
three passed the actual function. It verified the owned accepted result
and both interruption fields without changing source or provider state.
This is applicability proof; an import must still acquire its locks and
recheck after deployment.
- The first final-head CI run failed browser shard 5 at
`cloud-auth.spec.ts:34` with an APIResponse disposed during route
teardown. After the workflow completed, one scoped failed-job retry
passed that shard and its browser aggregate on the same head. The
initial failure is retained. No code, test, timeout or assertion changed
for that retry.
- `verify --explain` returned 2 because the documentation path requires
broad CI. Required CI passed on
`2050fa058cabe75e3d8f64f1f6bdb4b9b2fd0873`. Fresh Greptile review is 5/5
with zero open threads. No hosted workspace, provider, task, or recovery
state was changed for this PR.

## Risks

The new proof could accept an inconsistent recovery producer. Exact
company, run, task, allocation, action, result, and receipt checks keep
uncertain sources blocked. Changed evidence during capture prevents
publication. A saved local snapshot is not proof that remote files were
recovered, and this change never creates such a receipt.

## Model Used

OpenAI GPT-6 (Codex), with reasoning, tool use, and code execution. The
runtime does not expose a more specific model ID or context window.

## Checklist

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

---------

Co-authored-by: Paperclip <[email protected]>
2026-11-07 08:57:39 -06:00
DottaandClaude Opus 5.5 a94881d1a7 fix(heartbeat): save a comment refused by a temporary hold and route post-quarantine input to its own gate (#19829)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A user's follow-up comment or Retry must be delivered once,
eventually, even when a temporary company admission hold (accounting
still settling) is active
> - #19741 kept claimed runs queued through such a hold and woke held
work when it cleared
> - A restart-chaos wave still lost a comment, because its wake was
refused before admission and written as a terminal skip
> - The same wave left a Retry waiting forever for a stop record of a
process that never existed
> - This pull request saves the refused comment as a deferred receipt,
and routes input after a pre-launch quarantine failure to the
quarantine's own gate
> - The benefit is that the comment is delivered after the hold, and the
task asks for the action that actually unblocks it instead of offering a
Retry that can never start

## Linked Issues or Issue Description

Refs #19741 and #19702.

**What happened?**
In a restart-chaos wave, a failed run's accounting held the company for
600 s.
1. A user commented while the task's run was live. The enqueue budget
gate refused the comment's wake before it reached the deferral behind
the live run. Because it was a user request with no saved receipt, it
was written as `skipped` (`budget.blocked`). The live run finished
without it and the task ended `done`, so the comment was lost.
2. A Retry created a run that the native cleanup-quarantine fence
refused before it requested any runner launch
(`native_session_cleanup_quarantined`, no process events). A second
Retry then waited with `process_identity_missing` ("The previous run has
no verified stop record") for a process that never existed. The task
kept offering Retry, and that Retry could never start.

**Expected behavior**
A comment refused by a temporary hold stays saved and is delivered once
later. Input after a run that was refused before launch is handled by
the quarantine gate: Retry is not offered (send a new message), and a
new message gets the verified-stop check of the previous provider
session.

## What Changed

- `heartbeat/queue.ts`: when the enqueue budget gate refuses a user's
comment wake and the block is temporary (not a reached hard stop, per
`isBoardOwnedBudgetBlock`), the wake is saved as a
`deferred_issue_execution` receipt. The receipt carries the comment ids
and an `executionWait` of `budget.blocked`, and the gate notifies the
deferred-receipt worker and throws `PersistedExecutionWaitRefusal`. It
is not written as `skipped`. Normal promotion (when the live run ends)
and #19741's hold-release wakeups deliver it.
- `native-local-process-stop.ts`: new `nativeRunRefusedBeforeLaunch`. It
returns true for a failed native run with
`native_session_cleanup_quarantined`, no recorded process, and no
launch-request or identity event.
- `explicit-native-continuation.ts`: such a run passes the
process-identity check, so the existing quarantine branch decides. A
Retry gets `cleanup_quarantined` (shown with `canRetry: false` and "send
a new message"). A new message gets
`verifyStoppedNativeSessionForContinuation`. Any launch request or
identity record keeps the original stop proofs.

- `native-session-executor.ts`
(`verifyStoppedNativeSessionForContinuation`): when the source has no
process record and no launch event, the verifier relies on the previous
owner's stop evidence. Before this change it required the source to
share the owner's process identity, so a new message stayed held even
after the owner stopped. A source that carries a process identity must
still match the owner exactly.

## Verification

Each new test fails on `master` and passes with this change, on embedded
Postgres:
- `server/src/__tests__/heartbeat-held-comment-wake.test.ts`: a comment
whose wake the hold refuses at enqueue is saved with a `budget.blocked`
wait and is not skipped. It is delivered exactly once after accounting
settles.
- `server/src/services/explicit-native-continuation.test.ts`:
- A run refused by the quarantine before launch routes a new message to
the quarantine's stop verification and a Retry to `cleanup_quarantined`.
These failed on `master` with `process_identity_missing`.
- Guards (green on both): a quarantined run that requested a launch, and
another failure without a process record, still require
`process_identity_missing`.
- `server/src/services/quarantined-task-retry.test.ts`: a never-launched
quarantined source is certified once the previous owner has stopped (red
on `master`). It stays held if the source requested a launch or the
owner is still running. All 31 tests pass, and the `stopped native
conversation physical cleanup` suite passes (46).
- Related suites pass locally (6 files, 620 tests): explicit native
continuation, held comment wake, queued comments routes, comment wake
batching, refused assignment wake, deferred receipt work. Two
`deferred-execution-receipt-work` tests failed once under parallel load
and passed when run alone.

## Risks

- The saved-comment path applies only to user comment wakes refused by a
temporary budget hold. Hard stops, system wakes, case handoffs, durable
chat requests, and existing saved receipts are unchanged.
- The pre-launch rule is limited to the quarantine failure code with no
launch evidence. Every other missing process identity still requires the
existing stop proof.

## Model Used

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

## 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 <[email protected]>
2026-11-07 08:54:14 -06:00
DottaandPaperclip 2e52dfa864 fix(slack): retain DM thread tasks after completion (#19486)
## Thinking Path

> - Paperclip manages AI agents and their work.
> - Slack conversations store that work in tasks and comments.
> - A native thread must keep its task when work finishes.
> - The server treated all DMs as linear conversations.
> - A reply in a completed DM thread therefore started a new task
without its earlier comments.
> - This pull request uses the native thread identity for task
continuity.
> - People can continue the same work with its saved context.

## Linked Issues or Issue Description

**What happened?**

A reply in a Slack DM thread created a new task after the preceding task
was Done or Cancelled. It also created a new task when the saved
conversation was completed. The new task lacked earlier comments.

**Expected behavior**

A native Slack thread keeps one task in channels and DMs. Later replies
retain earlier user and agent comments. Top-level DMs keep their
existing task generation rules.

**Steps to reproduce**

1. Start work in a Slack DM thread.
2. Mark its task Done.
3. Reply in the same thread.
4. Before this fix, the reply creates another task.

**Paperclip version or commit**

Reproduced from master at eb5c24b61c.

**Deployment mode**

Built from source. Tests use local PostgreSQL and provider fixtures.

Related PR: #16151 covers the DM mention exemption. This fix preserves
that behavior. GitHub searches found no duplicate fix for completion
routing.

## What Changed

- Detect Slack native thread roots in the Slack module.
- Classify native DM threads as persistent threads for task routing.
- Select the resource type from the actual DM destination. Keep DM
access checks.
- Preserve DM task controls. Select the latest DM task even after
completion. Require a delivered control receipt before an explicit new
task generation.
- Add completion, duplicate, saved-history, separate-root, Idle, and
explicit-control regressions.
- Document the task boundary. Correct only the inaccurate DM-thread
statement in the existing feature recipe.

## Verification

- Before the production edit, the new PostgreSQL regression failed in
all three threaded-DM completion cases: two conversations existed
instead of one. The channel-thread and top-level-DM controls passed.
- The initial eight routing cases passed. CI then found five existing
slash-control failures. All five reproduced locally. Greptile reported
the same regression at 4/5.
- The first correction passed all 16 focused PostgreSQL cases. Secondary
review then found that slash commands skipped Done, Cancelled, and
completed DM tasks. Four new cases failed before the lookup change.
Commit 23ff4f04f passed all 20 focused cases, including status, explicit
new/close, delivered receipts, and a new task generation. The original
same-thread continuity cases still pass.
- Command: `pnpm exec vitest run
server/src/__tests__/chat-channels.integration.test.ts -t 'preserves
Slack thread history across completion|keeps separate Slack DM thread
roots|returns two answered Slack turns to Idle|binds Slack DM slash
controls|treats D-prefixed Slack status|serializes duplicate Slack
admission with an owned provider-reply settlement|suppresses a queued
provider reply when destination reach|continues to accept authenticated
Slack slash close/new'`.
- Map validation and `git diff --check` passed. Map validation proves
structure only.
- All 107 neighboring Slack adapter, SDK runtime, and receipt tests
passed across three files.
- The initial complete server typecheck passed with
`PAPERCLIP_TYPECHECK_LOW_MEMORY=1 pnpm --filter @paperclipai/server
typecheck`, including all three test-tree shards.
- Full local recursive typecheck and build stopped because this
workspace has no `cargo`. A separate default server typecheck was killed
under concurrent memory load; its lower-memory rerun passed. The broad
local test run was interrupted to free memory, so no full-local-suite
pass is claimed. The complete server typecheck passed again for
correction 367b6f9b3, including all three test-tree shards. The [full CI
pipeline](https://github.com/paperclipai/paperclip/actions/runs/42250458200)
passed for exact head 367b6f9b3f. This
includes typecheck, build, all chat and other test lanes, browser E2E,
Runner, and aggregate verify. The first CI run passed broad typecheck,
build, E2E, Runner, and other test groups, but its chat failures remain
recorded above.
- The initial regression launch could not load the unbuilt plugin SDK.
It ran after building the required workspace dependencies. A test-only
resource assertion was corrected to read persisted DM resources, which
the settings list omits. The completed-conversation case preserves an
existing review status.
- The initial correction test used a follow-up timestamp older than the
delivered control. The chronology gate correctly rejected it. A current
provider timestamp now verifies the new generation without weakening
that gate.
- Greptile's map finding was addressed by removing recipe expansions and
all agent-access map changes. Both review threads have replies and are
resolved. Greptile reviewed exact head
367b6f9b3f at [5/5 with zero new
findings](https://github.com/paperclipai/paperclip/pull/19486#issuecomment-6689117840).
The secondary Paperclip Review rated that head 4/5 and found the
completed-task slash lookup error. Commit 23ff4f04f fixes this finding.
Greptile and Paperclip Review both rate head
23ff4f04f6 at 5/5. All three review
threads are resolved. The [full CI
pipeline](https://github.com/paperclipai/paperclip/actions/runs/42255926149)
now passes on this exact head. This includes typecheck, build, all test
lanes, browser E2E, Runner checks, Canary Dry Run, and aggregate verify.
All checks are terminal: 54 success, 1 neutral, and 3 skipped. Snyk also
passes. The PR has no merge conflict.
- The complete server typecheck passed again on 23ff4f04f, including all
three test-tree shards.
- The new test first asserted the conversation-list state. That API
displays terminal tasks as completed before the control is sent. The
test now reads stored binding state to check that completion waits for
the delivered receipt.
- No live Slack send or deployment was performed.

## Risks

- Native DM threads now reopen the existing task after Done or
Cancelled. Explicit new/close controls still release the binding after
their response is delivered. Top-level DMs still start new generations
after completion.
- Existing split tasks stay separate. Future replies use the latest
saved binding. This change does not merge old tasks or copy surrounding
DM history into a newly created thread.
- No schema, credential, provider scope, or permission changes are
required.

## Model Used

OpenAI Codex, based on GPT-6, with reasoning, tool use, repository
editing, and code execution. The runtime does not expose a more precise
serving model ID or context-window size.

## Checklist

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

---------

Co-authored-by: Paperclip <[email protected]>
2026-11-07 08:35:29 -06:00
Nicky Leach 45c2bd94c6 fix(native): name the interrupted attempt when an exact resume is refused (#19709) 2026-11-07 06:21:46 -08:00
DottaandClaude Opus 5.5 8373ae905a fix(accounting): settle a never-started run whose recorder bound an empty fence map (#19826)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Budgets hold new work while a native run's final usage receipt is
outstanding
> - A run that never started a provider has nothing to account. #19700
added a rule that settles such a run at once.
> - In restart chaos waves, Retry runs that failed before any provider
start still held company admission for up to 10 minutes
> - This pull request fixes the condition that stopped the rule from
matching
> - The benefit: a failed run that provably never started a provider
settles at once and never holds other work

## Linked Issues or Issue Description

Refs #19700, #19724.

**What happened?**
An OpenCode Retry run failed at once with
`native_session_cleanup_quarantined`. It had no model, no usage
receipts, no process start and no provider events, yet it held company
admission for the full 600-second closure bound. Two more waves had the
same shape, with holds of 309 and 324 seconds.

**Root cause**
`nativeProviderWorkNeverStarted` (`run-cost-accounting.ts`) required
`usage_json.accountingReceiptSourceFences` to be absent.
`createRunUsageRecorder` binds every run with that key, as an empty map
`{}`, even when no earlier receipt source exists. So the rule never
matched a run that had a usage recorder. The run then waited for the
closure bound as a run whose receipt was still closing.

The restart-evidence check for the unpriced-usage exemption
(`budget-exposure.ts`, from #19724) had the same flaw: it treated the
presence of the key as restart evidence.

**Expected behavior**
A failed native run with no receipts, no replaced receipt source and a
process that never launched (or was verified stopped) settles at once
with no charge. A run whose provider might still be spending keeps
holding.

## What Changed

- `run-cost-accounting.ts`: the never-started rule treats an empty fence
map as "no replaced source". Only a non-empty map shows that provider
work began.
- `budget-exposure.ts`: restart evidence also requires a non-empty fence
map.
- `doc/SPEC-implementation.md`: documents the rule.

## Verification

- Red then green, `closed-native-receipt-accounting.test.ts`:
- "settles a quarantined Retry whose recorder bound an empty fence map
at once". This is the exact chaos row: failed,
`native_session_cleanup_quarantined`, terminal_failure finalization,
receipt sequence 0, empty fence map, no receipts, no process events.
- "does not count an empty fence map as restart evidence for a timed-out
closure".
- Existing tests still pass. A run with a recorded but unverified
process still waits for the bound. The restart-derived exemptions still
apply to runs with a non-empty fence map or a recovery transition.
- Local: `closed-native-receipt-accounting` (51), the budget suites,
`heartbeat-cost-accounting`, `adapter-cost-failure-paths`,
`native-stop-accounting` and `native-session-executor` all pass. Server
`tsc --noEmit` is clean.

## Risks

- Low. The change only narrows what counts as a replaced receipt source.
A run whose source was really replaced has a non-empty map and behaves
as before.

## Model Used

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

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-07 08:15:49 -06:00
Dotta 3112e4c955 Verify original Dot OAuth authority for signed Cloud requests (#19819)
## Thinking Path

> - Paperclip manages AI agents and their work.
> - Dot uses a user OAuth grant to act in one company.
> - Cloud must use that original grant when it resolves a selected
user's identity.
> - A Cloud signature alone must not create user or board authority.
> - The tenant's OAuth owner must check the token and its current
restrictions.
> - This pull request adds a signed private HTTP boundary for that
check.
> - The result contains only the checked identity fields. It grants no
task or board access.

## Linked Issues or Issue Description

**Subsystem affected**

Server external-agent identity, public MCP OAuth, Cloud control
verification, and HTTP logging.

**Problem or motivation**

Cloud needs a private way to resolve the original Dot token for the
selected user, client, company and resource. The existing generic Cloud
control route creates an operator actor. That route is not the right
owner for this identity check.

**Proposed solution**

Add `POST /mcp/runner/oauth/cloud-principal`. Require the existing Cloud
signature and replay verifier. Bind the whole request to a new, closed
`mcp-dot:principal` action with a maximum 60-second lifetime.
Authenticate the original token once through the normal Dot OAuth owner.
Check current namespace authority before and after that call.

**Alternatives considered**

A Cloud-created grant or a board actor would change the authority owner.
Neither is used. The private route reuses the existing OAuth and Cloud
identity owners.

**Roadmap alignment**

This is a narrow boundary for the existing MCP and Cloud areas in
`ROADMAP.md`. It adds no work queue, public hosted profile, or provider
enablement.

Related public work: Refs #14846, #19427, #19750 and #19776. Searches
for the exact private path and action found no duplicate PR. The
matching private Cloud caller has merged. The current App head passed
its required checks and review.

## What Changed

- Add an original-token principal reader. Keep the original client,
selected user, company, resource, purpose, scope, membership and feature
restrictions.
- Add a 16 KiB raw JSON private route. Reject compressed, invalid UTF-8,
malformed and unknown-field bodies. Bind an ordered whole-body digest to
the signature and existing nonce fence.
- Compare the current stack, claim, instance, origin and connector
identity before and after OAuth. An absent owner or namespace fails
closed.
- Mount the route in normal `createApp` after hostname, runtime-claim
and standby checks, before session/actor work. Do not add it to generic
board-control routing.
- Keep private request and response logs content-free, including raw
buffers, headers, query data and reflected errors.
- Add controlled HTTP/privacy tests and a real normal-App
OAuth/Cloud-signer integration case. Document the machine-only contract
and correct the existing `assistant-connections/client-use` access
entry. No new capability ID or agent API entry is added. No schema,
lockfile, workflow, Runner, Muse or useful-task fixture changes belong
to this PR.

## Verification

- Current `70c03593`: normal module-boundary policy and all seven App
compiler programs passed. The public module factory has an explicit
portable Express return contract. Whole HTTP/log controls passed again:
30/30. The existing real paired OAuth/Cloud-signer/private HTTP case
then passed again on a fresh PostgreSQL fixture: 1/1, zero skipped. All
known child processes and five database connections closed; the final
independent SQL audit was empty.
- The original remote policy failure rejected an App import of a
module-internal path; dependent E2E/verify gates stopped on that
failure. The fix delegates through the public module API. A first local
declaration run reported TS2883 for its inferred return type; the
explicit public type fixed it. Original logs remain preserved. No
checker, compiler option, test body or authority check was weakened.

- The normal full App graph passed on the integrated source. The later
fixture-only repairs each passed the complete affected owning
program:405 entries, including 403 source roots. The remaining programs
and own dependency outputs carried only where their source was exact.
- Whole private HTTP/log files passed:30 tests, zero failures or skips.
The HTTP controls use a controlled OAuth owner with real signatures. The
log controls use the actual HTTP logger.
- The paired Cloud source passed its normal strict build and whole 26
signer/client/namespace and credential-error controls. These are
controlled caller tests, not provider tests.
- One normal-App paired case passed with a dedicated PostgreSQL
database. It used real DCR 201, PKCE, consent, token exchange and a
persisted unpaired grant; actual signed metadata publication and a
committed Cloud receipt; the real Cloud signer/client and actual App
HTTP; exactly-once authentication; refresh; current namespace
restrictions; and revocation. The Cloud registry/provider were the
existing controlled in-memory/Fake fixtures. Recorded inbound Hosts
matched, with zero metadata overflow. No grant, readiness or mirror ACK
was fabricated.
- The original principal helper's 21 controlled tests and two real-OAuth
tests passed before the HTTP slice. Those three files remained unchanged
through integration. They are separate source-scoped evidence, not an
extra combined run.
- Preserved failures: a helper import and test typing error were
corrected before the passed checks. An earlier suite failure did not
serialize its hook error and remains unexplained. Later runs captured a
missing normal managed-config initialization and HTTP 404 from the
loopback mapper. The fixture now calls the normal initialization owners
and uses the existing Host-preserving canonical HTTP adapter. All
original authority assertions and deadlines remain.
- Reproduce the local files with `pnpm --filter @paperclipai/server exec
vitest run src/modules/external-agents/cloud-principal-http.test.ts
src/modules/external-agents/cloud-principal-log.test.ts --maxWorkers=1
--no-file-parallelism`. The real integration file is
`server/src/__tests__/cloud-principal-normal-app.postgres.test.ts`; it
requires its explicit dedicated test DB and an own compiled matching
Cloud checkout.
- The normal `node scripts/check-maps.mjs` check passed for 35 areas and
192 capability IDs. This validates map structure, not business
acceptance. The later edits are documentation/map only; all executed
business source and own outputs from the paired case remained exact.
- Current-head remote CI passes: 59 successful checks and three
intentional skips. Greptile reviewed this exact head at 5/5 with no
findings or review threads. The prior Ponytail review was Ship 5/5 at
the earlier head; no current-head Ponytail turn is present, which is
nonblocking under the authorized review policy. The three
full-repository commands were not repeated locally; the focused normal
App graph and affected tests above passed, and required remote CI
supplies the broad gates. No deployed, provider-capacity, cold-wake or
full hosted lifecycle result is claimed.

## Risks

- This is a security boundary. Signature, current namespace and original
OAuth restrictions must continue to fail closed. The response gives
identity only; callers still need normal operation authority.
- The request carries the original token. The reserved route uses a
closed log projection on both accepted and refused requests. Privacy
regressions remain a material risk.
- Both repositories must agree on the action and digest contract. Older
code rejects the unsupported action; no fallback creates authority.
- This is a prerequisite private boundary. The remaining hosted
discovery, wake, capability, operation, event, retry/reactivation and
revocation lifecycle work, plus Muse, remains open. No profile or
capability is enabled here.
- No migration is needed. Rollback removes the private boundary and
paired caller; normal OAuth and Personal MCP remain with their existing
owners.
- The engineering documentation describes this private machine boundary
and its access restriction. No operator-facing feature is enabled.
Documentation for full hosted enablement remains with that later work.

## Model Used

OpenAI `gpt-6.1-sol` (Sol6.1), through Codex, produced the source and
tests with tool use and local compiler/test execution. A GPT-6-based
Codex coordinator reviewed the work. Its exact model ID is unavailable.
Context window sizes are unavailable.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (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-07 08:02:42 -06:00
DottaandClaude Opus 5.5 b2af45f0f1 fix(runner): let OpenAI API-key Codex agents generate images with their existing key (#19654)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - New users often connect a Codex agent with an OpenAI API key during
onboarding.
> - A user asked such an agent for a logo. The agent said that the
built-in image generator was not available and that its fallback needed
a separate API key. It then drew an SVG by hand.
> - #19516 removed `--disable image_generation`. That change is correct,
but it does not reach this path. The pinned Codex 0.160.0 shows its
`image_gen` tool only when Codex signs in with ChatGPT. The check is
`image_generation_available` in `codex-rs/core/src/tools/spec_plan.rs`.
An API key never gets the tool.
> - The runner keeps the provider key out of the agent's shell on
purpose, so the skill's `scripts/image_gen.py` fallback cannot run.
> - This pull request adds a Paperclip `generate_image` tool. The server
makes the image with the agent's existing OpenAI key. The key never goes
to the agent process or the shell.
> - The benefit is that API-key Codex agents can make images with no
second key. The spend goes to the agent's budget and the budget can stop
it.

## Linked Issues or Issue Description

Refs #19516

**What happened?**
An agent connected through onboarding with an OpenAI API key (Codex
harness, `gpt-5.6-sol`) could not generate images. The agent said: "The
image skill's built-in generator isn't available in this run, and its
fallback would require a separate API key." It then drew an SVG instead.

**Steps to reproduce**
1. Create a company with a Codex agent that uses an OpenAI API key.
2. Ask the agent for a raster image, for example a logo PNG.
3. The agent reports that image generation is not available. It does not
make the image.

**Expected behavior**
The agent makes the image with the OpenAI key that already powers it.

**Root cause**
The agent ran on the native runner's Codex app-server path, with
`auth.json = {"OPENAI_API_KEY": ...}`. Codex 0.160.0 removes
`image_gen.imagegen` from the model's tools unless
`current_auth_uses_codex_backend()` is true. That function is true only
for a ChatGPT sign-in.

I proved this with the real pinned binary. I gave it the runner's
isolated launch arguments and the API-key auth file, and pointed it at a
local Responses endpoint. The first model request offers only `exec`,
`wait`, `request_user_input` and the collaboration tools. #19516's test
checked only `codex features list`, so it could not see this gate.

## What Changed

- **New `server/src/services/native-runtime/generate-image-tool.ts`:**
  - The tool schema and definition.
- `imageGenerationBindingForRun`. It applies only to native Codex runs
on a direct OpenAI `api_key` connection with no router selection.
- The OpenAI Images API call (`gpt-image-2`, `b64_json`, PNG check, 180
s timeout). The reply is read as a stream with a byte cap before it is
parsed. A decoded image larger than 20 MiB is refused.
- A reviewed rate card for `gpt-image-2`: text input $5, image input $8
and image output $30 per 1M tokens, from
developers.openai.com/api/docs/pricing, retrieved 2026-11-06.
  - A create-only, symlink-confined local write.
- **`paperclip-runner-tool-authority.ts`:**
  - Adds the `imageGeneration` binding.
- Advertises the tool only for standard mode with a workspace. A remote
target must also have an installer. Dot, Muse, Grok and native review do
not get the tool.
- Adds the `#generateImage` handler. Before any spend, it checks again:
the run and task, budget authority, `issue:mutate`, case handoff, and
the connection.
- Stores a claim-before-spend per-call receipt in
`resultJson.imageToolReceipts`. A retry or recovery replays the receipt
and does not pay again.
- Saves a `charged` receipt (usage, price, request ID and charge time)
as soon as OpenAI returns. A failed save, failed accounting write or
crash can then still settle the spend. A retry or recovery records the
cost once and never buys again.
- Records one `cost_events` row through `createCostEventInTransaction`.
The row is `metered_api` with `costStatus: "estimated"`, and the budget
is evaluated. The row is recorded even if saving fails.
- Checks the task's project budget, as well as the company and agent
budgets, before spending.
- Writes content-free `runner.image_generate` activity entries: started,
completed, failed and unconfirmed.
- **`ai-connections.ts`:** adds `selectImageGeneration`. It checks again
that the connection is an active, healthy OpenAI `api_key` connection
with no custom routing. It also checks the responsible user's
membership, the grant audience and the agent or company installation.
These are the same rules that runtime selection uses.
- **`native-session-executor.ts`:** gets the binding from the run's
admitted `contextSnapshot.aiConnection`. The value comes only from the
server, never from tool input.
- **`native-session-resume.ts`:** the native tool contract changes from
`v18` to `v19` and now lists `generate_image`. A persisted provider
thread cannot add tools when it resumes, so older threads must rotate.
- **`doc/runner-api-tools.md`:** a new section about image generation
for OpenAI API-key Codex agents.
- **#19516: kept, not reverted.** Its changes have no effect on the
API-key path. They are still correct for Codex agents that sign in with
ChatGPT, because `image_gen` works there and saves to
`CODEX_HOME/generated_images`. Those agents need the read grant that
#19516 added. On the API-key path, the new tool replaces the built-in
tool. The tool's description tells the agent to use it instead of
`image_gen` or `scripts/image_gen.py`.

## Verification

**Red on `master` (e482f0f632).** Behavioural red with the first version
of the new test:

```
 FAIL  src/services/native-runtime/generate-image-tool.test.ts > generate_image for OpenAI API-key Codex agents (N-12) > lets an API-key Codex agent make an image with its existing OpenAI key, saved in its workspace and billed
AssertionError: expected [ 'connections_search', …(51) ] to include 'generate_image'
 ❯ src/services/native-runtime/generate-image-tool.test.ts:78:60
      Tests  1 failed | 3 passed (4)
```

The final test file on master's source also fails. The reason is that
the module does not exist: `Cannot find module
'./generate-image-tool.js'`.

**Real pinned Codex binary.** The new test is
`packages/paperclip-runner/src/drivers/codex/codex-api-key-image-tools.test.ts`.
It uses the runner's isolated launch, the API-key `auth.json` and a
local Responses endpoint. It shows that the model gets no `image_gen`,
`imagegen` or `image_generation` tool, and that a server-advertised
`generate_image` tool does reach the model:

```
 Test Files  1 passed (1)
      Tests  1 passed (1)
```

**Green after the fix:**
`server/src/services/native-runtime/generate-image-tool.test.ts` uses
embedded PostgreSQL and a stubbed OpenAI fetch.

```
 Test Files  1 passed (1)
      Tests  11 passed (11)
```

The 11 tests cover these behaviours:

- **Main path.** The tool is advertised. The request goes to
`api.openai.com/v1/images/generations` with the existing key. The PNG is
written to the workspace. The key does not appear in the result or in
activity. One `cost_events` row is written: 50 text tokens and 1000
output tokens, which is $0.03025, estimated, from the rate card. A retry
and `recover()` replay the receipt with no second fetch and no second
cost row.
- **No binding:** the tool is not offered and `execute` refuses.
- **Budget hard-stop.** The stop pauses the agent, so the call is
refused before any fetch and no file is written.
- **Bad grant:** the call is refused before any fetch.
- **Project budget stop.** The task's project is paused by its budget,
so the call is refused before any fetch.
- **Charge settlement.** A crash left a `charged` receipt. The retry and
`recover()` record the cost once, report the image path and do not buy
another image.
- **Oversized reply.** The read stops before the body is parsed.
- **Save failure.** The spend is still recorded. A same-call retry does
not pay again and reports the failure.
- **Binding gate.** Only Codex on a direct OpenAI API key gets the
binding. A subscription, ACPX, OpenRouter or a routed connection does
not.
- **Rate card math.**
- **Provider errors.** The error text does not contain the key, and
non-PNG data is rejected.

**Neighbouring suites:**

- `src/drivers/codex` in the runner package: 24 files, 350 tests passed.
- Server: `paperclip-runner-tool-authority.test.ts`,
`native-session-resume.test.ts`, `fast-responses.test.ts`,
`ai-connection-session.test.ts` and the new suite. 157 of 158 passed.
- The one failure is `native-session-resume.test.ts › archives a damaged
prior epoch … with real runnerd` (ENOENT on `authority-epochs`). It
fails the same way on unmodified `master` in this local environment, so
this change did not cause it.
- `tsc --noEmit -p server/tsconfig.json`: clean.
- `pnpm verify` exits 2 (broad). Required CI supplies the broad
coverage.

## Risks

- **New security surface: this needs maintainer sign-off.** The server
now sends the agent's OpenAI API key to `api.openai.com` on the agent's
request. The tool is limited in these ways:
  - The key stays in the server.
- Each call checks run, task, budget, mutation and connection authority
again.
- The destination name comes from the server and must match
`^[a-z0-9-]+-[0-9a-f]{8}\.png$`. The write is create-only and confined
to the workspace root. On macOS it uses `O_NOFOLLOW_ANY`. On Linux it
uses a `/proc` descriptor walk.
- **Spend.** Each call costs money. The tool description tells the agent
to make only what the task needs. The cost is an estimate from a rate
card (`costStatus: "estimated"`), not an invoice. The rate card applies
the image-output rate to all output tokens, so the estimate errs on the
high side.
- **Checkpoint rotation.** The tool-contract bump from v18 to v19 makes
existing native provider threads start fresh once at their next turn.
This is what the contract requires when the tool surface changes.
- **Pricing drift.** If OpenAI changes `gpt-image-2` prices, the rate
card must be updated.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code with tool use (shell, file edits, sub-agents) and extended
reasoning.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-07 07:48:39 -06:00
DottaandClaude Opus 5.5 5eac099fbb fix(agents): batch independent follow-up questions into one card (#19634)
## Thinking Path

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

## Linked Issues or Issue Description

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

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

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

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

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

## What Changed

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

## Verification

Red on master (tests added, source unchanged):

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

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

Green after the fix:

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

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

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

## Risks

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-07 07:48:28 -06:00
DottaandPaperclip 8812c1941d perf(storage): overlap bounded S3 multipart payloads (#19817)
## Thinking Path

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

## Linked Issues or Issue Description

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

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

**Paperclip version or commit**

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

**Deployment mode**

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

## What Changed

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

## Verification

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

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

## Risks

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

## Model Used

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

## Checklist

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

---------

Co-authored-by: Paperclip <[email protected]>
2026-11-07 06:58:10 -06:00
DottaandPaperclip c92bc9e5ec perf: reuse authenticated workspace upload readback (#19811)
## Thinking Path

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

## Linked Issues or Issue Description

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

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

**Paperclip version or commit**

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

---------

Co-authored-by: Paperclip <[email protected]>
2026-11-07 06:33:12 -06:00
DottaandPaperclip ae1f781cac fix(native): preserve web files without settling remote recovery (#19814)
## Thinking Path

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

## Linked Issues or Issue Description

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

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

**Paperclip version/commit**

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

**Deployment mode**

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

## What Changed

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

## Verification

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

From `server/`:

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

## Risks

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

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

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

## Model Used

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

## Checklist

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

---------

Co-authored-by: Paperclip <[email protected]>
2026-11-07 06:16:04 -06:00
DottaandPaperclip 585c885168 fix: preserve prepared sources held by provider artifact failures (#19810)
## Thinking Path

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

## Linked Issues or Issue Description

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

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

**Paperclip version or commit**

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

---------

Co-authored-by: Paperclip <[email protected]>
2026-11-07 05:27:55 -06:00
DottaandPaperclip 384fa2b7be Allow context compaction while Agent Chat waits (#19801)
## Thinking Path

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

## Linked Issues or Issue Description

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

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

OpenAI Codex based on GPT-6 managed the change and reviewed the shared
queue fix. OpenAI `gpt-6.1-sol` authored the focused regression
coverage. Both used reasoning, repository tools and local code
execution. The runtime did not expose the context-window size.

## Checklist

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

---------

Co-authored-by: Paperclip <[email protected]>
2026-11-07 05:11:44 -06:00
DottaandPaperclip d2efe3392b fix(native): preserve retired authority evidence (#19806)
## Thinking Path

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

## Linked Issues or Issue Description

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

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

**Paperclip version/commit**

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

**Deployment mode**

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

## What Changed

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

## Verification

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

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

## Risks

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

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

## Model Used

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

## Checklist

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

---------

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

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

## Linked Issues or Issue Description

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

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

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

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

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-07 04:52:08 -06:00
DottaandPaperclip e801be7aa9 fix: fence nested Git repositories during workspace migration (#19805)
## Thinking Path

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

## Linked Issues or Issue Description

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

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

---------

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

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

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

Co-Authored-By: Devin Foley <[email protected]>
Co-Authored-By: Paperclip <[email protected]>
2026-11-07 02:24:23 -08:00
Devin FoleyandPaperclip 51ab7ebdc6 fix(recovery): admit recovered scheduled preparation retries (#19802)
Allow explicit Retry after a recovered scheduled setup failure.

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

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

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

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

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

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

Co-Authored-By: Paperclip <[email protected]>
2026-11-07 02:14:42 -08:00
DottaandPaperclip 27170dca49 fix(workspaces): preserve the read-only capture phase budget (#19803)
## Thinking Path

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

## Linked Issues or Issue Description

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

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

## What Changed

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

## Verification

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

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

## Risks

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

## Model Used

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

## Checklist

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

---------

Co-authored-by: Paperclip <[email protected]>
2026-11-07 03:58:24 -06:00
DottaandClaude Opus 5.5 ef83d566ca fix(native): ask for the result once after a restart continuation ends without one (#19771)
## Thinking Path

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

## Linked Issues or Issue Description

Refs #19501, #19699, #19720

**What happened?**

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

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

**Expected behavior**

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

**Root cause**

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

## What Changed

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

## Verification

Red before the fix, green after:

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

Also run:

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

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

## Risks

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-07 03:22:37 -06:00
DottaandClaude Opus 5.5 6adc8b1049 fix(recovery): keep restarted Retry attempts as task directories and settle failed re-preparation (#19759)
## Thinking Path

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

## Linked Issues or Issue Description

Refs #19666

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

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

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

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

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-07 02:17:24 -06:00
af0fe8fe81 Keep personal MCP connections functional while idle tenants sleep (#19427)
## Thinking Path

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

## Linked Issues or Issue Description

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

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

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

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

---------

Co-authored-by: Paperclip <[email protected]>
Co-authored-by: Codex <[email protected]>
2026-11-07 02:12:48 -06:00
DottaandPaperclip 909f4d1a33 fix(native): recover exact historical provider pack code (#19779)
## Thinking Path

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

## Linked Issues or Issue Description

Refs #19558, #19622, #19755.

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

---------

Co-authored-by: Paperclip <[email protected]>
2026-11-07 02:06:02 -06:00
DottaandPaperclip ae2d2956b3 Reject relative local Runner paths before native admission (#19776)
## Thinking Path

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

## Linked Issues or Issue Description

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

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

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

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

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

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

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

## Linked Issues or Issue Description

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

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

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

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

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

---------

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

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

## Linked Issues or Issue Description

Refs: #19726.

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

## What Changed

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

## Verification

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

Focused commands, from `server/`:

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

## Risks

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

## Model Used

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

## Checklist

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

Co-authored-by: Paperclip <[email protected]>
2026-11-07 01:24:31 -06:00
Nicky LeachandPaperclip beb3e55072 Continue a released Runner after confirmed graceful eviction (#19760)
## Thinking Path

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

## Linked Issues or Issue Description

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

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

---------

Co-authored-by: Paperclip <[email protected]>
2026-11-06 23:21:45 -08:00
DottaandClaude Opus 5.5 3895195f4b fix(native): report a retained provider pack the controller does not ship as incompatible (#19755)
## Thinking Path

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

## Linked Issues or Issue Description

Refs #19558, #19743

**What happened?**

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

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

**Expected behavior**

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

**Steps to reproduce**

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

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

---------

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

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

## Linked Issues or Issue Description

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

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

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

Refs #19696.

## What Changed

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

## Verification

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

## Risks

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

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-07 01:08:11 -06:00
DottaandPaperclip 33ca62f776 Keep external-agent feature reads on their admission transaction (#19750)
## Thinking Path

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

## Linked Issues or Issue Description

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

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

**Paperclip version or commit**

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

## What Changed

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

## Verification

Local controlled regression passed at tested revision
`d8dece250b75cffee67d3b7f4f0e669826282164`:

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

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

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

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

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

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

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

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

## Risks

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

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

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

## Model Used

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

## Checklist

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

---------

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

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

## Linked Issues or Issue Description

Refs #19720, #19625

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

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

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

## What Changed

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

## Verification

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

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

## Risks

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

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-07 01:00:34 -06:00
DottaandPaperclip f673bd3cb5 fix(native): verify legacy harness filesystem independence (#19748)
## Thinking Path

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

## Linked Issues or Issue Description

Refs #19715, #19729, #19733.

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

## What Changed

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

## Verification

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

Focused command from `server/`:

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

## Risks

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

## Model Used

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

## Checklist

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

---------

Co-authored-by: Paperclip <[email protected]>
2026-11-07 00:57:20 -06:00
96bc021edb fix(server): certify settled legacy processes for future retries (#19377)
## Thinking Path

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

## Linked Issues or Issue Description

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

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

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

## What Changed

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

## Verification

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

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

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

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

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

## Risks

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

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

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

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

## Model Used

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

## Checklist

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

---------

Co-authored-by: Paperclip <[email protected]>
Co-authored-by: Codex <[email protected]>
2026-11-07 00:56:04 -06:00
c3180398fa fix(native): resume a remote Runner after a settled suspension
fix(native): resume a remote Runner after a settled suspension

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

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

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

Refs: #19366

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Paperclip <[email protected]>
Co-Authored-By: Paperclip <[email protected]>
2026-11-06 22:39:40 -08:00
Nicky Leach 08b049f381 Admit qualified Claude sources through the existing Core claim (#19196) 2026-11-06 22:25:22 -08:00
DottaandClaude Opus 5.5 e620ce3678 fix(heartbeat): deliver each accepted follow-up exactly once through restarts and admission holds (#19741)
## Thinking Path

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

## Linked Issues or Issue Description

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

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

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

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

---------

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

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

## Linked Issues or Issue Description

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

---------

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

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

## Linked Issues or Issue Description

Refs #19622

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

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

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

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

## Linked Issues or Issue Description

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

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

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

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

## What Changed

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

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

## Verification

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

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

## Risks

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

## Model Used

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

## Checklist

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

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

---------

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

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

Co-Authored-By: Sentry Bot <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Paperclip <[email protected]>
2026-11-06 21:33:59 -08:00
DottaandClaude Opus 5.5 464e6e16e0 fix(native): resume copyback after a restart kills its same-host owner; one board action otherwise (#19717)
## Thinking Path

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

## Linked Issues or Issue Description

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

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

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

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

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-06 23:31:07 -06:00
DottaandPaperclip 7a72caeda1 perf(workspaces): batch online companion discovery (#19735)
## Thinking Path

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

## Linked Issues or Issue Description

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

---------

Co-authored-by: Paperclip <[email protected]>
2026-11-06 23:26:53 -06:00