Commit Graph
5067 Commits
Author SHA1 Message Date
DottaandPaperclip 39fec166b9 Merge the exact Pi v23 install-probe digest
Co-Authored-By: Paperclip <noreply@paperclip.ing>

* codex/claude-runner-default:
  fix(runner): pin the Pi v23 digest in its install probe
2026-10-12 12:55:37 -05:00
DottaandPaperclip cbc0d6e946 Merge the exact Pi v23 install-probe digest
Co-Authored-By: Paperclip <noreply@paperclip.ing>

* codex/claude-runner-protocol:
  fix(runner): pin the Pi v23 digest in its install probe
2026-10-12 12:55:33 -05:00
DottaandPaperclip 33c9216614 fix(runner): pin the Pi v23 digest in its install probe
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-12 12:55:28 -05:00
DottaandPaperclip a1ac0a7324 Merge the current Pi clean-install assertion
Co-Authored-By: Paperclip <noreply@paperclip.ing>

* codex/claude-runner-default:
  fix(runner): verify the current Pi profile in clean installs
2026-10-12 12:55:06 -05:00
DottaandPaperclip d0cc4332ab Merge the current Pi clean-install assertion
Co-Authored-By: Paperclip <noreply@paperclip.ing>

* codex/claude-runner-protocol:
  fix(runner): verify the current Pi profile in clean installs
2026-10-12 12:55:01 -05:00
DottaandPaperclip ca52d2998c fix(runner): verify the current Pi profile in clean installs
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-12 12:54:57 -05:00
DottaandPaperclip 4c7e0538c3 Merge the reviewed compact question envelope fix
Co-Authored-By: Paperclip <noreply@paperclip.ing>

* codex/claude-runner-default:
  fix(runner): point v7 question answers to the compact envelope
2026-10-12 12:41:48 -05:00
DottaandPaperclip 8cd1212d08 Merge reviewed v7 question-answer correction
Co-Authored-By: Paperclip <noreply@paperclip.ing>

* codex/claude-runner-protocol:
  fix(runner): point v7 question answers to the compact envelope
2026-10-12 12:40:55 -05:00
DottaandPaperclip bd966ff9cd fix(runner): point v7 question answers to the compact envelope
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-12 12:40:52 -05:00
DottaandPaperclip d0da37c221 feat(runner): assemble target-owned Claude Linux provider packs
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-12 12:37:57 -05:00
DottaandPaperclip 18626f677f Merge reviewed Claude behavior into platform qualification
Co-Authored-By: Paperclip <noreply@paperclip.ing>

* codex/claude-runner-default:
  docs: link the approved protocol review boundary
  fix(runner): retain versioned profiles in Docker context
  docs: record Claude review fixes and protocol prerequisite
  fix(claude): preserve permissions and connection claims at creation
  feat(runner): admit Claude effort before native prompts and resume
  fix: preserve TOML table semantics during Codex startup trust
2026-10-12 12:30:54 -05:00
DottaandPaperclip 8d07b40907 Merge protocol Docker context correction
Co-Authored-By: Paperclip <noreply@paperclip.ing>

* codex/claude-runner-protocol:
  fix(runner): retain versioned profiles in Docker context
2026-10-12 12:30:02 -05:00
DottaandPaperclip a950d9b163 docs: link the approved protocol review boundary
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-12 12:30:02 -05:00
DottaandPaperclip 269e802fe6 fix(runner): retain versioned profiles in Docker context
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-12 12:29:56 -05:00
DottaandPaperclip 342a9f4b18 docs: record Claude review fixes and protocol prerequisite
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-12 12:25:31 -05:00
DottaandPaperclip 12f30702d7 Merge branch 'codex/claude-runner-protocol' into codex/claude-runner-default
* codex/claude-runner-protocol:
  feat(runner): admit Claude effort before native prompts and resume
  fix: preserve TOML table semantics during Codex startup trust

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-12 12:24:59 -05:00
DottaandPaperclip 6263a47185 fix(claude): preserve permissions and connection claims at creation
Retain native import settings, explicitly select Legacy in qualification, pin updated CLI fixtures, and version shared source attestations.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-12 12:24:17 -05:00
DottaandPaperclip 2e8d5e906e feat(runner): admit Claude effort before native prompts and resume
Version the shared Pi and pending Copilot source attestations without changing their executable closures or platform qualification.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-12 12:24:15 -05:00
DottaandPaperclip a200495cc7 fix: preserve TOML table semantics during Codex startup trust
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-12 12:14:38 -05:00
DottaandPaperclip c860863295 feat(runner): select verified Claude Linux ARM64 artifacts
Automatic defaults remain gated until installed execution qualification passes.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-12 12:08:07 -05:00
DottaandPaperclip ee315082e4 Merge remote-tracking branch 'origin/codex/codex-runner-default' into codex/claude-runner-default
* origin/codex/codex-runner-default:
  test: bind installed native payloads and await saved state

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-12 12:02:02 -05:00
DottaandPaperclip 7c0f3d75a6 fix(ui): hide legacy commands for native Claude
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-12 12:01:54 -05:00
DottaandPaperclip 7ede05c1c5 docs: record successful Claude API resume acceptance
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-12 11:54:11 -05:00
DottaandPaperclip 966b3d5bc2 Merge latest Codex platform qualification into Claude default
Preserve Codex GNU libc eligibility and Linux ARM64 behavior; keep Claude automatic selection limited to Apple Silicon.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-12 11:48:10 -05:00
DottaandPaperclip 598e190cb3 test: bind installed native payloads and await saved state
Isolate actual Codex resolver fixtures from global bridge lookup and preserve host qualification blockers with literal evidence.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-12 11:45:22 -05:00
DottaandPaperclip 9788c05d0a test(runner): retain bounded Claude qualification evidence
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-12 11:42:37 -05:00
DottaandPaperclip 5a60820588 fix(runner): acknowledge Claude cold session resume
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-12 11:42:37 -05:00
DottaandPaperclip 123a188374 fix: separate Codex login and ARM host qualification
Keep daemon ABI admission separate from CLI login and preserve restricted Docker installation checks before native host startup.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-12 11:20:36 -05:00
DottaandPaperclip 695506ad39 perf: cache native builds and verify Linux runtime targets
Preserve actual target selection, record the GNU libc baseline, and retain primary installed-startup failures for ARM qualification.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-12 10:53:24 -05:00
DottaandPaperclip 5f0b898e9e merge: reconcile latest Codex runner target qualification
Preserve the user-approved Codex Linux ARM64 default and keep Claude automatic selection limited to Apple Silicon.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-12 10:50:42 -05:00
DottaandPaperclip 6fd56c445d feat: default Claude to the native runner on Apple Silicon
Carry Claude effort through versioned native admission and recovery. Preserve existing connections and saved runner choices, and price Opus 5.5 API receipts.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-12 10:48:21 -05:00
DottaandPaperclip dcdf09a0f3 feat: include Linux ARM64 in native Codex runner targets
Ship a source-bound ARM64 daemon alongside Linux x64 and both Mac targets.
Use native hosted builds and the existing clean npm/native startup verifier.
Read target requirements from the immutable source to preserve historical
three-target promotions. Keep Codex payloads as official npm dependencies.

Resolve ARM64 creation and login through the shared platform contract. Share
the existing legacy CLI permission calculation with import previews without
changing execution behavior. Record targeted checks and live Mac evidence.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-12 10:13:34 -05:00
DottaandPaperclip 058af8f1b6 fix: preserve release artifacts during dev builds and display imported policy
Keep source-built daemons separate from source-bound release targets and select
fresh compatible source output only inside the development supervisor. Preserve
release manifests, running executable inodes, and explicit operator overrides.

Show the existing translated Codex permission policy in import controls while
preserving runner-only override payloads. Record real Apple Silicon task and
paused import proof alongside exact qualification sources.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-12 09:32:28 -05:00
DottaandPaperclip d522822a5d fix: preserve imported runner choices and qualify Mac install fixtures
Forward explicit import runner overrides, preserve native harness identity without experimental Codex gating, refresh existing host daemon outputs atomically, and retain strict public install probes with target paths and the executing Node SDK.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-12 08:55:54 -05:00
DottaandPaperclip dc7304d7c4 test: align Dot readiness fixtures and clean sealed runtime assets
Enable the existing experiment only in its intended fixture journeys, and restore owner permissions only inside worker-owned scratch directories during exit cleanup.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-12 08:11:12 -05:00
DottaandPaperclip f4abdc2cfc Merge master b189f586d into Codex runner default candidate
Preserve master transport, lifecycle, and release verification controls alongside macOS qualification.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-12 07:55:09 -05:00
DottaandPaperclip 7b8d29a856 feat: qualify macOS Codex runner defaults and release artifacts
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-12 07:48:52 -05:00
github-actions[bot]andlockfile-bot b189f586d2 chore(lockfile): refresh pnpm-lock.yaml (#15960)
Auto-generated lockfile refresh after dependencies changed on master.
This PR only updates pnpm-lock.yaml.

Co-authored-by: lockfile-bot <lockfile-bot@users.noreply.github.com>
2026-10-12 06:52:34 -05:00
DottaandPaperclip db2f00947b fix(runner): include Claude ACP in the server package (#15912)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The server ships a copy of Paperclip Runner for native agent
execution.
> - The Claude ACP bridge is a runner dependency, but it was absent from
the server package.
> - The runner resolves this bridge dynamically, so the import-based
packaging check missed it.
> - A Claude session then failed before startup and showed a generic
connection error.
> - This pull request ships the qualified bridge and reports a missing
runtime with repair steps.

## Linked Issues or Issue Description

**What happened?**

A server install with the vendored runner could not load
`@agentclientprotocol/claude-agent-acp/package.json`. The runtime test
suggested checking the model or provider connection. The task reported
an interrupted runner session.

**Expected behavior**

The server package includes its qualified Claude ACP runtime. A missing
bundled runtime reports an installation error and explains how to repair
it.

**Steps to reproduce**

1. Use a server install with the vendored runner and no Claude ACP
bridge.
2. Select the Paperclip Runner ACP harness and the Claude provider.
3. Run the runtime test or open a session.
4. Observe a missing-module failure before Claude starts.

**Paperclip version or commit**

Reproduced on `e733bddc45`. This branch is based on current master.

**Deployment mode**

Self-hosted server built from source.

Related: #15224 upgrades the Claude bridge. This PR fixes its omission
from the server package and keeps the current qualified version. A
future bridge upgrade must update the server pin too.

## What Changed

- Add Claude ACP `0.73.0` to the server dependencies and bundled
dependencies.
- Pin the qualified Claude SDK and bridge dependencies in the npm bundle
producer graph. Publish its native packages as consumer optional
dependencies instead of shipping the producer executable.
- Bind source-built vendored runners to the shared pnpm dependency store
while retaining the server manifest as the provider authority. Keep
published npm and self-rooted deployment roots unchanged.
- Include dynamically resolved Claude and Codex bridges in the vendor
dependency check.
- Preserve the missing-package category through the sidecar, Rust
transport, and native session executor. Stop automatic retries for this
installation failure.
- Show **Runtime missing** in the runtime test and task thread. Explain
the reinstall or rebuild, server restart, and runtime test steps.
- Add regression tests and document the repair procedure.

## Verification

- Red/green comparison with the production sidecar: the same
missing-package setup returns a raw require-stack error on the original
code and an actionable, non-retryable missing-runtime error on this
branch.
- The rebuilt server's vendored Claude installation probe passes using
the source-server authority selected by the launcher, without starting a
provider or making a model request.
- Focused server verification: 830 tests passed. Runner installation and
sidecar error tests: 83 passed, with 6 platform-specific skips.
Packaging tests: 28 passed. Rust transport tests: 7 passed.
- Source/deployment authority tests: 3 passed. Red/green packaging tests
include a Linux-produced Claude SDK bundle, actual npm pack, and closed
executable admission on Linux x64 and macOS ARM64/x64.
- Full UI suite: 8,113 tests passed.
- `pnpm -r typecheck` and `pnpm build` passed on the rebased branch.
- Full current-head CI passed, including all general and serialized test
lanes, Runner Rust/Vitest verification, typecheck, build, and all eight
e2e shards. Greptile reviewed `d6a2741834ff922956560f2f689382827bbaf9e0`
at 5/5 with no unresolved threads.
- Local full-suite limitation: an earlier parallel attempt hit Postgres
startup failures and timeouts; the affected server tests and a database
fixture passed in isolation. The subsequent serial `pnpm test:run` was
stopped while still running after complete CI passed. Focused local
tests, full UI tests, typecheck, and build completed as listed above.
- Token gates and `git diff --check` passed.

## Risks

- Bundling the Claude bridge increases the server package size. The
producer graph retains the qualified SDK version and existing isolation
patch. Native platform declarations move to the published server
manifest, while the runtime keeps its exact version, declared-slot,
no-follow, and executable digest checks.
- The new failure category is permanent. Operators must repair the
server installation before they retry the task.
- Existing failed runs retain their recorded error. The clearer message
applies to new runtime tests and runs.
- No database schema or migration changes.

## Model Used

OpenAI GPT-6 through Codex. The system reports the model family; the
exact deployment ID and context-window size are not exposed in this
session. Used reasoning, browser inspection, tool use, and code
execution.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
canary/v2026.1012.0-canary.5
2026-10-12 06:51:22 -05:00
1b4c803816 fix(costs): preserve usage attribution and explain unavailable run costs (#15948)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The cost ledger records usage and controls agent budgets.
> - Some runs lose the selected model or API billing category before
their usage reaches the ledger.
> - The dashboard reports missing costs but does not identify the
affected runs or the available actions.
> - This pull request preserves usage attribution and adds a compact
view of cost gaps grouped by agent.
> - Operators can inspect runs, open agent settings, and record saved
costs without starting new agent work.

## Linked Issues or Issue Description

Refs #15634, #11938, #15879.

This follows the merged subscription reporting in #15634. The older
proposal in #11938 addresses part of ACP model attribution. This change
also rejects conflicting models across a turn, fixes credential-backed
billing capture, and explains cost gaps in the existing dashboard. The
Pi receipt recovery work in #15879 covers a separate adapter recovery
path. This PR preserves unresolved receipts and does not recover
provider usage or reprice past entries.

**What happened?**

ACP results use the requested model even when the runtime selects a
model. Native and OpenCode API runs can report unknown billing despite a
matching API credential. The dashboard shows notices without links to
the affected runs. These gaps make API and subscription totals harder to
interpret.

**Expected behavior**

Record the model selected by the runtime. Identify API billing only from
a matching credential. Keep costs unresolved when reliable usage or
pricing is absent. Show the affected agents and runs. Offer an
accounting retry only when saved evidence is ready.

**Steps to reproduce**

1. Run an ACP agent with its model left unset, or use a matching API
credential with a native or OpenCode agent.
2. Inspect the recorded model and billing category for the run.
3. Open Costs Overview for a period with unpriced usage or terminal runs
awaiting receipts.
4. Inspect the notice and try to find the affected run or the relevant
agent settings.

**Paperclip version or commit**

Current master at `657e71584a0`. The patch is rebased on that commit.

**Deployment mode**

Self-hosted server and local development. The cost detail tests use a
disposable PostgreSQL database.

## What Changed

- Preserve the runtime-selected ACP model in checkpoints and final
results. Conflicting models across one turn remain unpriced.
- Share credential-backed API billing checks across native and OpenCode
results. Keep unknown billing when the provider and credential do not
match.
- Add company-scoped cost detail and accounting retry endpoints. Board
viewers can read details but cannot retry. Agents and foreign companies
are denied.
- Limit each detail category to 25 entries. Use one consistent database
snapshot and sparse indexes matching the filters and sort order. Add the
unknown-billing count to the existing summary query.
- Replace the notice with an expandable count line. Group entries
alphabetically by agent. Show the saved blob icon, link each name to
configuration, and put run and retry links beside the date.
- Preserve estimate and unavailable-price labels on Providers and
Billers; keep retry confirmation after the resolved notice disappears.
- Fetch details only while expanded. Retain the last loaded details
after a refresh error. Retry saved accounting only; do not rerun the
agent.
- Add capture, database, authorization, UI, and Storybook coverage.
Update the implementation contract and cost reporting guide.

## Verification

- `pnpm -r typecheck` passed after rebase.
- `pnpm build` and `pnpm build-storybook` passed after rebase.
- `pnpm check:token-gates` and `git diff --check` passed.
- 1,086 focused tests passed across 17 files. These cover ACP, native
and OpenCode capture, PostgreSQL reports, route authorization, OpenAPI,
Costs UI, and existing accounting regressions.
- Desktop and mobile browser checks passed during UI review. They cover
lazy expansion, alphabetical groups, loaded blob icons, configuration
and run links, keyboard retry, and layout overflow.
- `pnpm test:run` was attempted with command-scoped
`init.defaultBranch=master` for Git fixtures. It was stopped after a
native session recovery failure reproduced on unchanged base
`657e71584a0` (`archives a damaged prior epoch before completing guarded
same-task replacement with real runnerd`). An MCP upload case failed in
the broad run but passed in isolation on both the base and PR branch.
These local limitations do not affect the 1,086 passing focused tests.
- Current-head CI is green, including all unit test lanes, serialized
server suites, runner checks, typecheck, build, policy, and eight
browser shards. Apex re-review is 5/5 on `56de60effdb`; all four
original findings are addressed and every review thread is resolved.
- PostgreSQL applies migration `0333` in the integration fixtures.
EXPLAIN ANALYZE on the production detail statements uses the new sparse
indexes behind 5,000 healthy records in each table.
- Ten provider/biller card cases cover missing and estimated prices; the
retry test covers row removal and notice unmount. A live Storybook
browser check verifies the confirmation and updated count.
- Run `pnpm storybook`. Open **Costs / Subscriptions / Cost Data Quality
Details** to inspect the production page with representative fixtures.

## Risks

- Model and billing metadata change for future runs. Estimates still
require complete usage and a supported price. No past cost amount is
rewritten.
- A missing final receipt remains unresolved for budget checks. The
display changes after fifteen minutes; that time limit never authorizes
settlement.
- Accounting retry is a write action. It retains the existing company
checks, receipt fences, idempotency, and audit logging.
- Detail reads are bounded and run only after expansion. They poll every
30 seconds while open. No provider probe or new summary query is added.
- Migration `0333` adds two partial indexes; it changes no stored cost
amounts or table columns. Index creation scans the existing tables and
may briefly block writes during deployment. No lockfile or workflow
change is included.

## Model Used

OpenAI Codex, based on GPT-6, with repository inspection, code editing,
browser checks, and test execution. The exact serving model ID,
reasoning setting, and context window were not exposed to this session.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `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: Codex <noreply@openai.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
v2026.1023.0 canary/v2026.1012.0-canary.4 nightly/v2026.1012.0-nightly.0 beta/v2026.1012.1-beta.0
2026-10-11 22:48:29 -07:00
4611d6e95a fix(restore): retry sandbox copy-back before holding the task for repair (#15920)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Legacy sandbox runs copy the agent's workspace files back to the
host when the run settles.
> - When this copy-back fails one time, the server keeps the sandbox and
puts a board-owned repair hold on the task.
> - The hold blocks new messages. Only an operator API runbook can clear
it. The board shows that runbook text, so users do not know what to do.
> - Many copy-back failures are transient: a dropped transfer, a
provider error, or a merge lock that another restore holds.
> - This pull request retries those failures automatically while the run
still has its merge baseline and before any host file changes.
> - The benefit is that most failures repair themselves, and the
remaining holds explain the situation in plain language.

## Linked Issues or Issue Description

Refs #14590 (related: no-replay recovery holds)

**What happened?**
A legacy sandbox run finished its work, but one workspace copy-back
attempt failed. The task then showed "Recovery needed" with
operator-only steps ("verify its exact stop receipt … record
workspaceRepairEvidence"). Messages from the user stayed saved and were
not delivered.

**What did you expect?**
Paperclip retries a transient copy-back failure automatically. If a
repair is still necessary, the task says clearly what happened and who
must act.

**Steps to reproduce**
1. Run an agent in a legacy sandbox environment.
2. Make the workspace copy-back fail one time, for example with a
dropped transfer or a held merge lock.
3. Open the task. It is blocked behind a repair hold, and new messages
wait.

**Additional context**
Separately, distinct live events can render toasts with the same title
and body (for example, two runs of the same agent that both succeed).
The toast list showed both, and the desktop app forwards each one as a
notification.

## What Changed

- `packages/adapter-utils/src/sandbox-managed-runtime.ts`: retry the
workspace copy-back up to three times in total, with a 2 s and then a 5
s backoff. A retry occurs only when the failure is `restore_failed` or
`restore_lock_timeout` and no host workspace file was written. The merge
now marks the first host write in `beforeApply`, which runs under the
merge lock before any write.
- Unsafe archives, permission failures, and failures after the merge
started writing are never retried. Each retry uses a new temp directory
and a new imported Git ref.
- New optional `workspaceRestoreRetryDelaysMs` input, so tests can
shorten the backoff. Nested repository restores receive the same value.
- `ui/src/components/ExecutionBlockerNotice.tsx`: for a workspace repair
hold, the notice now says what happened in plain language. The stored
operator runbook text moves under "Technical details".
- `ui/src/context/ToastContext.tsx`: also dedupe toasts on their visible
tone, title, and body inside the existing 3.5 s window.
- `doc/workspace-restore-locks.md`: document the automatic retry.

## Verification

- `pnpm --filter @paperclipai/adapter-utils exec vitest run
src/sandbox-managed-runtime.test.ts src/workspace-restore` (218 tests
pass). New tests cover:
  - a retry after a dropped transfer,
  - a retry after a merge-lock timeout,
  - a stop after three attempts,
- no retry for an unsafe archive, a permission failure, or a failure
after the merge started writing.
- The existing step-diagnostic test now uses failures that persist
across retries. It still expects one diagnostic per failed step.
- `pnpm --filter @paperclipai/ui exec vitest run
src/context/ToastContext.test.tsx
src/components/ExecutionBlockerNotice.test.tsx
src/context/LiveUpdatesProvider` (120 tests pass).
- `tsc --noEmit` for adapter-utils passes. `node
scripts/check-token-gates.mjs` is clean.
- Not fully run: the full adapter-utils suite has two failures in
`github-launcher*.test.ts`. They read the managed GitHub credentials of
the local sandbox environment and do not touch restore code. UI `tsc -b`
reports five errors in `PluginOrganizationSwitcher.tsx`, which is not
changed here, because the plugin SDK is not built in a fresh install.

## Risks

- A copy-back that fails before the merge can take about 7 s longer
before it reports a failure. Lock timeouts can take up to about 97 s in
total (three 30 s lock waits plus backoff). Settlement already waits for
the copy-back.
- The retry logic depends on `beforeApply` running before the first host
write in `mergeDirectoryWithBaseline`. That is true today, and a comment
marks it.
- A Git bundle import before the merge can leave unreferenced Git
objects after a retry. They are harmless, and the existing ref cleanup
still runs on each attempt.
- Content-based toast dedupe can hide a second toast with identical text
inside 3.5 s. That is the intended behavior.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`, Anthropic) in the Claude Agent
SDK, with tool use (shell, file edit, code search) and a subagent for
code exploration.

## Checklist

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

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

---------

Co-authored-by: Paperclip Agent <agent@paperclip.ing>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
canary/v2026.1012.0-canary.3
2026-10-11 21:44:11 -07:00
657e71584a fix(daytona): record provider state when lease creation hits its deadline (#15946)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Agents can run in Daytona sandboxes. The Daytona plugin acquires a
lease before each run.
> - Some acquisitions time out. The report then says
`environmentAcquisitionPhase=create` and that the whole 300 s budget was
used.
> - The Daytona SDK create timeout is equal to the acquisition budget.
The outer timer always fires first. Thus the report never shows the
state that Daytona left the sandbox in.
> - Without that state, we cannot choose a fix. A stuck snapshot pull, a
slow start, and a missing allocation need different fixes.
> - This pull request adds one bounded, read-only provider lookup at the
create deadline. It records a fixed-enum provider state in the existing
acquisition diagnostic.
> - The benefit is that the next timeout identifies where the provider
stopped. Retry, budget, and cleanup behavior do not change.

## Linked Issues or Issue Description

Refs #15736 (bounded Daytona acquisition failure observations). This
pull request extends those observations.

**What happened?**

A Daytona lease acquisition timed out with `Daytona lease acquisition
timed out; allocation cleanup is pending`. The run-failure report gave
`environmentAcquisitionPhase=create`,
`environmentAcquisitionElapsedMs=300004`, and
`environmentAcquisitionBudgetMs=300000`. The report did not show the
Daytona sandbox state. The SDK create timeout is equal to the outer
budget, so the outer timer always fires first. The SDK never reports the
stuck state.

**Expected behavior**

When creation is still pending at the deadline, the report should show
where the provider left the allocation. Examples are `pulling_snapshot`,
`starting`, `error`, or `absent`.

**Steps to reproduce**

1. Configure a Daytona environment.
2. Start a run when Daytona does not finish create and start within the
acquisition budget.
3. Inspect the setup-failure report. Only the phase, the elapsed time,
and the budget are present.

**Paperclip version or commit**

`master` at ba04968.

## What Changed

- `packages/plugins/sdk`: Add an optional `providerState` field to
`PluginEnvironmentAcquisitionDiagnostic`. The values are a fixed enum:
`absent`, `creating`, `pending_build`, `building_snapshot`,
`pulling_snapshot`, `starting`, `started`, `error`, `build_failed`,
`other`, `unavailable`. The reader omits an unknown value and keeps the
other fields. Old envelopes stay valid.
- `packages/plugins/sandbox-providers/daytona`: When the acquisition
deadline fires while `client.create` is still pending, make one
`client.get` of the provisional `paperclip-create-<attempt>` name. The
lookup has a 5 s bound, inside the host's 30 s RPC margin. Report the
state only when the name and all ownership labels match. Otherwise
report `unavailable`. A not-found error reports `absent`.
- The lookup does not delete. It does not set `observedProviderLeaseId`.
It does not change retries, budgets, or the cleanup envelope.
- After the deadline, only the timer settles the call. A late create
result cannot replace the timeout error before the lookup completes. A
test proves this guard.
- `server`: Project the value to
`run_execution.environmentAcquisitionProviderState`. It uses the same
private, run-scoped receipt as the other acquisition fields.
- `doc/observability.md`: Document the new field and its limits.

## Verification

- `cd packages/plugins/sandbox-providers/daytona && npx vitest run
--config vitest.config.ts`: 392 passed, 6 skipped.
- `npx vitest run
packages/plugins/sdk/tests/environment-creation-cleanup.test.ts
server/src/services/__tests__/run-failure-diagnostics.test.ts
server/src/__tests__/environment-acquisition-diagnostics.test.ts
server/src/__tests__/run-failure-sentry-real-sdk.test.ts
server/src/__tests__/environment-runtime.test.ts`: 302 passed, 11
skipped.
- `tsc --noEmit` passes for `packages/plugins/sdk` and
`packages/plugins/sandbox-providers/daytona`.
- New tests cover state mapping, `other` for unlisted states, `absent`,
`unavailable` for lookup failure and label mismatch, a hung lookup
bounded to 5 s, no deletion, an unchanged cleanup envelope, and a late
create that settles during the lookup.
- A mutation check removed the late-settlement guard. The late-create
test then failed.
- The local server `tsc` was killed by the sandbox memory limit. CI must
confirm the server typecheck. The server change is one line.

## Risks

- Low risk. The timeout rejection can come up to 5 s later, and only for
a create-phase deadline. The host RPC margin is 30 s.
- The lookup is one extra read-only Daytona API call for each
create-phase timeout.
- This is a diagnostic. It does not fix the upstream cause. The next
timeout report must show the state before a fix is chosen.
- This is a Sentry run-failure report change. It is not a Telemetry
change. The new value is a fixed enum. It does not contain provider IDs,
causes, URLs, or paths.

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

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), used through Claude
Code with tool use (shell, file edit, test runs). Context window and
reasoning configuration are set by the runtime.

## 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: Devin Foley <139239+devinfoley@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
canary/v2026.1012.0-canary.2
2026-10-11 21:22:39 -07:00
ba04968b10 fix(server): report failing workspace jobs as 422 instead of 500 (#15941)
A non-zero workspace job exit is the operator job's own result. Map it to 422 workspace_job_failed with the exit code instead of a 500 that the error handler reports to Sentry as a server crash. A job without a command is also 422. Other workspace command failures are unchanged.

Co-Authored-By: Amy (Opus) <claude@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
canary/v2026.1012.0-canary.1
2026-10-11 19:51:23 -07:00
f331d386dc fix(ui): attribute stackless unhandled rejections with event and lifecycle facts (#15940)
Diagnostics only for Sentry PAPERCLIP-FRONTEND-G: record trusted/synthetic unhandledrejection origin, promise kind, and coarse page lifecycle facts without reading rejected values.

Co-Authored-By: Amy (Opus) <claude@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:26:44 -07:00
bfe7c6357d fix(ui): stack confirmation prompt and hide raw custom target keys (#15895)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents ask the board for decisions with `request_confirmation`
interaction cards
> - The compact task-chat card puts the prompt and the target pill in
one `justify-between` flex row
> - An agent can send a `custom` target with only a machine `key` (for
example `github:org/repo:pull:16`) and no `label`
> - The card shows that raw key as a mono pill that does not shrink, so
the card splits about 50/50 and the prompt is squeezed into the left
half
> - This pull request stacks the prompt above the pill and hides custom
targets that have no human label
> - The benefit is a readable, full-width confirmation prompt without
meaningless identifier pills

## Linked Issues or Issue Description

**What happened?**
A pending confirmation card split its body into two columns. The prompt
used the left half. A pill with the raw text
`github:paperclipai/paperclip-desktop:pull:16` used the right half. The
line breaks in the prompt were also lost.

**Steps to reproduce**
1. Create a `request_confirmation` interaction with a multi-line
`prompt` and `target: { "type": "custom", "key":
"github:org/repo:pull:16" }` (no `label`).
2. Open the task chat for the issue.
3. See that the prompt and the key pill share one row, split about
50/50.

**Expected behavior**
The prompt uses the full card width and keeps its line breaks. A custom
target key is an identifier for idempotency and stale detection, so it
does not show as display text. A target pill shows only when the target
has a human label.

## What Changed

- `TaskChatCompactInteractionCard`: stack the prompt full width with the
target pill below it. This applies to the confirmation and the
checkbox-confirmation cards.
- `TaskChatCompactInteractionCard`: keep line breaks in the prompt
(`whitespace-pre-line`, `break-words`).
- `TaskChatCompactInteractionCard` and `IssueThreadInteractionCard`:
show no target chip for a `custom` target without a `label`. Labeled
custom targets (for example connection authorization) and
`issue_document` targets do not change. The stale-target chevron shows
only when both chips show.
- Add regression tests for the stacked layout, the hidden unlabeled key,
and the labeled link pill. The tests cover the compact confirmation and
checkbox-confirmation cards, and the thread card pending chip and
stale-target expiry (with labeled, unlabeled, and mixed custom targets,
and the chevron rule).
- `CONTRIBUTING.md`: add a "Keep Screenshots Out of the Repo" section.
Contributors attach review screenshots to the PR on GitHub. They do not
commit them to the repository.

## Verification

- `cd ui && npx vitest run
src/components/task-chat/TaskChatInteractionCard.test.tsx
src/components/IssueThreadInteractionCard.test.tsx`: 2 files, 100 tests
pass. If you revert the `isDisplayableRequestConfirmationTarget` guard,
3 of the new thread-card tests fail.
- `node scripts/check-token-gates.mjs`: all gates clean.
- `pnpm --filter @paperclipai/ui typecheck`: no errors in changed files.
The only errors come from `@paperclipai/plugin-sdk/ui`, which had no
built `dist` in the local workspace. These errors are not related to
this change.
- Manual: create a confirmation as in the steps above. The prompt is
full width and no key pill shows. Add `label: "PR #16"` and an `href`. A
`PR #16` link pill shows below the prompt.

### Screenshots

These are Storybook renders of the compact task-chat card at 720 px. The
three cards are: a confirmation with an unlabeled custom target (the
reported case), a confirmation with a labeled custom target, and a
checkbox confirmation with an unlabeled custom target.

The images load from an earlier commit of this PR. A later commit
deletes them, so they are not part of the merged change.

| Before (light) | After (light) | After (dark) |
| --- | --- | --- |
|
![before](https://raw.githubusercontent.com/paperclipai/paperclip/ea22e6b484f21de1dbed258286bfc271cdbcc9a1/docs/pr-screenshots/pr-15895/before-light.png)
| ![after
light](https://raw.githubusercontent.com/paperclipai/paperclip/ea22e6b484f21de1dbed258286bfc271cdbcc9a1/docs/pr-screenshots/pr-15895/after-light.png)
| ![after
dark](https://raw.githubusercontent.com/paperclipai/paperclip/ea22e6b484f21de1dbed258286bfc271cdbcc9a1/docs/pr-screenshots/pr-15895/after-dark.png)
|

## Risks

- Low risk. This is a UI-only change. No schema or API change.
- Agents that send a custom target without a label lose the visible
pill. The key was never readable display text. To show a pill, agents
set `label`.

## Merge Note

The first commit was authored as Paperclip. When you squash-merge, keep
this trailer in the squash body:

Co-Authored-By: Paperclip <Paperclip-Paperclip@users.noreply.github.com>

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code agent
harness with tool use (shell, file edit). Standard context window.

## Checklist

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

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: devinfoley <139239+devinfoley@users.noreply.github.com>
canary/v2026.1011.0-canary.12
2026-10-11 12:29:05 -07:00
Devin FoleyandPaperclip d49cbf4c4f Permit idle sleep with consumed retries and passive worktrees (#15907)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Idle sleep must wait for accepted work and durable cleanup.
> - The retry dispatcher keeps its due date after it promotes a retry.
> - Finished retries and passive local directories can therefore look
like pending work.
> - This pull request requires completion proof and permits known
passive directory records.
> - It retains the records and keeps real work and unknown state awake.

## Linked Issues or Issue Description

Related: #15897 and #15851. The open runtime-service work in #13413
retains its separate service and ingress guarantees. This patch does not
change those checks.

**What happened?**

Idle sleep treats a consumed retry date as pending work. It also blocks
on every retained execution workspace, including a local directory
without runtime activity.

**Steps to reproduce**

1. Promote a scheduled retry, then finish its run without accounting
debt. Retain its scheduled retry date.
2. Retain a local primary directory or Git worktree without runtime
configuration, commands, or cleanup due.
3. Request an owned idle safety report. The report returns `present` for
this history.

**Paperclip version or commit**

Reproduced on master at 4b59e2a2ec.

**Deployment mode**

Server with PostgreSQL. The regression tests use embedded PostgreSQL.

## What Changed

- Permit terminal runs with a finish time at or after their retry date
and no accounting debt. Apply that proof to linked wake,
answer-delivery, and verified failed-lease history.
- Permit known passive local primary directories and Git worktrees. Keep
due cleanup, commands, runtime configuration, shared project runtime
inheritance, pending reopen, runtime owners, and unfamiliar state awake.
- Retain retry dates, workspace records, files, and metadata. Document
the proof and retention limits.

## Verification

- Passed all 200 tests in `pnpm exec vitest run
server/src/__tests__/idle-sleep-safety.test.ts` against embedded
PostgreSQL.
- Passed the 52 retry dispatcher tests.
- Passed `pnpm -r typecheck` and `pnpm build`. After the provider
correction, server typecheck and build passed again.
- Full CI is green for commit
`0510669439e855b90a4e03a2fc7827503cea507f`, including all test shards,
typecheck, build, and browser end-to-end tests. One browser test
initially raced its comment write; the failed jobs passed on retry
without a source change.
- Local `pnpm test:run` recorded 18,721 passed, 105 skipped, and four
failures in existing AI-login, Git-fixture, and native runner-recovery
tests; it exited before the remaining workspace and serialized groups.
The AI-login test passed alone. Both Git fixtures passed with
command-scoped `init.defaultBranch=master`. The native recovery failure
also occurs on the parent revision on this Mac. These suites pass in CI.
- Greptile Apex scored the final commit 5/5. Its provider-coverage
comment was addressed and the review thread resolved.
- Reviewed the diff for secrets, private data, generated files, and
unrelated changes.

## Risks

- Missing finish times, later retry dates, active children, and
accounting debt continue to block sleep. Failed lease history still
requires an exact provider termination receipt.
- Future cleanup eligibility can wait for the next wake. Due cleanup
blocks sleep. Storage must remain attached across sleep and keep its
backup policy.
- Shared directories with a project workspace and unfamiliar providers,
strategies, modes, or configuration remain conservative blockers.
- No migration, provider teardown, retry scheduling change, or tenant
data deletion.

## Model Used

OpenAI Codex, GPT-6, with repository tools and code execution. The
runtime does not expose a more specific model snapshot 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
- [ ] I have run tests locally and they pass (focused tests pass; the
full local run has the environmental failures documented above)
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-11 12:27:51 -07:00
2223b7de9e perf(ci): stop building the standalone packages inside the public-install verifier (#15908)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Every pull request runs the `Build` check. Its last step is the
public npm install verifier, `scripts/verify-grok-npm-install.mjs`,
which stages the eighteen public packages reachable from the server and
the CLI, packs them, and installs them in a clean consumer.
> - Before it stages anything, the verifier runs
`scripts/build-standalone-public-packages.mjs`. That script installs and
compiles the seven sandbox-provider plugins that live outside the pnpm
workspace: one network `pnpm install --ignore-workspace` and one `tsc`
per plugin. The line was copied from the build block of `release.sh`
when the verifier was written in #14024.
> - release.sh needs that build because it publishes those plugins. The
verifier does not for anything it stages. Its package closure follows
`workspace:` specs from the server and the CLI, and a `workspace:` spec
can only name a workspace package, so none of the eighteen packages it
packs is a standalone package and no tarball reads that build's output.
> - One optional path at the end of the verifier does read it:
qualification-package retention, which `docker-runner-check.yml` enables
with `PAPERCLIP_RUNNER_QUALIFICATION_PACKAGES_DIR` after running only
`pnpm build`, packs the Daytona plugin from its `dist`. The PR `Build`
job never sets that variable.
> - The timing line from #15868 and #15888 puts the verifier's staging
phase at 75.8s on the latest run, of which the staged sources, the
bundled installs and the packs account for 46.5s. The rest, about 29s,
is the work before staging, and the standalone build is almost all of
it. `Build` at 433-530s is the slowest check in most PR runs once the
server shards drop, so that time sits on the run's critical path.
> - This pull request runs the standalone build only when retention is
requested, keeps the `ui-dist` copy the server package needs, and times
both on the timing line so what is left before staging is visible.
> - The benefit is a `Build` job about 21-28s shorter, with no change to
what the verifier installs or checks, no loss of coverage for the
standalone build because Canary Dry Run still runs it through
`release.sh`, and no change to the retention path that reads it.

## Linked Issues or Issue Description

Replaces #15892, which GitHub closed automatically when the branch of
#15888 (its base at the time) was deleted on merge. Same two commits,
rebased onto `master` on top of #15888 (1ac48aff), #15868 (026bdcd0) and
#15861 (85f4205d), which are all merged. #15892 reached Greptile 5/5
with every check green on the same content; the review history is there.

**What existing behavior does this improve?**

The `Build` PR check. Its public npm install verifier builds the seven
standalone sandbox-provider plugins before it stages the eighteen
packages it installs, and none of those packages uses that build.

**Subsystem affected**

CI and release scripts (`scripts/verify-grok-npm-install.mjs`,
`scripts/build-standalone-public-packages.mjs`).

**Current behavior**

The verifier runs `node scripts/build-standalone-public-packages.mjs`
and then `bash scripts/prepare-server-ui-dist.sh` before it stages any
package, on every run. The first script removes each sandbox-provider
plugin's `node_modules`, runs `pnpm install --ignore-workspace` against
the registry, links the in-repo plugin SDK, and runs the plugin's `tsc`
build, four plugins at a time. On the last `Build` run before this
change
([38146108750](https://github.com/paperclipai/paperclip/actions/runs/38146108750))
`stage_packages_ms` was 75.8s and the staged sources, bundled installs
and packs inside it took 0.9s, 15.7s and 29.9s, which leaves about 29s
before staging. The verifier's package closure, computed in the same way
as the script computes it, contains eighteen workspace packages and no
sandbox-provider plugin.

**Proposed behavior**

Run the standalone build only when
`PAPERCLIP_RUNNER_QUALIFICATION_PACKAGES_DIR` is set, because that is
the one path that reads it, and report it as `stage_standalone_build_ms`
when it runs. Always run the `ui-dist` copy before staging. With
`PAPERCLIP_RELEASE_REUSE_UI_DIST=1`, which the verifier already sets,
that copies the `ui/dist` that `pnpm build` produced into
`server/ui-dist`, which the server package publishes. Report that copy
as `stage_ui_dist_ms` on the timing line. Leave `release.sh` unchanged,
so the release flow and Canary Dry Run still build the standalone
packages where they are published.

**Reason and benefit**

The verifier proves that the eighteen public packages install cleanly
from the public registry. The standalone build adds seven network
installs and compiles whose output is never packed. Removing it from the
PR path shortens the staging phase by about 21-28s. Measured on this
PR's first commit
([38150787810](https://github.com/paperclipai/paperclip/actions/runs/38150787810)):
`stage_packages_ms` 54.2s against 75.8s, with `stage_ui_dist_ms` 21ms,
and the time before staging, `stage_packages_ms` minus the four staging
keys, down from about 29s to under 0.1s.

**Breaking changes**

None. The same eighteen packages are staged, packed, checked and
installed from the same sources. The retention path builds exactly what
it built before. `scripts/build-standalone-public-packages.mjs` exports
two existing helpers for the new guard test and otherwise does not
change.

## What Changed

- `scripts/verify-grok-npm-install.mjs`: the
`build-standalone-public-packages.mjs` call before staging runs only
when `PAPERCLIP_RUNNER_QUALIFICATION_PACKAGES_DIR` is set, in the same
position as before, and reports `stage_standalone_build_ms`. The flag is
read once into `retainQualificationPackages`, and the retention call at
the end is gated by the same variable, so the build and the retention
that reads it cannot diverge. The `prepare-server-ui-dist.sh` call
stays, is timed, and reports `stage_ui_dist_ms`. The comment explains
why release.sh and the retention path need the standalone build and the
staged packages do not. The timing-line comment lists both new keys.
- `scripts/build-standalone-public-packages.mjs`:
`parseWorkspaceEntries` and `isWorkspacePackage` are exported. No
behavior change.
- `scripts/__tests__/grok-public-install-sandbox.test.mjs`: one new
guard test. It asserts the verifier references the standalone build
exactly once, inside the retention gate, timed under its own key, and
that no second reading of the retention variable exists; that
`retain-runner-qualification-packages.mjs` still packs the Daytona
plugin from its `dist`, and that the Daytona plugin is a standalone
package, so the gate cannot pass vacuously; recomputes the verifier's
package closure from `release-package-map.mjs list` and the manifests
through `workspace:` specs; checks that the public package map still
carries packages outside the pnpm workspace; checks that every package
in the closure is inside the workspace globs of `pnpm-workspace.yaml`;
checks that the server package still publishes `ui-dist`; and checks the
order: closure, gated standalone build, `ui-dist` copy, then staging,
with the reuse flag set and the timing keys written.

### Review fix, second commit

Greptile's P1 on the first commit was valid: `docker-runner-check.yml`
runs `pnpm build` and then the verifier with
`PAPERCLIP_RUNNER_QUALIFICATION_PACKAGES_DIR` set, and
`retainRunnerQualificationPackages` copies
`packages/plugins/sandbox-providers/daytona/dist`. The Daytona plugin is
excluded from the pnpm workspace, so `pnpm build` never produces that
directory and the first commit would have failed that job with `ENOENT`
after every install check had passed. The second commit restores the
build on exactly that path: the verifier builds the standalone packages
when, and only when, retention is requested, and the guard test pins the
gate to the reason for it.

## Verification

- `node --test scripts/__tests__/grok-public-install-sandbox.test.mjs
scripts/__tests__/pack-staged-packages.test.mjs
scripts/__tests__/stage-bundled-packages.test.mjs
scripts/__tests__/prefetch-child.test.mjs
scripts/__tests__/retain-runner-qualification-packages.test.mjs`: 37
tests pass.
- The updated guard test fails against the first commit's verifier (no
retention gate around the build) and passes with the second commit. The
first commit's test failed against #15888's verifier the same way.
- `node --check` passes on the verifier.
- The verifier itself needs Docker and a network, which this workspace
does not have. The first commit's `Build` job
([38150787810](https://github.com/paperclipai/paperclip/actions/runs/38150787810))
ran the branch's verifier and measured the saving: `stage_packages_ms`
54.2s (was 75.8s), `stage_ui_dist_ms` 21ms, `stage_sources_ms` 0.8s,
`stage_bundled_ms` 21.0s, `stage_pack_ms` 32.4s, so under 0.1s remains
before staging where about 29s was. The second commit does not change
the PR path: the gate is false on every PR run, so that job's timing
line should show the same shape with no `stage_standalone_build_ms` key.
- The retention path was not run here: it needs Docker and the
`docker-runner-check` workflow's inputs. The code on that path is the
same `run(process.execPath, [...build-standalone-public-packages.mjs])`
call the verifier made before this PR, in the same position.
- `scripts/__tests__/release-dry-run-notes.test.mjs` exercises the
standalone build inside `release.sh` and is unchanged; 11 of its 12
tests spawn `pnpm`, which is not installed in this workspace, and fail
the same way on #15888's head. They pass in CI, where `pnpm` is
installed.
- The first commit's run failed only on `General tests (server
(11/12))`: all 74 files and 1257 tests passed, then Vitest caught a
teardown `EACCES` removing the per-worker `PAPERCLIP_HOME` under
`native-session-resumption.test.ts`, the same error as the first attempt
of #15888's run 38146108750. Neither PR touches server code; the failure
is noted on the tracking issue.

## Risks

- If a future change makes a package the verifier installs depend on a
standalone package, the verifier would stage an unbuilt `dist`. That
cannot happen through a `workspace:` spec, which is the only kind the
closure follows, and the guard test fails if the closure ever contains a
package outside the workspace globs.
- If a future path in the verifier reads a standalone build without
setting the retention variable, it would see no `dist`. The guard test
pins the single reference to the gate; a new reader has to extend the
gate and the test together.
- The sandbox-provider plugins' builds are no longer exercised on every
PR by the `Build` job. They are still built by `release.sh` in Canary
Dry Run on every PR, by the verifier whenever retention is requested,
and the `build-standalone-concurrency` and `release-dry-run-notes` tests
still cover the script.
- Low risk otherwise. The bundled installs, the packs, the consumer
install, and every probe are unchanged.

> 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 Fable 5.1 (Anthropic), model ID `claude-fable-5-1`, run as a
Paperclip agent through the Claude Code local adapter with tool use
(shell, file edits, GitHub CLI). Reasoning mode: extended thinking.

## Checklist

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

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

---------

Co-authored-by: Bender (Fable) <bender-fable@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: devinfoley <139239+devinfoley@users.noreply.github.com>
2026-10-11 12:14:32 -07:00
Devin FoleyandPaperclip 0ba88d2195 fix(opencode-local): preserve deadlines in Daytona process sessions (#15905)
## Thinking Path

> - Paperclip manages work done by AI agents.
> - The OpenCode adapter can run its CLI in a provider sandbox.
> - A long CLI run must remain observable until it exits or the host
stops it.
> - The legacy main command does not request a provider process session.
A launch without an active run step uses one synchronous command
request.
> - That request can lose its response before the resolved execution
budget ends.
> - This PR requests the existing asynchronous session path for the main
Daytona OpenCode command. It also carries the resolved command deadline
through the host and provider contracts. The host still applies the
effective capability gate.
> - The change removes that synchronous request from eligible sandbox
runs.

## Linked Issues or Issue Description

Related reporting fix: #15789. A search of open and closed GitHub issues
and PRs found no duplicate transport fix.

**What happened?**

A hosted Linux legacy OpenCode run retained a gateway-timeout response
about one hour after CLI startup. It had a four-hour resolved budget and
recent agent output. The earlier reporting fix correctly kept the
configured policy separate from the timeout message. The original
response owner and specific timer were not retained, so this PR does not
claim to prove that provider cause.

**Expected behavior**

A Daytona target that supports persistent process sessions should
dispatch the long agent command asynchronously. Paperclip should observe
its output and exit through the existing session path. A single command
HTTP request should not need to stay open for the full agent run.

**Steps to reproduce**

1. Use legacy OpenCode with a Daytona sandbox target and a four-hour
execution budget.
2. Launch outside an active measured step. The current main CLI call
does not request a persistent process session.
3. In the SDK fixture, make the synchronous command request return a
provider timeout before the budget ends. Keep the asynchronous session
healthy for more than one hour.
4. The regression tests require session dispatch, incremental output,
one command dispatch, a real exit result, and session cleanup.
5. Set a shorter command deadline and stall dispatch, a status read or a
log snapshot. The command must return a timeout at its own deadline.
Healthy output must not extend that deadline.

**Paperclip version or commit**

Observed source: `e733bddc45e00efb1819fffa8677f7b0d07802c5`. PR base:
`4b59e2a2ecc67e98b336b8eb4c09959681672d54`.

**Deployment mode**

Hosted Linux server, legacy OpenCode adapter, Daytona sandbox. The host
gate retains the old path when a provider cannot keep process sessions.

## What Changed

- Add an optional sandbox session choice to the shared process helper.
Leave the choice absent for existing callers.
- Request that session only at the main Daytona OpenCode CLI invocation.
Keep probes, preparation and other providers on their existing paths.
- Carry a separate optional command deadline through the shared runner,
host service and plugin SDK. Existing session callers can omit it. Zero
leaves the command deadline disabled.
- Bound dispatch, live output and recovery observations with one command
timer. Clear it on settlement. Fence late stream callbacks. Remove
deadline listeners after each read and clear interrupted polling waits.
Preserve a known exit code if only the final logs time out. A timeout
result does not claim that the process stopped.
- Add adapter and process-helper regression coverage. Test the existing
Daytona session for a virtual run beyond one hour, without command
replay, with release cleanup.
- Document transport selection and the limits of the timeout evidence.

## Verification

- `pnpm -r typecheck` passed on Node 25.6.1 and pnpm 9.15.4 with the
command deadline changes.
- `pnpm build` passed with the updated contracts. The separately
installed Daytona package also passes its own typecheck and build
against the pinned SDK 0.203.0 and TypeScript 5.9.3.
- Five focused process, adapter, host and provider suites passed all 349
tests. They cover a run beyond one hour, five stalled observation
phases, healthy-stream deadlines, zero/absent budgets, late callbacks,
session release, SSH and capability fallback.
- The real environment-runtime forwarding tests passed: 19 selected
tests, including the separate command budget and forced session flag in
the worker RPC.
- An earlier full local `pnpm test:run` began before a clean build and
overlapped the correction. That mixed-version run was stopped and is not
passing evidence. After the build, the two previously uncollected suites
pass (1,659 tests). Two unchanged workspace fixtures fail with this
machine's Git default branch `main`; both pass with a test-process-only
`master` override. One unchanged native recovery fixture fails on both
this branch and an independently installed and built checkout of base
`4b59e2a2ecc67e98b336b8eb4c09959681672d54`, with the same
terminal-result mismatch. It is not introduced by this change. The full
local suite is not claimed green. These limitations do not replace fresh
hosted CI.
- Final head `8e96638cdef8ca6e91f7a42a52c83a075180c137` passes all
hosted checks: 54 passed and 2 expected Storybook skips. Greptile
reviewed this exact head at 18:12 UTC and scored it 5/5. Both actionable
findings are fixed and resolved. GitHub reports no conflicts and a clean
merge state. Earlier-head results are not used as final-head proof.
- The standalone provider test command passes 373 tests, with 14
expected live-probe skips.
- SDK tests use a controlled virtual clock. No live provider experiment,
task replay, manual deployment, or public PR merge was performed.

## Risks

The main legacy OpenCode command uses Daytona's existing session route
when the effective capability gate permits it. A quiet log stream can
use the existing status and snapshot fallback. Those observations add
provider reads. The new optional command budget is separate from each
read's timeout and returns a timeout without the host RPC's extra
30-second allowance. Existing session callers that omit it keep their
previous lifetime.

Callback bridge control commands retain their explicit session bypass.
The capability gate retains the previous path for unsupported targets.
Other providers, local and SSH execution keep their existing path. Retry
policy, Stop acknowledgement and lease ownership rules stay the same.
The SDK exposes no cancellation handle for an abandoned stream; existing
Stop and lease teardown still own command termination. This fix does not
establish the original gateway owner or prove recovery in a live
environment.

## Model Used

OpenAI Codex, based on GPT-6, with reasoning, repository tools and code
execution. The exact backend identifier and context window size are 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 (changed paths; the
independently reproduced local baseline failure is documented above)
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-11 12:08:45 -07:00
1ac48aff8d perf(ci): pack the verifier's staged packages concurrently (#15888)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Every pull request runs the `Build` check. Its last step is the
public npm install verifier, `scripts/verify-grok-npm-install.mjs`,
which stages the eighteen public packages, packs them, and installs them
in a clean consumer.
> - The verifier packs the eighteen packages one after another with `npm
pack`. Each pack is a Node start-up plus gzip of that package's files.
The timing line added in #15868 shows this loop at 35.6-37.9s on four
`Build` runs, the largest part of the staging phase now that the bundled
installs overlap.
> - `Build` at 433-530s is the slowest check in most PR runs once the
server shards drop, so its staging phase sets the wall-clock time of the
run.
> - Each pack reads only its own `package-<index>` directory and writes
one tarball. The runner has four cores and the packs are CPU-bound.
There is no reason to run them one after the other.
> - This pull request runs the packs as concurrent children through the
prefetch helper, one per core, bundled packages first, each into its own
directory, and puts the results back into dependency order before every
tarball is checked.
> - The benefit is a shorter `Build` job, and a timing line that reports
the packs' serial cost next to their wall time, so the saving is
measured on this PR's own run instead of estimated.

## Linked Issues or Issue Description

Refs #15868, #15861. This branch is stacked on the branch of #15868,
which is stacked on #15861, because the same execution branch holds both
open PRs. The diff here is this PR's one commit. After #15868 merges,
this PR is retargeted to `master` and rebased.

**What existing behavior does this improve?**

The `Build` PR check. Its public npm install verifier packs the eighteen
public packages in one serial loop.

**Subsystem affected**

CI and release scripts (`scripts/verify-grok-npm-install.mjs`,
`scripts/prefetch-child.mjs`).

**Current behavior**

After the bundled installs are joined, the verifier runs `npm pack
--pack-destination <root>` for each of the eighteen packages in turn,
finds the new tarball by listing the shared root, and checks it with
`tar -tzf`. `stage_pack_ms` was 35.8s, 37.1s, 35.6s and 37.9s on the
four runs of #15868. The parent is idle apart from the one pack it waits
on, and three of the four cores are idle.

**Proposed behavior**

Start the packs as children through `startPrefetch`, at most one per
core (`os.availableParallelism()`), bundled packages first because they
make the largest tarballs. Each child packs into its own `pack-<index>`
directory, so the tarball a package produced is read from there instead
of guessed from a shared directory. Register each child for cleanup as
it starts. Put the results back into dependency order, then check every
tarball with `tar -tzf` as before, then copy them into the assets
directory as before. Report `stage_pack_check_ms`, `pack_concurrency`,
`pack_serial_ms` and `pack_longest_ms` on the timing line, and keep
`stage_pack_ms` as the whole step.

**Reason and benefit**

With four slots the step takes about the longest single pack or a
quarter of the serial sum, whichever is larger. The expected saving is
20-25s of the 36-38s, measured on this PR's own `Build` job by
`pack_serial_ms` against `stage_pack_ms`.

**Measured on this PR's first run
([38146108750](https://github.com/paperclipai/paperclip/actions/runs/38146108750),
`Build` 433s, verifier 214s):** `stage_pack_ms` 29.9s against 35.6-37.9s
before, so the step shed 6-8s, not the 20-25s estimated.
`pack_serial_ms` is 44.4s with `pack_concurrency` 4, and
`pack_longest_ms` is 28.3s: one pack (the server with its 513 MB bundled
closure, gzip level 9 inside npm) runs for almost the whole step, so the
step is bounded by that pack and not by the slot count. The seventeen
other packs fit inside it. The serial sum is 7-9s higher than the old
serial step, which is the four processes contending for the runner's
cores. `stage_packages_ms` 75.8s against 80.4-89.6s on #15868's runs;
`image_wait_ms` and `grok_wait_ms` stay 0.

**Breaking changes**

None. The same packages are packed with the same `npm pack` arguments,
the tarballs are checked the same way, and the consumer receives them in
the same order.

## What Changed

- `scripts/pack-staged-packages.mjs` (new): `packStagedPackages`
launches the packs in the order given with at most `concurrency`
children at once, through the prefetch helper, and resolves with each
entry's tarball path and wall time in the same order. Every child is
passed to `register` as it starts. The first failure rejects with the
tail of that child's log and nothing further is launched. A child that
leaves anything other than one tarball in its directory is a failure.
`packConcurrency` is one per core. `packTimings` gives the timing-line
keys.
- `scripts/verify-grok-npm-install.mjs`: each entry gets its `index` and
its own `pack-<index>` destination. The pack loop is replaced by one
`packStagedPackages` call, launched bundled-first and registered into
the `prefetches` list as each child starts, so an earlier failure stops
any pack still running before the scratch root is removed. The results
are sorted back into dependency order, every tarball is checked in that
order, and the copy into the assets directory is unchanged. Only the
lifecycle sentinel is still packed in-process.
- Timing line: `stage_pack_check_ms`, `pack_concurrency`,
`pack_serial_ms` and `pack_longest_ms` are added. `stage_pack_ms` still
covers the whole step, so the before and after numbers compare directly.
- `scripts/__tests__/pack-staged-packages.test.mjs` (new): five tests
with a local stand-in for npm and no network. The default concurrency
and the pack arguments; four packs with two slots run the two slow packs
together, start a third only once a slot is free, receive the exact `npm
pack` argument list, register every child, and come back in entry order
with their tarballs and timing keys although the fast packs finish
first; a failed pack rejects with its log tail, launches nothing more,
and leaves the running pack to be stopped; a pack that produces two
tarballs or none is a failure; and no entries resolve at once.
- `scripts/__tests__/grok-public-install-sandbox.test.mjs`: the guard
test on the verifier source now checks the import, the bundled-first
launch with cleanup registration, the reorder before the checks, the
checks before the copy, that only the sentinel is packed in-process,
that each pack has its own directory, and the new timing keys.
- `package.json`: the new test file is in `test:release-registry`.

## Verification

- `node --test scripts/__tests__/pack-staged-packages.test.mjs
scripts/__tests__/grok-public-install-sandbox.test.mjs
scripts/__tests__/stage-bundled-packages.test.mjs
scripts/__tests__/prefetch-child.test.mjs`: 35 tests pass (30 on
#15868's head plus the five new ones). The new file passes three
consecutive runs.
- `node --check` passes on both scripts.
- With the real npm 11.12.1 on Node 25.9: two small packages packed
through `packStagedPackages` with two slots produce one tarball each in
their own `pack-<index>` directory, with the expected `package/` listing
from `tar -tzf`.
- The verifier itself needs Docker and a network, which this workspace
does not have. This PR's own `Build` job runs the branch's verifier; its
timing line carries `stage_pack_ms`, `pack_serial_ms`, `pack_longest_ms`
and `pack_concurrency`, which is the measurement. Before, on #15868's
four runs: `stage_pack_ms` 35.8s / 37.1s / 35.6s / 37.9s.
- `docker-runner-check.yml` runs the same script after `pnpm build` and
needs no change.

## Risks

- Four `npm pack` processes run at once and share the host npm
configuration and cache. `npm pack --ignore-scripts` reads the package
directory and writes one tarball; it does not write to the cache beyond
its per-process log file. If a shared-state race appears, the failing
pack's log tail is in the error and the fix is to lower the concurrency.
- Memory and CPU: four Node processes instead of one on the four-core
`Build` runner, for about thirty seconds. The largest pack (the server
with its bundled dependencies) is the same process as before.
- The pack output moves from the CI log into per-pack log files in the
scratch root. A failure reports the last 2000 characters of the failing
pack's log; a successful pack's output is discarded with the scratch
root.
- The tarball checks (`tar -tzf`) now run after all packs instead of
after each pack. A bundled Codex payload is still rejected before any
tarball reaches the consumer; the only change is that the error arrives
a few seconds later.
- Low risk otherwise. The bundled installs, the consumer install, and
every probe are unchanged.

> 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 Fable 5.1 (Anthropic), model ID `claude-fable-5-1`, run as a
Paperclip agent through the Claude Code local adapter with tool use
(shell, file edits, GitHub CLI). Reasoning mode: extended thinking.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green (run 38146108750: `server
(11/12)` failed on its first attempt on a teardown `EACCES` while
removing the per-worker `PAPERCLIP_HOME` after all 74 files and 1257
tests had passed, in a file this PR does not touch; the re-run of that
job passed and every check is 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: devinfoley <139239+devinfoley@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
canary/v2026.1011.0-canary.11
2026-10-11 12:02:19 -07:00
3d3b8d4371 fix(external-objects): distinguish hidden GitHub repos from genuinely-missing objects (#15906)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Issues reference external objects such as GitHub pull requests. The
instance resolves their status and shows it in the UI.
> - GitHub returns 404 both when a pull request or issue does not exist
and when the repository exists but the configured credentials cannot see
it. GitHub hides private repos behind a 404 on purpose.
> - The resolver treated every 404 as a terminal not-found verdict. It
froze references to inaccessible repos forever, and marked them "Not
found (Stale)".
> - This pull request probes the repository metadata to tell the two
cases apart and keep frozen objects re-checkable.
> - The benefit is accurate statuses instead of permanent "Not found"
labels for repos the instance cannot see.

## Linked Issues or Issue Description

No public GitHub issue exists for this report.

**What happened?**
- The issue properties panel and markdown decoration showed many GitHub
pull request and issue references as "Not found (Stale)" even though the
objects exist.
- GitHub returns 404 for objects the configured credentials cannot
access, not only for objects that are truly gone.
- The resolver treated every 404 as a terminal not-found verdict, so
those references were frozen and showed a misleading "stale" liveness.
- Detected-but-unresolved GitHub objects were labeled "Not yet resolved"
in inline markdown, which did not match the pill label "Not yet
refreshed".

**Expected behavior**
- A reference to a repo the credentials cannot see should surface as
authorization-required, not as a permanent "Not found".
- A truly missing object should still be "Not found".
- A terminal object (merged, closed, or confirmed not-found) should not
be labeled "Stale", because it is never refreshed again.
- Inline markdown statuses should match the pill statuses.

**Steps to reproduce**
1. Configure GitHub credentials with no access to a private repo.
2. Add a comment or issue description that links to a pull request in
that repo.
3. Wait for the external object resolver to run.
4. Observe the reference labeled "Not found (Stale)".

**Paperclip version or commit**
- `master` at `e733bdd` (before this change).

**Deployment mode**
- Self-hosted server.

## What Changed

- Added a repository visibility probe to the GitHub external object
provider.
- A 404 for an object now probes the repository metadata with the same
credentials:
- Repository visible: the object genuinely is gone. Keep the not-found
snapshot.
- Repository hidden: return an authorization-required result instead of
a terminal not-found verdict.
- Probe failed (network, rate limit, 5xx): return an unreachable result
and retry later.
- A rate-limited repository probe (403 with `x-ratelimit-remaining: 0`)
is treated as uncertain, not as missing access, so it retries instead of
showing a misleading "Requires auth".
- Re-check previously frozen GitHub not-found objects on their normal
schedule so they self-heal once credentials or repo visibility improve.
A frozen object is only re-checked until a probe confirms the repo is
visible; once confirmed deleted, it is terminal and no longer polled.
- Cleared any terminal verdict when a refresh fails, so a failed refresh
does not freeze an object.
- Kept terminal objects from displaying the "stale" liveness label.
- Matched the inline markdown status label to the pill status label, so
detected-but-unresolved GitHub objects say "Not yet refreshed" instead
of "Not yet resolved".
- Skipped the liveness suffix in markdown links and status icons when it
duplicates the status label (e.g. no more "Requires auth (Requires
auth)").
- Cached repository visibility probes per company+repo + credential
fingerprint, so many objects in the same repo share one probe per
refresh cycle instead of each issuing its own — and a verdict obtained
under one token is never reused by a later refresh running under a
different credential.

## Verification

- Added provider tests for visible, hidden, failed, and rate-limited
repository probes, per-company/per-repo probe caching, and
probe-verified not-found snapshots.
- Added service tests for terminal-object liveness, for re-checking
frozen GitHub not-found objects, and for ensuring probe-verified
not-found objects are never re-queued.
- Added UI tests asserting unresolved GitHub objects render as "Not yet
refreshed" (not "Not yet resolved") and that failed-first-refresh labels
do not repeat the liveness text.
- Ran `npx vitest run src/__tests__/external-objects-service.test.ts` in
`server/`: 34 passed (incl. provider + service suites).
- Ran the external-object markdown/pill/icon suites in `ui/`: 97 passed.
- `ci / Typecheck + Release Registry` and all other CI gates pass on
this head. Local `tsc` could not run to completion in the execution
sandbox (the sandbox memory cap killed it), so CI typecheck is the
authoritative gate.

## Risks

- Low risk. The change is confined to the GitHub external object
provider and the external object service.
- A repo that genuinely does not exist and a private repo both return
404 for the probe. Both now surface authorization-required or a retry
instead of an immediate not-found freeze, which stays re-checkable and
self-heals.
- Re-checking frozen not-found objects adds at most one repository probe
per affected object until the probe produces a definitive verdict, after
which the object is terminal and no longer polled. Probe results are
cached per company+repo+credential, so a credential rotation always
re-probes instead of inheriting a stale verdict.

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

## Model Used

- Provider: DeepSeek via OpenRouter, model ID
`openrouter/~deepseek/deepseek-v4-flash-latest`, running inside the
Paperclip opencode adapter (`opencode_local`).
- The model produced and assisted with the reasoning and code changes;
the execution workspace, test suite, and git/PR steps above were run by
the same agent run.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [ ] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
— the branch name is assigned by the managed execution workspace and the
harness requires keeping it
- [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 on the current head (all `ci /
*`, Greptile, Socket, and review checks pass)
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: devinfoley <139239+devinfoley@users.noreply.github.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-11 11:53:21 -07:00