Commit Graph
6700 Commits
Author SHA1 Message Date
DottaandClaude Opus 5.5 5a5bfe689f fix(heartbeat): finalize an accepted native result after a graceful shutdown (#19591)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runner runs go through a durable coordinator row
(`native_run_finalizations`) that holds the controller's execution
lease, the accepted result, and the finalization phase
> - A graceful server shutdown (SIGTERM) drains running native runs: it
detaches the executor and hands the run to the next server boot
> - When SIGTERM lands after `run.result.accepted` / `run.terminal` but
before finalization, the drain did not release the execution lease,
because it only updated coordinator rows without a result
> - Startup restart recovery skips accepted results, and both the
finalization reconciler and the orphan reaper wait for an unexpired
coordinator lease (20 minutes for a local run). The run stayed `running`
and the task stayed `in_progress` with no Retry
> - This pull request makes the drain release the execution lease of an
accepted result too, so the next boot finalizes the durable result
> - The benefit is that a completed task finishes after a restart
instead of stalling silently

## Linked Issues or Issue Description

Refs #19525, #19530, #19513

**What happened?**
A restart-chaos wave sent SIGTERM to the server 0.75 s after a native
OpenCode run emitted `run.terminal` (result accepted, `runTerminalState:
succeeded`). The graceful drain listed the run in
`restartSuspendedRunIds`. On the next boot, startup recovery claimed
only another run. The accepted run stayed `running` for more than 10
minutes. Its task stayed `in_progress` with
`interruption.state=working`, and no Retry appeared.

**Expected behavior**
After a restart, a run whose result was accepted and whose terminal
state is known is finalized as succeeded, and its task is completed. It
must never stay silently `running`.

**Root cause**
`drainRunningRunsForShutdown`
(`server/src/services/heartbeat/recovery.ts`) updated the coordinator
only `where result_id is null`. For an accepted result, the update did
nothing, so the dead controller's execution lease
(`NATIVE_SESSION_EXECUTION_LEASE_TTL_MS`, 20 min) stayed valid. The
detached executor does not finalize (by design, it hands off to the next
controller). Then:
- `claimNativeRestartRecoveries` only claims rows with `result_id is
null`.
- `reconcileNativeFinalizations` only selects rows whose lease is null
or expired.
- The orphan reaper skips a native run with pending workspace
finalization or an unexpired controller lease, and defers to the lease
deadline. The 5-minute stale threshold is not involved.

**Steps to reproduce**
See the new regression test: accept a result through
`PaperclipControlPlanePort.completeRun` under a held 20-minute execution
lease, run the SIGTERM drain, then run startup recovery on a new
heartbeat service.

## What Changed

- `drainRunningRunsForShutdown` now releases the coordinator execution
lease (`leaseExpiresAt = now`) for an accepted result as well. It keeps
`recoveryState` unchanged for an accepted result, because the run needs
finalization, not a runner reattach. Rows without a result keep the old
behavior (`awaiting_runner_reattach`).
- A `native-finalizer:` lease on an accepted result is kept. That lease
belongs to a finalization that is in progress in the draining process,
and the shutdown waits for it.
- The drain event for an accepted result now says that the result was
released for finalization (`runnerDisposition:
awaiting_result_finalization`).
- New regression tests in
`server/src/__tests__/heartbeat-native-restart-suspension.test.ts`:
- An accepted terminal result under a held execution lease, then SIGTERM
drain, then next-boot recovery: the run becomes `succeeded`, the
coordinator becomes `committed`, the task becomes `done`, there is one
result row and one run, and no provider turn starts.
  - A `native-finalizer:` lease survives the drain unchanged.

## Verification

- Red before the fix: `cd server && pnpm exec vitest run
src/__tests__/heartbeat-native-restart-suspension.test.ts -t "accepted
terminal"` failed with `{ status: 'running', phase:
'workspace_finalizing' }` instead of `{ status: 'succeeded', phase:
'committed' }`.
- Green after the fix: the same file passes 5/5.
- Related suites pass: `src/services/heartbeat/recovery.test.ts`,
`src/services/legacy-controller-lease.test.ts`,
`src/services/native-runtime/native-runner-restart-recovery.integration.test.ts`
(80 passed, 14 skipped), and
`src/__tests__/heartbeat-process-recovery.test.ts` except one case.
- The one failing case in `heartbeat-process-recovery.test.ts`
("dispatches interrupted CLI input and cleans up early fixture exits
(true)") also fails on unchanged `origin/master`. It is not related to
this change.
- `server` typecheck (`tsc --noEmit`) passes.
- Restart-chaos replay of the original cell: in progress. Results follow
in a PR comment.

## Risks

- Low to medium. The change only affects the graceful SIGTERM drain of
native runs owned by this boot.
- A rolling deploy can overlap the old and new servers. The old process
detaches the executor before it releases the lease, so it does not
finalize the result. A finalizer that already started keeps its
`native-finalizer:` lease, so the new server cannot finalize the same
result at the same time.
- If a `native-finalizer:` lease is in progress and the process exits
before it finishes, that run still waits for the 5-minute finalizer
lease. This was already the behavior and is out of scope.
- No schema, API, or UI change.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 14:11:34 -06:00
Devin FoleyandPaperclip 1c9767d323 test(workspaces): remove archive fixture open-order assumptions (#19585)
Select held archive reads by source path and observe replacement before releasing sibling reads. Validation: red reordered-open reproduction, all 102 archive tests and all applicable CI pass.

Greptile scored the exact head 5/5, and no review threads remain unresolved.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-11-06 12:05:02 -08:00
Devin FoleyandPaperclip 50038fac70 fix(native-runtime): contain backup verification deadline aborts (#19556)
Reject an already-aborted backup verification before constructing its file stream so cancellation does not raise an uncaught AbortError. Validation: 60 focused tests and all applicable CI pass.

Greptile scored the exact head 5/5, and no review threads remain unresolved.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-11-06 12:04:56 -08:00
Devin FoleyandPaperclip 91b641757b fix(sentry): keep low-trust project denials on the task (#19531)
Keep exact pre-dispatch project access denials on their task instead of the unexpected Sentry failure stream. Validation: 301 focused tests and all applicable CI pass.

Greptile scored the exact head 5/5, and no review threads remain unresolved.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-11-06 12:04:51 -08:00
DottaandClaude Opus 5.5 24508a8ba5 perf(dev): add agent preflight with a shared content-keyed build cache (#19555)
perf(dev): add agent:preflight with a shared content-keyed build cache

Agent worktrees each built the Runner from nothing: a cold cargo debug and
release build, the Runner TypeScript build and its checks, about 8 minutes
per fresh worktree on a loaded machine.

`pnpm agent:preflight` replaces the manual install + Runner build sequence.
It skips steps whose inputs are unchanged and reuses other worktrees'
results from ~/.cache/paperclip/agent-build:

- Runner and eval-kernel dist, keyed by the Runner build-input digest and
  the toolchain
- Runner debug binaries, keyed by every file the Rust build reads
- a cargo seed with third-party crates only; the Runner crate is scrubbed
  because cargo records its out-of-package includes by absolute path

Entries are written once with an atomic rename and restored as APFS clones.

Also exclude provider-assets from the Runner build inputs. The build writes
that directory itself, so the first build in every fresh checkout saw its
inputs change and never recorded a build stamp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 14:01:54 -06:00
Devin FoleyandPaperclip 17901d74ba Fix onboarding account changes after returning from Review (#19557)
Save changed onboarding connections on the existing agent after validation.
Select the chosen personal provider account and serialize default changes so
an abandoned save cannot override a later choice after Back. Preserve runner,
environment, and unrelated agent settings. Reject unsupported managed-account
to legacy-secret switches with an actionable Connect error.

Verified with 327 component tests, three backend credential-selection tests,
nine affected browser tests, typecheck, build, token gates, all CI jobs, and
Greptile 5/5 with no unresolved comments.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-11-06 11:55:29 -08:00
Nicky LeachandClaude Opus 5.5 79c724b292 ci: run the Runner support suite as a required PR check (#19537)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The Runner support suite (`pnpm test:e2e:runner:unit`) checks the
code behind paid Runner runs: workflow security policy, evidence
oracles, cost receipts and controller recovery.
> - Only a manual Docker Runner check ran this suite. PR CI did not run
it.
> - So changes on master broke 16 of its tests, and nobody saw it.
#19527 and #19529 fix those tests.
> - This pull request runs the suite in PR CI and makes the `verify`
check require it.
> - The benefit is that a change that breaks the suite fails on its own
pull request, before merge.

## Linked Issues or Issue Description

Refs #19527 and #19529 (both merged; they repair the suite). Also
accounts for #19533, which added the sandbox provider job.

**Merge order:** #19527 and #19529 have merged, so the suite now passes
on master. This PR can merge.

**What existing behavior does this improve?**

PR CI for `pr-trusted.yml`.

**Current behavior**

`pnpm test:e2e:runner:unit` runs only in `docker-runner-check.yml`, by
manual dispatch. Master changes broke 12–13 tests by Nov 3 and 16 by Nov
6, with no signal on any pull request.

**Proposed behavior**

A required "Runner support suite" job runs the suite on pull requests.

**Reason and benefit**

Drift fails on the pull request that causes it. Authors update
deliberate pins (profile versions, suite hashes, workflow policy) in the
same pull request.

## What Changed

- `pr-trusted.yml`: new job `verify_runner_e2e_support` ("Runner support
suite"). It uses the same steps as other trusted jobs: checkout without
credentials, setup-node directly before pnpm, the npm overrides, the
shared `setup-ci-dependencies` restore, and CI profiling. Then it runs
`pnpm test:e2e:runner:unit`. It has no secrets. Timeout: 20 minutes.
- `pr-trusted.yml`, job `verify`: the job now needs the new job and
checks its result. A failed, cancelled or wrongly skipped run fails
`verify`.
- `turbo.json`: new routing task `ci:runner-e2e-support`. Its inputs are
`server/**`, `cli/**`, `packages/**` and `ui/**`. Changes to `tests/**`,
`scripts/**` and `.github/**` already force full CI, so they also run
the job.
- `.github/ci-task-matrix.json`: one lane for the new job.
- Tests: `ci-plan.test.mjs`, `ci-profile.test.mjs`, `e2e-shard.test.mjs`
and `workflow-security.test.ts` now include the new job. A UI component
change now selects this one Runner task. The suite imports UI modules
and checks that catalog-listed UI files stay tracked in git, so UI
changes can break it.
- CI profiling: when `PAPERCLIP_CI_PROFILE` is on, the suite's Vitest
config adds `scripts/ci-profile/vitest-reporter.mjs`, and `run-unit.mjs`
runs the Node tests through `scripts/ci-profile/node-test.mjs`. So the
job uploads per-test timings, like the root and Runner package suites.
The launcher loads the profiling helper only after its "Vitest is not
installed" check.
- `tests/runner-e2e/README.md`: says that PR CI runs the suite, and that
deliberate pin changes go in the same pull request.

## Verification

- `node --test .github/scripts/tests/*.test.mjs scripts/ci/*.test.mjs
scripts/__tests__/e2e-shard.test.mjs
scripts/__tests__/release-verify-workflow.test.mjs
scripts/__tests__/runner-release-crate-cache.test.mjs`: 830 passed, 3
failed. The same 3 fail on the base branch without this change. They
need local tools that are not on this Mac.
- A local run with `PAPERCLIP_CI_PROFILE=true` wrote per-test and
per-file timing records for Vitest and for Node. Without profiling,
output and exit codes do not change. Both `unit-launcher.test.mjs` tests
pass.
- The aggregate test now covers the new job: `verify` rejects it when it
fails, is cancelled, or is skipped while selected.
- The pnpm bootstrap policy tests in `workflow-security.test.ts` pass
with 10 setups in `pr-trusted.yml`.
- On this PR's merged tree (`80b526d697`, master plus this PR): `pnpm
test:e2e:runner:unit` passed, Vitest 320 files (6,830 tests) and
node:test 373, in 58s. It also passes on master `4c1f629c31`.
- Suite runtime on a 10-core Mac: 43–63s. With `--maxWorkers=4`: Vitest
118s, about 490s of CPU in total. On a 4-vCPU GitHub runner I expect
about 3–4 minutes for the tests and 1–2 minutes for setup. The job runs
in parallel with the longer test shards, so it should not lengthen CI.
- `pr.yml` calls `pr-trusted.yml@master`. This PR's own CI does not run
the new job. The first real CI timing comes from the first pull request
after merge.

## Risks

- More runner minutes: one extra job on most pull requests (all except
documentation-only ones).
- The suite has not run on Linux CI recently: old `docker-runner-check`
logs have expired. A Linux-only failure would show up on the first pull
requests after merge. The job can be removed from the `verify` list
quickly if needed.
- #19229 removes `version: 9.15.4` from the pnpm steps in this file.
Whichever PR merges second needs a small rebase.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), by Anthropic, in Claude Code with
tool use (shell, file edits, git, GitHub CLI).

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 11:50:42 -08:00
DottaandClaude Opus 5.5 e8f07b2a39 test(restart-chaos): fire faults relative to user actions and run state, add K6 crash loop (#19513)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The control plane must keep tasks moving when the server restarts
> - The restart chaos harness (#19477) kills the server only at a random
time after a task starts
> - Many restart bugs depend on what the user or the run was doing at
that moment: a comment just sent, a Retry just clicked, a run still
queued, a long tool call, or a second crash during startup recovery
> - The harness cannot aim at these moments yet, so these cases occur
only by chance
> - This pull request adds fault triggers that fire relative to a user
action or a run state, and the K6 crash loop
> - The benefit is that chaos waves can target these boundaries on
purpose and replay them

## Linked Issues or Issue Description

Refs #19477

**What is the problem?**
The restart chaos harness fires each fault at a seeded random offset
from task launch. It cannot fire a fault right after the harness posts a
comment to an active run, right after a Retry click, while a run is
queued, during a long tool call, or a second time while the server is in
the `recovering` startup phase.

**Why is it a problem?**
These are the boundaries where restart bugs hide (plan faults K6 and K8,
boundaries B1, B6 and B8). Random timing seldom hits them, and a replay
cannot repeat them.

**What is the proposed solution?**
Add a `--triggers` option. Each task samples a trigger with its cell.
The observer arms the fault when the trigger condition holds, then fires
it after a seeded delay. Add the K6 crash-loop fault to the supervisor.

## What Changed

- `types.ts`, `config.ts`: add `TRIGGER_IDS` (`time`, `comment`,
`retry`, `queued`, `tool`), the `--triggers` option, and the `K6` fault
kind.
- `scheduler.ts`: sample a trigger per task. `comment` pairs only with
C1 and `tool` only with B5. Each trigger has a seeded delay range.
`retry` adds an extra K1 after the planned fault. A replay re-arms the
task's own triggered faults.
- `observer.ts`: arm each triggered fault from the observed state: the
follow-up reached a running run, an accepted Retry, the first run still
queued, or the first `bench.sh` progress. A trigger that can no longer
hold is recorded as `triggerMissed`.
- `supervisor.ts`: K6 kills the replacement again as soon as
`/api/ready` reports `recovering` (or `ready`, if recovery was too fast
to see), then once more just after `ready`. The receipt records the
phase that each kill hit.
- `main.ts`, `ledger.ts`: pass the trigger to the fault receipt, the
timeline and the ledger. The summary shows the trigger in each cell.
- `triggers.test.ts`: unit tests for planning, firing, misses, replay
and the crash-loop phase rule.
- `README.md`: document the triggers and K6.

## Verification

- `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts`
(52 tests pass).
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos` passes.
- Live: `node cli/node_modules/tsx/dist/cli.mjs
tests/restart-chaos/main.ts --env local --adapters codex,claude,opencode
--faults K1,K6 --triggers comment,retry,queued,tool --concurrency 3
--tasks 6` runs waves with the new triggers.

## Risks

- Low risk. The change is in the test harness only. The default trigger
is `time`, so existing waves and replays keep their behavior.
- A trigger can miss its window (for example, a run starts before the
first poll). The ledger records the miss, so the task is not counted as
a crossing of that boundary.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 13:49:52 -06:00
DottaandPaperclip 124b2cd0ad fix(server): certify background deadlines for idle tenants (#19001)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Background services own future checks, retries, and recovery work.
> - An idle tenant can stop only when a durable wake covers its
remaining duties.
> - The current alarm catalog covers several domains but leaves these
owners blocked.
> - A saved future timestamp alone cannot prove that reconstruction or
admitted work has settled.
> - This pull request binds durable domain snapshots to the actual
registered worker revisions.
> - The tenant can hand off supported future duties and retain all
checks for uncertain or active work.

## Linked Issues or Issue Description

No matching public issue was found. Related work: [domain deadline
handoff #18656](https://github.com/paperclipai/paperclip/pull/18656),
[owned blocker diagnostics
#18632](https://github.com/paperclipai/paperclip/pull/18632), and
[completed history proof
#18646](https://github.com/paperclipai/paperclip/pull/18646). The
separate [native idle-session change
#18988](https://github.com/paperclipai/paperclip/pull/18988) covers
runtime retirement. [Far-future retry re-probes
#13534](https://github.com/paperclipai/paperclip/pull/13534) changes
recovery responsiveness after an upstream success; this change preserves
existing retry decisions.

**What happened?**

Future status-card, external-object, skill-sync, watchdog, retry, and
quota duties lacked complete owner-bound coverage in the idle deadline
proof. A completed quota task could also retain its monitor policy and
timestamp and remain blocked after the scheduler had disarmed it.

**Expected behavior**

A settled registered owner can hand off its exact supported future
deadline. Active work, dirty reconstruction, transaction uncertainty,
changed revisions, claims, unknown state, and unsupported duties keep
the tenant awake. Completed history retains its data and stops recurring
work when the owner has no remaining duty.

**Steps to reproduce**

1. Produce a supported future duty through its normal service.
2. Let the registered worker finish reconstruction and admitted work.
3. Request an owned idle report with the negotiated domain deadline
catalog.
4. Check the saved earliest deadline and generation before durable alarm
registration.
5. Stop and reconstruct the worker. Check that ordinary admission and
claims resume useful work once due.
6. Complete a quota task. Keep its saved policy and timestamp. Check
that linked unfinished work still blocks sleep and that settled history
does not.

**Paperclip version or commit**

The owned patch was qualified at
`699c8569fa34338dd75cdc5731381ec1e2efdb45`. Publication head
`491fac368ee72eaac8ae105d53e6bf6b305d8a6a` normally merges actual master
`039e0dad2c51c80251e1302a19c5566e039c357a`. All 43 owned files are
byte-identical. All 29 new upstream paths are preserved. The later
normal current-master integration is
`8c912e1ba4fc66397a7341ade59373af8075068f`, described separately below.

**Deployment mode**

Source checkout with real PostgreSQL 17 and a controlled local Node
process. No deployed alarm, tenant suspension, database suspension, or
fleet result is claimed.

## What Changed

- Add registered-worker reconciliation revisions and invalidate them on
stop.
- Keep status-card, external-object, and skill deadline interpretation
with their owners.
- Reuse the watchdog classifier against the supplied database snapshot.
- Validate the exact saved retry run and paired queued wake.
- Validate legacy quota source bindings and the current assignee or
reviewer policy.
- Preserve common issue scope, dependency, sibling, and state checks for
covered waits.
- Compose normalized owner snapshots within the existing bounded
deadline proof and recheck revisions after inspection.
- Bind actual service and worker instances from server startup. Missing
bindings preserve existing blockers.
- Retain completed quota policy and timestamps while independently
rejecting linked unfinished runs or wakes.
- Block unconsumed budget evaluation inputs through the owning queue
predicate. Accounting policy and settlement stay unchanged.
- Bind completed quota history through fixed-size UUID-array parameters,
preserve exact company/task/source associations, and guard external
references before the run-ID join.
- Batch watchdog subtrees and shared classifier facts without changing
the 64-query or two-second proof budget; preserve exact company pairing
for first-run evidence.
- Extend production-path regression controls and document supported
coverage and limits.

## Verification

Current integration head: `688abc6cac6016b9fb50e5832f81e2975ed5d49f`.
Fresh exact-head checks completed with **58 successes, three intentional
skips and one neutral review**, including aggregate `ci / verify`. The
complete current Greptile review is **5/5**. The complete current
Ponytail verdict is **Ship (4/5)**, with only optional cleanup of an
unused MCP proof input; both prior correctness findings are resolved.
All review threads and nested pages were inspected. The normal master
merges preserve the registered retry deadline owner and current startup
completion log after the same readiness await. The earlier focused
evidence below remains tied to its tested source revisions.


Current-master integration `8c912e1ba4fc66397a7341ade59373af8075068f`:

- Normally merge actual master
`46fb933b0155d183ce64a8bdc4950d1b58c926f6`. Every nonoverlapping tree
blob retains its owning parent across 14,044 paths. All 42 owner-only
files and 15 critical binding/proof/readiness files remain
byte-identical. Three automatic overlaps preserve the existing watchdog
binding, conservative unavailable-reader fallback and optional actual
owner arguments. The two scoped-deadline conflicts retain the entire
upstream resource-reservation/mid-pass admission/abort algorithm inside
the existing reconciliation revision/running/failure fence, with both
parent test sets.
- Actual TypeScript 7.0.2 server noEmit passed in 9.434 seconds, normal
source declaration layers in 12.102 seconds, and four selected
test/helper/config layers extending the actual CI configuration in 3.167
seconds. No compiler option weakening, heap override, donor declarations
or SDK replacement stub.
- Focused PG0 controls passed: 102 scheduler/proof/owner controls in
16.451 seconds, including all 21 scoped-deadline cases; 121 startup
controls; and all 107 unchanged instance-settings route cases in 6.456
seconds. The latter first hit a local prewire-guard
listener-registration race. Only the ignored guard was corrected to
register the same actual OS-bound server port immediately after native
listen returns. Closed, unknown, foreign, offhost and PG destinations
remain blocked. Original failure evidence is retained; product code and
assertions were unchanged.
- The exact current UI policy command now reports zero findings (0.362
seconds); its seven scanner controls pass. The earlier dependent PR used
an old scanner/source baseline under a newly enforcing trusted workflow
and reported 397 findings. Carrying the genuine current master resolves
that mismatch without editing unrelated UI sites or relaxing the gate.
- Every meaningful worker checked its executing PID/thread/preload hash
and matching receipt. PG0 controls opened no PostgreSQL sockets; all
owned processes exited. Original dependency-cache/DNS, ignored-harness
syntax/type and guard failures remain separate from product results.
- This is fresh semantic and focused runtime qualification, not a local
whole-repository or current-head PostgreSQL acceptance rerun. The
upstream workspace-busy parked-retry/terminal-holder notification path
remains unchanged by this merge and needs its separate current-graph
producer check. Historical six-case/scale results below stay tied to
their measured sources. A matching controlled default-cadence follow-up
is in #19191; no fresh current-head two-cycle or deployed suspension
claim follows from this merge.
- All 59 exact-head check contexts now pass (55 success, one neutral,
three intentional skips), plus the legacy security status. The complete
edited Greptile review is 5/5 for this exact commit, with all
root/nested pages inspected and no unresolved threads. Ponytail stopped
without completing its new turn; it supplied no new verdict or finding.
Prior actual findings remain addressed. Earlier scores/checks below
belong to their explicit earlier commits.


On frozen clean merged source `699c8569fa`:

- Real PostgreSQL acceptance: **6/6 passed**,83.483 seconds wall. Actual
retry producer creates the exact saved run/wake; stopped and
reconstructed owner resumes through current admission, one durable
start, real local process output and normal task completion. Includes
cancellation, drain arrival, retained quota history with queued-run/wake
negatives, watchdog review settlement and budget-input
empty/pending/consumed control using the actual owner.
- Affected coordinator, deadline, scoped-deadline, recovery and
composed-proof controls: **78/78 in five files passed**,32.114 seconds
wall, worker 1/no file parallelism.
- `pnpm exec tsc --noEmit -p server/tsconfig.json`: **passed**,17.467
seconds. Native TypeScript 7.0.2 and Node 24.22.2.
- PostgreSQL tests use one dedicated test database, matching
fixture/admin URLs, pool limit four, one worker, and no embedded
fallback. Every run recorded unchanged source hashes and a clean
checkout.
- The eight-second real-clock retry is a domain execution check. It is
below the Cloud minimum wake lead and is not a positive accepted hosted
sleep cycle.
- The completed quota-history product failure and original
fixture/protocol failures were preserved; the owning-reader fix passed a
real PostgreSQL red/green regression before the fresh merged six-case
pass.
- After runtime completion, the dedicated database had zero other
clients and no owned runtime processes. All three qualification process
PIDs exited. The coordinator independently audits and drops the
database.
- Comment-only cleanup has equal strict TypeScript parser ASTs across 15
files. The final lexical audit finds no added comments relative to
actual master. `git diff --check` passed.
- Earlier retry fixtures omitted responsible-user membership, advanced
JavaScript time past the real PostgreSQL lease clock, or produced stdout
without normal task completion. Corrected fixtures use real membership,
an eight-second real deadline, and ordinary task comment/completion. The
last case exposed legitimate disposition repair rather than duplicate
retry promotion.
- The quota and watchdog controls first ran before admitted work or the
existing 30-second reconstruction duty settled. The stronger tests
require an interim blocked report and then exact settled-owner proof.
Production deadlines were not changed. The separate
completed-quota-history defect was fixed in its reader and verified with
linked unfinished-run and wake controls.
- The status/object and skill owners reviewed the actual final startup,
route and post-backup bindings. Their domain source evidence carries
where token-equivalent. The master coordinator coalescing change was
reviewed separately. Historical domain PostgreSQL results are not
presented as fresh full-suite results on this head.

The later master merge adds retry predecessor cleanup checks under the
existing issue claim lock. The source audit confirms unchanged saved
deadlines and run/wake identities. The three actual retry
lifecycle/cancellation/drain cases passed on the batched candidate with
this parent (31.817 seconds). Those local dependency controls preceded
the current exact-head remote run. Native backup preservation and
TestOps changes from master are retained. No pass is inferred for
changed dependency behavior solely from unchanged owned files.

Review repair published as `643f1e7400`:

- The original reader returned `unknown` after seven actual watchdog
reviews settled across seven companies. The unchanged composed-report
regression passed after batching; the final reader passed in 61.841
seconds, including active queued-work and stopped-owner rejection.
- A separate two-company red/green control rejects terminal-run context
from another company as first-run grace evidence. Shared facts are
serialized once, with per-root IDs and review summaries.
- The affected recovery, proof and diagnostic unit controls passed 47/47
(14.575 seconds). Startup owner-contract fixtures passed 121/121 (10.589
seconds).
- Diagnostic PostgreSQL controls passed7/7 (12.406 seconds), including
the normal budget-input producer and its exact blocker reason. Initial
empty-state setup uses the same cleanup as per-case teardown when a
dedicated test database is reused.
- Actual repository `pnpm exec tsc --noEmit -p server/tsconfig.json`
passed 12.710 seconds with no heap override. Final source has no added
code comments; whitespace checks pass.
- The original query-budget failure, two-company discriminator, initial
cross-suite fixture failure and an interrupted incorrect local-driver
selector are retained separately. The interrupted run is not a pass.
Independent cleanup found zero owned clients or runtime processes.

Quota-scale repair published as `6696a37f8a`:

- The exact 10,000-row completed-history case reproduced
`MAX_PARAMETERS_EXCEEDED`:90,002 bound parameters exceeded the driver's
65,534 limit, and the composed report returned `unknown` despite settled
owners. Original failure and actual driver diagnostics are retained.
- Fixed pending-work queries use three typed UUID-array parameters plus
the result limit, keeping exact company/issue/source tuple bindings. The
primary run-ID column stays uncast; a guarded CASE validates the raw
external reference before conversion. The 10,000-row cap and
64-query/two-second proof budget are unchanged.
- The same 10,000-row composed-report case passed 18.860 seconds, with
unfinished run/wake rejection, malformed-reference safety and
conservative 10,001-row overflow. Its fixture derives retained history
from the actual quota producer and normal task completion, expands valid
persisted issue/source pairs, then starts the actual registered owners.
- The existing actual quota producer case passed 14.187 seconds,
including exact future deadline, retained completion history, unfinished
work, real foreign-company source references and noncanonical/malformed
references.
- Affected owner/proof controls passed 22/22 (26.444 seconds).
Production server noEmit passed 16.039 seconds.
- CI on 643 identified TS7034/TS7005 in the settled-watchdog fixture's
initially untyped collection. It is now explicitly typed. A separate
affected-entry semantic check extending the repository's actual
`server/src/__tests__/tsconfig.json` passed 15.093 seconds: six affected
tests, two owner files, ambient declarations and current imported
source, with unchanged strict/module/lib/path settings. The production
server config excludes that test directory and is not presented as
test-layer proof. No custom suppression or replacement SDK stub was
added.
- All runs were sequential with pool max 4, worker 1,
source/config/command hashes and cleared inherited
provider/LIVE/other-database variables. Cleanup found zero owned
database clients or local runtime processes. At prior head
`6696a37f8a5718cd0f79054969a0084462d41d47`, all required CI gates pass.
The exact-head inventory has 55 successes, one neutral review and three
intentional skips. Greptile is 5/5. The complete latest Ponytail verdict
confirms the quota finding is fixed; its only remaining note is optional
unused MCP proof state, not a behavior defect. All three review threads
are resolved. The earlier 643 result remains historical.

The repository-wide typecheck, tests, build and browser gates passed in
the prior `6696a37f8a5718cd0f79054969a0084462d41d47` CI run. The new
`8c912e1ba4fc66397a7341ade59373af8075068f` full CI also passes. They
were not run as a full local workspace command in the focused PostgreSQL
lease. Local evidence above uses bounded external databases and does not
claim embedded-cluster, live-provider or deployed-alarm qualification.

## Risks

- An unsupported or uncertain owner state must remain blocked. An
optimistic exemption could lose work while compute is stopped.
- Changes during reconstruction or inspection must invalidate the proof.
A memory-only timer is not durable alarm coverage.
- Current authorization, budgets, cancellation, accounting, and resource
ownership still apply on dispatch. The proof does not grant execution
authority.
- The supported writer model uses in-process transaction signals and
startup/resume reconstruction. Independent SQL writers and other
replicas need an explicit wake contract.
- Native, review, coalesced, and taskless retry variants remain
conservative until their owner cases qualify. Reviewer quota and hosted
alarm qualification remain open.
- The shared catalog remains opt-in. Legacy callers and missing owner
bindings retain their old blockers.

## Model Used

OpenAI `gpt-6.1-sol` was the configured model for the delegated service
implementations and reviews. OpenAI Codex, based on GPT-6, performed
managing review and integration. Both used repository tools and code
execution. The exact served backend snapshots and context-window 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 (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
- [x] I have run tests locally and they pass on the final merged head
- [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 — exact current-head run passes
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups —
complete current-head review and all paginated threads inspected
- [ ] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 13:48:55 -06:00
DottaandClaude Opus 5.5 694076245f fix(status-cards): inline query guidance in the compile task instead of an unprovisioned skill (#19554)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Status cards let the board watch a slice of company work with a
prose prompt
> - A new card starts a compile task. The summarizer agent compiles the
prompt into search queries and writes the first summary.
> - The compile task told the agent to use the bundled
`status-card-query` skill. Nothing gives that skill to the built-in
Summarizer or to a per-card override agent.
> - The agent saw no assigned skills, got `runner_skill_not_assigned`,
and then searched the filesystem until the run deadline. It never wrote
the card query.
> - This pull request puts the query guidance directly in the compile
task, so the task names only things the run has.
> - The benefit is that any summarizer agent, on any model, can compile
and save the card query without a missing dependency.

## Linked Issues or Issue Description

**What happened?**
Creating a status card with a summarizer on a smaller model (for example
an OpenCode agent on a DeepSeek flash model) never finished. The compile
run reported no assigned skills, then spent its full budget looking for
`status-card-query`. No `PUT /api/status-cards/{id}/query` request was
sent, and the run hit its deadline. Stronger models sometimes worked
around the gap.

**Expected behavior**
The compile task contains everything the run needs. The agent writes the
query, reads the compiled scope, and writes the first summary.

**Steps to reproduce**
1. Enable status cards and configure a summarizer agent that does not
have the `status-card-query` skill (the default Summarizer only installs
`summarize-status`).
2. Create a status card.
3. Watch the compile run: it looks for the skill and does not write the
query.

## What Changed

- `server/src/services/status-cards.ts`: the compile task no longer says
"Use the bundled `status-card-query` skill". It now inlines the query
format (fields, allowed status and priority values from shared
constants, limits), the project and label lookup endpoints, an example
query body, and an explicit `GET /api/status-cards/{id}/dry-run` step
before the summary write.
- The example body uses a `text` fence, so the existing parser still
reads the `json` generation payload.
- New `server/src/__tests__/status-card-compile-prompt.test.ts`: asserts
that the compile task names no skill, contains the full query format and
write-back sequence, has an example that passes
`companySearchQuerySchema`, and keeps a parseable generation payload.

## Verification

- `cd server && npx vitest run
src/__tests__/status-card-compile-prompt.test.ts
src/__tests__/status-cards.test.ts
src/__tests__/status-card-evaluation-work.test.ts`: 3 files, 74 tests
pass.
- `npx tsc --noEmit -p server`: no errors in the changed files.
- `pnpm verify` selected 2224 test files. Most failed in this fresh
worktree because `@paperclipai/plugin-sdk` was not built yet (`Failed to
resolve entry for package "@paperclipai/plugin-sdk"`). After a build of
that package, the status-card files above pass. Required CI supplies the
broad coverage.

## Risks

- Low risk. The change is prompt text only. The write-back routes,
validators, and authorization do not change.
- The compile prompt is longer. The added text is a small, bounded
block.
- The `status-card-query` catalog skill still exists for agents that
author cards. The two texts can drift. The new test pins the prompt
contract.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 13:48:45 -06:00
Nicky LeachandPaperclip 1391f64551 Add gated release/resume methods to plugin Runner owners (#19584)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Core owns Runner tasks through a lease-bound plugin owner.
> - The SDK already defines release and resume in #19468.
> - The owner still expects an accepted result for both operations, so
it cannot use those SDK results.
> - This pull request adds both methods behind the existing capability,
warm-task and account checks.
> - It uses the SDK parser to verify the result and task ID, and
preserves the owner's cancellation checks.
> - This prepares the owner boundary for later lifecycle integration. It
does not start automatic release or resume.

## Linked Issues or Issue Description

**Problem or motivation**

The plugin Runner owner cannot forward the existing SDK release/resume
pair. Its local result mapping rejects their valid results. Later
lifecycle code needs both operations on the same checked task owner.

**Proposed solution**

Pass the existing description capability to the owner. Add explicit
release and resume methods and use the SDK's operation-result parser.
Require warm-task support and a selected account. Reject calls and late
receipts after owner cancellation or detachment.

**Alternatives considered**

Adding automatic release here would also require connection quieting,
settled-event drain, pending-ticket retry ownership and safe disposal.
Keep those changes together in the next integration. Do not add a new
flag, schema, timer or lifecycle mode here.

Related: #19468. I checked ROADMAP.md and found no duplicate planned
item. An open-PR title search for release/resume found no overlapping
pull request.

## What Changed

- Add the capability-gated owner facade for Codex and ACPX. Resume
returns only `resumed` or `unavailable`.
- Use the SDK parser for exact task-ID and operation-result validation.
- Pass `releaseResumeVersion` from the checked description and test that
it does not cause an automatic operation.
- Cover account-preserving owner rebind, missing prerequisites,
malformed results, ambiguous errors, cancellation and late replies.
- Document the caller's remaining settlement, connection, ticket and
attachment duties.

## Verification

Passed locally on Node 26.12.0 with the pinned workspace dependencies:

- In `server/`: `pnpm exec vitest run
src/services/native-runtime/plugin-runner-task.test.ts` — 74 tests
passed.
- In `server/`: `pnpm exec vitest run
src/services/native-runtime/native-session-executor.test.ts -t 'uses a
task-only'` — 3 cases passed; 1,160 unrelated cases were not selected.
- Built the real workspace SDK dependencies and Runner TypeScript. A
focused strict TypeScript program for the owner and its tests passed.
- `git diff --check` and the added-line secret/PII scan passed.

The first root test command selected a nonexistent Vitest project name
and ran no tests. Running the test from the server package fixed the
command. The first focused TypeScript setup could not find Node types;
pointing it to the package's installed type definitions fixed that
setup. The full regression suite and release checks run in GitHub.

## Risks

The facade has no automatic caller. Existing warm timeout, bootstrap,
outbound connection and pool behavior stay in place. No plugin
capability is advertised or task control channel enabled by this pull
request.

Future integration must serialize release with new work and disposal. It
must quiet the owned connection, keep the exact pending ticket after an
ambiguous reply, authenticate the source again and finish ordinary run
attachment before new work. This change does not qualify provider
parking, cold recovery, retention policy or deployment.

## Model Used

OpenAI GPT-6.1 Sol, exact model ID `gpt-6.1-sol`, High reasoning. Used
code execution and targeted tests. The context-window size was not
exposed.

## Checklist

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

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 11:42:32 -08:00
Devin FoleyandPaperclip bc4cad3ac8 fix(environments): report an undescribable plugin Runner as a failed check (#19172)
Return a retryable environment check when a plugin Runner cannot be described. Preserve stable Runner revision mismatch checks and preconfigured Runner-home handling.

Validation: 108 focused tests pass. All 53 applicable checks pass, with three intentional skips. Greptile scored the exact head 5/5 and no review threads remain unresolved.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-11-06 11:37:55 -08:00
Nicky LeachandClaude Opus 5.5 4c1f629c31 test(evals): repair the Runner support suite after master drift (#19529)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The Runner support suite (`pnpm test:e2e:runner:unit`) tests the
code behind paid Runner qualification: workflow security policy,
evidence oracles, cost receipts and controller recovery.
> - PR CI does not run this suite. Only a manual
`docker-runner-check.yml` run does.
> - So deliberate changes on master broke its tests, and nobody saw it.
There were 12–13 failures on Nov 3 and 16 on Nov 6.
> - This pull request finds the master change behind each failure and
fixes the side that is wrong. No security check becomes weaker.
> - The benefit is that the suite passes on master again (with #19527),
so it can become a required check.

## Linked Issues or Issue Description

Refs #18516, #18625, #18983, #18325, #19265, #18994, #17009, #19195.
Companion PR: #19527 (the one workflow fix).

**What happened?**

At master `543aa4d331`, `pnpm test:e2e:runner:unit` had 16 failures in 8
files. At `98ebeefbc9` I saw 15 in 7 files. The two embedded-Postgres
files passed in every run I made, and the 256MiB packaging test timed
out once.

**Expected behavior**

The suite passes on master. Each test still checks the rule it was
written for.

**Steps to reproduce**

Run `pnpm install`, then `pnpm test:e2e:runner:unit` on master.

## What Changed

Each failure, its cause, and the fix:

- **`workflow-security.test.ts`: chat catalog workflows hold provider
keys and use `runner-e2e-private-paid`.** This is a deliberate policy.
#18516 admitted guarded, pinned-source chat TestOps cells, with special
checks. #18625 removed the pilot workflows and their checks. It did not
move the checks to the full-catalog workflows from #18542 and #18647,
which use the same trust model. The fix adds `chat-catalog-testops.yml`
and `chat-catalog-testops-case.yml` to the provider-key allowlist. It
also brings back the #18516 checks, adapted to the caller plus
reusable-workflow layout:
- The cell is `workflow_call` only. Its single caller is the master-only
dispatcher.
- The trusted guard runs before the pinned candidate checkout.
`verify-selection` runs before each provider key.
- The paid job never sees `ALLURE_TOKEN`. The report job runs in
`runner-e2e-private-history` and has no candidate checkout. The upload
job has no private environment and no secret except `ALLURE_TOKEN`.
- Provider keys appear only in the two caller jobs. Those jobs need
`authorize, select, build` and pass the guard's `target_sha`.
- I mutation-tested this: an `ALLURE_TOKEN` in the paid job fails, and a
second caller workflow fails.
- **`workflow-security.test.ts`: `ref: ${{ github.sha }}` 6 vs 4,
`persist-credentials: false` 12 vs 10.** #18983 added two trusted
TestOps jobs (`testops_selection`, `publish_testops`). Both check out
reviewed code next to the TestOps token. This is the safe direction. The
counts are updated. A new check makes sure both jobs check out only
`github.sha`, do not install the candidate lockfile, and use no secret
except `ALLURE_TOKEN`.
- **`pi-file-evidence.test.ts` (4 tests): "one observed provider
validation execution".** #19265 deliberately changed the ACPX tool
normalizer to keep the latest concrete title. Pi's shell call now goes
from `bash` to the exact command. The ACPX sidecar uses this normalizer,
so the oracle would also reject real Pi runs. The oracle now accepts one
change only, from `bash` to the exact validation command, with no change
back. It also counts every completed `execute` call, whatever its name.
This is stricter than before: a refined title cannot hide a second shell
command. New mutations cover a regressed name, a different command, and
an extra call that has its own command name. The retained 44-row real
stream still passes.
- **`pi-controls-evidence.test.ts`: Pi profile 37.** #18325 deliberately
reset Pi to profile 37. The pin and the `pi-native` suite hash are
updated. I checked that the old hash comes back exactly when the version
is set to 36. So the version is the only change.
- **`controller-outage.test.ts`: 1 continuity cell, not 2.** #18994
deliberately limited this lane to providers with managed warm sessions.
The test now expects exactly `runner-codex`.
- **`operating-receipts.test.ts` (3 tests): "Operating overhead
allowance needs review".** This is a time bomb from #17009. The fixture
allowance was valid from Oct 1 until Nov 1, 2026. `admitAttempt` reads
the real clock. The allowance window is limited to 31 days, so a longer
fixture is not possible. These tests now pin only `Date` to the fixture
time. Timers and the filesystem stay real. I ran the full suite with the
clock moved to Dec 15, 2026, and found no other date-bound failures.
- **New file `private-trust-workflow-security.test.ts`.** The private
archive trust test, the chat catalog checks and the full-stack TestOps
job checks moved here from `workflow-security.test.ts`. That file would
have crossed 1,000 lines. It is now 970 lines. The checks did not
change.
- **`remote-native-bootstrap.test.ts`: cannot load
`@paperclipai/plugin-sdk`.** Since #19195, server code under test
imports the built plugin SDK. `run-unit.mjs` now runs
`scripts/ensure-plugin-build-deps.mjs` first, as `test:run:general`
already does. This step is cached.
- **`private-framework-report.test.ts`: 256MiB packaging test timed
out.** It takes about 10s alone and more than 30s with the whole suite
running in parallel. It now has a 120s budget.
- **`controller-database.test.ts`,
`controller-recovery-observer.integration.test.ts`: no change.**
Embedded Postgres started and both files passed in all my runs. The
earlier failures look like local resource problems.

## Verification

- Combined check (this branch plus #19527, on master `5d25117ffa`):
`pnpm test:e2e:runner:unit` — Vitest 317 files passed (6,815 tests, 3
skipped), node:test 373 passed, 0 failed. It took 63s on a 10-core Mac.
- `pnpm test:e2e:runner:typecheck` — passed.
- This branch alone fails only the two pnpm-setup tests that #19527
fixes.
- Focused files: `pi-file-evidence` 75 passed, `pi-controls-evidence` 72
passed, `operating-receipts` 12 passed, `workflow-security` 14 of 16
passed (the other 2 pass with #19527), `private-trust-workflow-security`
3 passed, `remote-native-bootstrap` 71 passed.

## Risks

- Low risk. Only test files, one evidence oracle under
`tests/runner-e2e/`, and the suite launcher change. No product code
changes.
- The Pi oracle change affects how paid Pi file qualification is graded.
It accepts the old and new title shapes, and it is stricter about extra
shell calls. A live Pi qualification run would confirm it on real
Rust-normalized events. I did not run one, because it is paid.
- The chat catalog policy is the same as #18516. Under that design, the
pinned candidate source runs in `runner-e2e-private-paid` with
`id-token: write`. The guards above limit who can start it and which
source runs. This PR does not change that trust model.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), by Anthropic, in Claude Code with
tool use (shell, file edits, git, GitHub CLI).

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
canary/v2026.1106.0-canary.19
2026-11-06 11:26:44 -08:00
DottaandClaude Opus 5.5 86e9c9354d test(opencode): pin Stop during a pending MCP tool call (#19550)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The native runner runs OpenCode agents and settles their cost after
each run
> - An operator can stop a run while the agent waits inside a Paperclip
MCP tool call
> - Before #18610, OpenCode then kept calling the model after the Stop.
The turn never closed, so the run cost never settled
> - #18610 fixed the cause, but no test covers a Stop during a pending
MCP tool call
> - This pull request adds that regression test at the driver boundary
> - The benefit is that a change to empty-answer recovery or
interruption cannot bring back runaway model work after a Stop

## Linked Issues or Issue Description

Refs #18610 (the fix that this test pins)
Refs #19497 (accounting that waits on a terminal run receipt)

**What happened?**

A board operator stopped a native OpenCode run. The agent was blocked
inside a gated `paperclip_call_assigned_tool` call. The runner
acknowledged the interrupt, but OpenCode made 8 more model requests in
the next 60 seconds. No `turn.cancelled` event arrived. The run stayed
at `costAccountingPending: true` and `accountingReceiptReady: false`, so
cost accounting never settled.

**Expected behavior**

A Stop aborts the provider. The turn closes with `turn.cancelled`. The
provider makes no new model requests. Cost accounting then settles from
the final receipt.

**Steps to reproduce**

1. Start an OpenCode turn with a dynamic Paperclip tool whose handler
does not return.
2. Let the provider call that tool through the MCP bridge.
3. Fail the pending call (runnerd does this first on Stop), then
interrupt the turn.
4. Let OpenCode report the abort as a bare `session.idle`.
5. Before #18610, the driver sent a corrective "give a final answer"
prompt. The provider then continued work.

**Root cause**

The driver sent `/session/:id/abort` correctly. OpenCode 1.18.34 aborts
correctly, and sometimes reports the abort only as `session.idle`.
Empty-answer recovery treated that idle as an incomplete answer and sent
a new prompt. #18610 added `#interruptedTurnId`, so recovery now stops
for an interrupted turn. The failing eval ran on `3b66c3c144`, which is
earlier than #18610.

## What Changed

- Add
`packages/paperclip-runner/src/drivers/opencode/interrupt-pending-tool-call.test.ts`.
It calls the real MCP bridge over HTTP, holds the dynamic tool call
open, and interrupts the turn. It checks three abort reports:
- `idle-after-settle`: runnerd fails the call, then the interrupt, then
a bare idle.
- `idle-before-settle`: an idle while the call is still pending, then
the late failure, then an idle.
  - `aborted-error`: `MessageAbortedError`.
- Each case asserts one prompt only, exactly one terminal event
(`turn.cancelled`), and no empty-response correction diagnostic.
- No product code changes. Master already has the fix.

## Verification

- `pnpm --filter @paperclipai/paperclip-runner exec vitest run
src/drivers/opencode/interrupt-pending-tool-call.test.ts`: 3 passed on
this branch.
- The same test at `3b66c3c144` (before #18610): `idle-after-settle` and
`idle-before-settle` fail with 2 prompts (the corrective prompt).
`aborted-error` passes. This matches the eval symptom.
- `vitest run src/drivers/opencode/`: 14 files, 185 tests passed.
- `tsc -p tsconfig.test.json --noEmit` for the runner: no new errors. A
pre-existing error in `src/eval/workflow-harness.ts` comes from an
unbuilt local eval-kernel package.
- `server` `native-stop-accounting.integration.test.ts`: 5 passed. This
confirms that settlement completes after a Stop when the terminal event
and final receipt arrive.
- Manual, not committed: the same flow with the real OpenCode 1.18.34
binary, a mock OpenAI-compatible model, and a gated tool. I tested it
through the driver alone and through runnerd, the proxy, and the driver.
The interrupt produced `turn.cancelled`, and the model received no more
requests. I also tried different gate-release and model-latency timings.
- The new test waits for the terminal event with a bounded
`expect.poll`, not a fixed delay (review feedback).
- `pnpm verify` exited 2 (broader verification required) because the new
test file is outside the resolved test graph. CI supplies the broad
coverage.

## Risks

Low risk. This change adds one test file only. The test uses the
existing fake OpenCode fixture and an injected `fetch`. It uses real
timers with short waits.

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

## Model Used

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 13:23:24 -06:00
TonioandPaperclip c650f36007 fix(ui): refine agent profiles, identity editing, and list actions (#19535)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Agent pages expose identity files, reporting relationships, and
runtime settings.
> - Several controls had inconsistent spacing, weak navigation, or
misleading save feedback.
> - Empty task-row detail slots also reduced the title width before
truncation.
> - This pull request fixes those behaviors and aligns the reviewed UI
details.
> - The result is clearer agent editing and more readable task and agent
lists.

## Linked Issues or Issue Description

**What happened?**
Agent headers and file controls had inconsistent layout. The Identity
footer appeared without edits and did not provide clear save
confirmation. File names could wrap or omit the Markdown suffix. Task
titles truncated while empty detail slots occupied the row.

**Expected behavior**
Keep headers and actions aligned. Reveal the Identity footer only for
changes or save feedback. Use valid Markdown file names and actual file
dates. Let titles use available space.

**Steps to reproduce**
1. Open an agent overview, then its Identity tab.
2. Edit, undo, save, copy, and add an instruction file.
3. Resize the file panel and inspect file names and date labels.
4. Hover an agent row and inspect the Chat action and tooltip.
5. Open a task list with no recovery or external details and inspect
title width.

Related: #18716 is the prior UI bundle. #16934 and #14483 concern role
editing; this change does not add a role picker.

## What Changed

- Refine the character header, inline title and name editing, crown
position, runtime shortcut, and Chat/New task actions.
- Align profile metadata and manager selection; hide empty direct
reports.
- Refine Identity panels, Markdown filename validation, file-row hover,
truncation, Paper Mono typography, and copy feedback.
- Add a viewport-bound save footer with dirty, saving, and saved states.
Keep undo/discard separate from successful saves.
- Return recorded creation or modification dates for instruction files.
Reuse the active file transaction for metadata reads to avoid exhausting
the small connection pool.
- Reveal the agent-row Chat icon and label on hover, with a standard
delayed tooltip.
- Collapse empty task-row details so titles can use the available width.

## Verification

- Browser checks covered the local overview and Identity flows, narrow
file panels, successful file creation/update, undo/discard, header
navigation, row hover, tooltip delayed-open state, and full task titles.
- Focused instruction, save-footer, date, font-asset, and task-row tests
passed during development. After review, 55 file/instruction tests
passed, including cold restoration with recorded and unknown dates;
server typecheck and 18 immediate-save response tests also passed.
- Full `pnpm -r typecheck` and `pnpm build` passed after rebase onto
current master. The complete `pnpm test:run` attempt hit a 15-second
timeout in the existing chat Retry HTTP integration test; that case
passed in isolation (1 test passed). A second full attempt hit timeouts
in two other chat integration cases (GitHub webhook draining and
delivery retry budget); both passed in isolation without increasing
timeouts. All 55 remote checks passed on f31405a730, including the full
test matrix, build, typecheck, and final verification gate.
- `pnpm check:token-gates`, `node scripts/check-maps.mjs`, and `git diff
--check` passed on the submitted revision.

## Risks

- File dates are optional. Managed files require recorded dates because
restored projections have synthetic filesystem timestamps. External
files can use modification time; unknown dates are omitted.
- Save-footer timing depends on animation completion; reduced-motion and
failed-save paths have tests.
- Global Paper Mono typography can change text widths. Browser checks
covered the edited surfaces.
- No schema migration or new execution permissions are introduced.

## Model Used

OpenAI Codex, GPT-6, with repository tools, code execution, and browser
verification. The exact serving model ID and context-window size 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 references)
- [x] My branch name describes the change and contains no internal
Paperclip ticket id or instance-derived details
- [x] I have run the relevant tests locally and they pass; full-run
timeout limitations and successful isolated reruns are documented above
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 11:14:56 -08:00
DottaandClaude Opus 5.5 705c3e955d fix(chat): show one onboarding question card when onboarding moves to agent chat (#19512)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - New users start with an onboarding first task. It has an opening
question card and, after "Interview me", an interview question card.
> - With the coordinator chat enabled, the agent's chat with the user
mirrors pending questions from that agent's direct tasks. The onboarding
task is a direct task.
> - A tester moved onboarding from the task into the chat. The agent
asked its interview in the chat. The chat then showed the chat card and
the mirrored onboarding-task card. Both were answerable.
> - This pull request expires the agent's pending onboarding-task
questions when the agent asks a question in its chat with that user.
> - The benefit is one interview card, answerable once, wherever
onboarding runs.

## Linked Issues or Issue Description

**What happened?**
Onboarding testing on Paperclip Cloud staging (Codex API path). The
steps were: enable the experimental chat and coordinator features, start
the "Interview me" onboarding task, switch to the agent chat, and ask
the chat to run the onboarding there. The chat agent started the
interview, and a duplicate question card appeared.

**Root cause**
`conversationDecisionReceiptsService.listPendingQuestions` projects
pending questions from direct tasks (created by the chat user, assigned
to the chat agent) into the chat. The onboarding first task matches. The
PAP-437 sibling-supersede rule in `issueThreadInteractionService.create`
only works on the same issue, and it is off for chat questions. So the
task's pending interview card and the new chat card both stayed pending
and visible in the chat.

**Expected behavior**
One interview question card in the chat, answerable once.

**Steps to reproduce**
1. Enable the experimental Agent Chat and coordinator features.
2. Create the onboarding first task and choose "Interview me" on its
opening card.
3. Open the chat with the same agent and ask it to run the onboarding
there instead of in the task.
4. The agent asks its interview in the chat. The chat shows two pending
question cards: its own card and the mirrored onboarding-task card.

**Paperclip version or commit**
`master` at 819420a60a (Paperclip Cloud staging build at the time of the
test).

**Deployment mode**
Paperclip Cloud (staging). The same logic runs in self-hosted mode.

## What Changed

- `server/src/services/issue-thread-interactions.ts`: when the chat
agent creates an `ask_user_questions` card in its chat with a user, the
server expires that agent's pending `ask_user_questions` cards on the
onboarding first task that the same user created and assigned to the
agent. The result is the existing `superseded_by_newer_interaction`
shape and points at the chat card. Pending chat publications for those
cards are cancelled and terminal publications are queued, the same as
the existing sibling supersede.
- The rule does not change questions on other direct tasks, handoff
tasks, or other chats. Confirmations and plan approvals on the
onboarding task are not changed.
- `server/src/__tests__/coordinator-handoff.test.ts`: a new red/green
test for the reported flow, and a control test that an ordinary
direct-task question stays pending when the agent asks something else in
chat.
- `server/src/onboarding-assets/first-task/README.md`: one paragraph
that documents the behavior.

## Verification

Red on `master` (819420a60a) with only the new tests added:

```
$ cd server && npx vitest run src/__tests__/coordinator-handoff.test.ts -t "onboarding interview|ordinary direct task"
 × moves a pending onboarding interview into the agent chat instead of showing two question cards
AssertionError: expected [ …(2) ] to deeply equal [ Array(1) ]
  [
    "26b1e6ae-e1e5-4c26-8475-ad7ad93a7754",
+   "2364fd68-a40f-42be-854d-89210bfc5811",
  ]
 ❯ src/__tests__/coordinator-handoff.test.ts:231:53
      Tests  1 failed | 1 passed | 23 skipped (25)
```

The chat showed its own interview card and the mirrored onboarding-task
interview card.

Green after the fix:

```
$ cd server && npx vitest run src/__tests__/coordinator-handoff.test.ts
 Test Files  1 passed (1)
      Tests  25 passed (25)
```

Neighbouring suites:

```
$ npx vitest run src/__tests__/issue-thread-interactions-service.test.ts src/__tests__/issue-onboarding-first-task-routes.test.ts src/__tests__/chat-interaction-publications.test.ts src/__tests__/coordinator-handoff.test.ts
 Test Files  4 passed (4)
      Tests  183 passed (183)
```

`tsc --noEmit -p server` shows no errors in the changed files. Other
errors in that local run come from the unbuilt
`@paperclipai/paperclip-runner` package. CI typecheck covers the full
build.

Not done: a live browser repro. Staging has no Codex company and no
agent chat with this flow. The evidence is the tester's notes and the
service-level test.

## Risks

- If a user ignores the onboarding card and the agent asks an unrelated
question in its chat, the onboarding task's pending question card
expires. The onboarding task stays open, and a message on the task still
wakes the agent. The chat already showed both cards to that user, so
this keeps one live ask from the agent.
- Race: if the onboarding task run posts its interview card after the
chat card, both stay pending. Agent Chat questions are intentionally not
expired by other cards, so this PR does not reverse the rule.
- No schema, API, or UI change.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 13:10:45 -06:00
DottaandClaude Opus 5.5 fca1f1be4f fix(attention): keep replaced confirmations out of Needs you after the newest is settled (#19520)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Needs you is the board inbox. It lists the decisions that wait for a
person.
> - Agents often ask for the same confirmation again on one task. Needs
you shows only the newest pending confirmation for each task.
> - When a person approved that newest card (for example in chat), or
the card expired, the older card that it replaced came back into Needs
you.
> - Manual onboarding tests reported this: decision cards that were
already approved in chat still showed in Needs you.
> - This pull request makes the newest answered or expired confirmation
also hide the older pending confirmations on the same task.
> - The benefit is that Needs you does not show stale cards after a
person answers the current one.

## Linked Issues or Issue Description

**What happened?**
A person approved a decision card in chat. Needs you still showed a
decision card for the same task. Expired cards also caused an older card
to show again.

**Steps to reproduce**
1. An agent creates a confirmation on a task. Then it creates a second
confirmation on the same task.
2. Needs you shows only the second card.
3. Approve the second card in chat (or let it expire).
4. The first card now shows in Needs you.

**Expected behavior**
Needs you shows no card for the task, because the newest ask is settled.

**Root cause**
`collapsePendingConfirmationsToNewest` compared only pending rows. Chat
cards use the same `issue_thread_interactions` row and the same accept
service, so there is no duplicate record. The server already drops
answered rows. The problem was the older pending row that the collapse
hid before.

## What Changed

- `server/src/services/attention-source-eligibility.ts`: the collapse
takes the newest settled (accepted, rejected, answered, expired)
ordinary confirmation for each issue. Older pending ordinary
confirmations stay hidden. A new `loadNewestSettledConfirmationAt`
helper reads that value with one grouped query.
- Cancelled and failed replacements do not settle the ask, so the older
card returns (the existing retention test keeps this behavior).
- Tool-action confirmations and quality-review confirmations are
excluded from the settled check. A settled tool action never hides a
different pending tool action.
- `server/src/services/attention.ts` and
`server/src/services/attention-retention.ts` use the new input, so the
feed and the retention worker agree.
- Review follow-up: the summary and task-index payload keeps
`toolAction` and `secretProposal`. A settled card only hides older asks
addressed to the same recipient. Tool actions, secret proposals and app
connections (`connectionAuthorization`) form one governed set. They
match the set the other confirmation readers use. A settled governed
card never settles an ordinary ask, and a pending governed card is never
hidden.
- `server/src/__tests__/attention-service.test.ts`: new tests for
"approved from chat", "rejected from chat", "expired", a re-ask after a
settled card, another recipient, a settled secret proposal, tool actions
in `list()`, and tool actions in `summary()`/`taskIndex()`. The tests
use two agents, because the service already expires a same-agent re-ask
at creation.

## Verification

Red on `master` (819420a60a), before the code change:

```
 RUN  v5.0.3 /Users/dotta/paperclipai/paperclip/.claude/worktrees/fix-n10-stale-needs-you-cards-47c895/server
 ❯ src/__tests__/attention-service.test.ts (36 tests | 2 failed | 34 skipped) 14165ms
   ❯ attention service (36)
     × does not resurface an older confirmation once the newest one is approved from chat  3502ms
     × does not resurface an older confirmation once the newest one is expired  2097ms
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯
 FAIL  src/__tests__/attention-service.test.ts > attention service > does not resurface an older confirmation once the newest one is approved from chat 
AssertionError: expected [ Array(1) ] to deeply equal []
- Expected
+ Received
- []
+ [
+   "a1f2f085-e4b2-42f1-ac3c-71d85f6e164c",
+ ]
 ❯ src/__tests__/attention-service.test.ts:1609:32
    1607|     }
    1608|
    1609|     expect(await pendingIds()).toEqual([]);
       |                                ^
    1610|   });
    1611|
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯
 FAIL  src/__tests__/attention-service.test.ts > attention service > does not resurface an older confirmation once the newest one is expired 
AssertionError: expected [ Array(1) ] to deeply equal []
- Expected
+ Received
- []
+ [
+   "8c8d01ad-1084-47b2-927a-24d18529a9a6",
+ ]
```

Green after the change (same tests):

```
$ npx vitest run src/__tests__/attention-service.test.ts -t "resurface an older confirmation|tool-action approval"
 ✓ does not resurface an older confirmation once the newest one is approved from chat
 ✓ does not resurface an older confirmation once the newest one is expired
 ✓ keeps a pending tool-action approval visible after a newer tool action is settled
```

Review follow-up red (on ebe8e0c0ae, before the follow-up code change):

```
 × keeps a pending tool-action approval in the summary and task index after a newer ordinary confirmation is settled
 × does not let another recipient's settled confirmation hide this recipient's pending one
 × does not let a settled secret proposal hide an older ordinary confirmation
AssertionError: expected [] to include '4c636383-…'
AssertionError: expected [] to deeply equal [ Array(1) ]
```

All three pass after 9245bd7b17.

Second review round red (before a36923c7fc):

```
 × does not let a settled app connection hide an older ordinary confirmation
AssertionError: expected [] to deeply equal [ Array(1) ]
```

It passes after a36923c7fc.

Neighbouring suites (attention-service, attention-retention-work,
issue-review-attention, issue-blocker-attention):

```
      Tests  125 passed (125)
 Test Files  4 passed (4)
```

Staging check (read-only): the ONB staging company had one pending Needs
you item, on a task with no newer card. Staging has no Codex company, so
I could not reproduce the Codex path there.

## Risks

- Low risk. The change is read-only and adds one grouped query when
pending confirmations exist.
- Older ordinary confirmations from the same agent were already expired
at creation. This change affects asks from different agents or users on
the same task.
- The service already expires an older ordinary confirmation from the
same agent at creation. This change affects asks from different agents
or users on the same task.
- Behavior change: an older ordinary confirmation that is still pending
stays out of Needs you after a newer one is answered or expired. It
stays visible in the task thread.
- Not changed: the live-update listener does not match
`issue.interaction.withdrawn` or `issue.interaction_cancelled`. Those
cards can stay in the client cache until the next refetch.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 13:10:26 -06:00
github-actions[bot]andlockfile-bot b5c06d660a chore(lockfile): refresh pnpm lockfiles (#19560)
## Thinking Path

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

## Linked Issues or Issue Description

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

Run a frozen install before and after this lock refresh.

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

Co-authored-by: lockfile-bot <lockfile-bot@users.noreply.github.com>
2026-11-06 19:09:36 +00:00
Nicky LeachandClaude Opus 5.5 7dfdc97b9f fix(deps): let Dependabot update the sandbox providers (#19489)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Sandbox providers (`packages/plugins/sandbox-providers/*`) are
standalone published packages. `pnpm-workspace.yaml` keeps them out of
the root workspace on purpose.
> - `.github/dependabot.yml` watches only `/`, so Dependabot never saw
the providers.
> - Because of this, the Kubernetes provider stayed on zod 3
(GHSA-mrhx-c7xc-m3w5) after the rest of the repo moved to zod `^4.6.6`.
#18839 fixed it by hand.
> - We do not want to keep these versions current by hand in many
places.
> - This pull request lets Dependabot update the providers, and keeps
each provider standalone.
> - The benefit is that a provider dependency that falls behind gets a
pull request that can pass CI.

## Linked Issues or Issue Description

Refs #18839 (adds `scripts/typecheck-sandbox-providers` and fixes
Kubernetes zod by hand).
Refs #18998 (a Dependabot pull request for the Pi distribution that
fails CI by design).

**What existing behavior does this improve?**
Dependabot version updates. They skip every package that the root pnpm
workspace excludes.

**Subsystem affected**
CI and dependency automation: `.github/dependabot.yml`,
`scripts/typecheck-sandbox-providers`.

**Current behavior**
Dependabot does not update the sandbox providers. A provider can stay on
an old or unsafe version, and nobody gets a pull request.

**Proposed behavior**
Dependabot updates the providers each week. Each pull request changes
the provider manifests and the lockfiles that CI installs frozen, so the
pull request can pass CI.

**Reason and benefit**
We stop having to find and fix old provider dependencies by hand.

## What Changed

- `.github/dependabot.yml`: add an npm entry for
`/scripts/typecheck-sandbox-providers`. Dependabot treats this workspace
as one unit. One pull request per dependency changes every provider that
uses the dependency and the shared `pnpm-lock.yaml` together. So the
entry needs no `groups`. One entry per provider would open a separate
pull request for each provider, and each one would fail the frozen
install in CI.
- `.github/dependabot.yml`: add an npm entry for
`/packages/plugins/sandbox-providers/daytona`. Daytona is the only
provider with its own lockfile, and the release build and the Daytona
end-to-end job install it frozen. Each lockfile has exactly one
Dependabot entry, so every pull request changes a manifest and its
lockfile together.
- Take Daytona out of the shared typecheck workspace. This pull request
does not commit `scripts/typecheck-sandbox-providers/pnpm-lock.yaml`:
since #19522, only the lockfile refresh workflow writes it. After merge,
the bot removes Daytona's packages from it (no other version changes;
checked with pnpm 9.15.4). Until then CI resolves it in place.
`scripts/typecheck-test-programs.mjs` now installs Daytona from its own
lockfile (`LOCKED_PROVIDERS`, with `--ignore-workspace`). A stale
lockfile is resolved in place for the run, for both installs.
- `.github/dependabot.yml`: add a comment that tells why the Pi and Kimi
runtime distributions have no entry (see Risks).
- `scripts/typecheck-sandbox-providers/pnpm-workspace.yaml`: list each
provider by its full path. Dependabot does not expand a workspace glob
that starts with `../`, so with the glob it saw no providers.
- `scripts/typecheck-test-programs.test.mjs`: add tests. They check that
the workspace list has no glob and matches `SANDBOX_PROVIDERS` without
`LOCKED_PROVIDERS`, that each locked provider has its lockfile, that a
locked provider installs frozen with `--ignore-workspace`, and that a
stale locked-provider lockfile is resolved in place.
- Add `.npmrc` (`auto-install-peers=true`) in
`scripts/typecheck-sandbox-providers` and in the Daytona provider.
Without it, Dependabot reads the root `.npmrc` (`false`). That rewrites
the lockfile settings, and the frozen install then fails. npm and pnpm
never publish `.npmrc`.
- `scripts/typecheck-sandbox-providers/package.json`: add
`"packageManager": "pnpm@9.15.4"`, the version CI uses. Without it,
Dependabot guesses pnpm 10.
- `doc/DEVELOPING.md`: say that contributors do not refresh or commit
Daytona's lockfile either, and that a new provider goes in both lists.

No provider moves into the root workspace. There is no pnpm catalog. The
root `pnpm-lock.yaml` does not change.

## Verification

I ran the real Dependabot updater on this branch with the Dependabot CLI
(`dependabot update ... --local`, updater image
`dependabot-updater-npm@sha256:c5a9451c…`):

- Before the change, the workspace entry loaded only
`cloudflare/bridge-template` (the one path without a glob). It skipped
all the other providers.
- After the change, `--dep typescript` gave one pull request. It changed
the 9 non-Daytona provider `package.json` files and the shared
`pnpm-lock.yaml`, and did not touch Daytona.
- `--dep wrangler` changed only the wrangler closure in the shared
lockfile. Before the `.npmrc` fix, the same run changed
`autoInstallPeers` to `false` and 757 lockfile lines.
- The Daytona entry (`--dep @daytonaio/sdk`) changed Daytona's
`package.json` and its own lockfile in one pull request, and kept
`autoInstallPeers: true`. It keeps the exact pin exact: `0.203.0`
becomes `0.227.3`.
- Both frozen installs (shared workspace, and Daytona with
`--ignore-workspace`) pass with pnpm 9.15.4.
- Dependabot installed `pnpm@9.15.4` from the new `packageManager`
field.

Other checks:

- `.github/dependabot.yml` passes the SchemaStore `dependabot-2.0`
schema (Ajv). A wrong key fails the same check.
- With pnpm 9.15.4, `install --resolution-only --ignore-scripts
--no-frozen-lockfile` in `scripts/typecheck-sandbox-providers` (the
lockfile bot's command) gives exactly the lockfile that passed the
frozen installs earlier in this pull request.
- The lockfile rule from #19522 allows this pull request's diff: it
changes no lock content.
- `node --test scripts/typecheck-test-programs.test.mjs`: the new test
and the provider tests pass. Two other tests need a root `node_modules`
(`tsc`), which my worktree did not have. CI runs them.

These checks cannot run before merge. GitHub reads `dependabot.yml` only
from the default branch. After merge, open Insights → Dependency graph →
Dependabot and make sure that the two new entries show no errors and run
on Monday.

## Risks

- A dependency in both a non-Daytona provider and Daytona (for example
`typescript`) gets two pull requests: one from each entry. Each one
passes alone, and they can merge in any order.
- The typecheck job now typechecks Daytona's tests against Daytona's own
lockfile instead of the shared one. Its versions are a little older (for
example vitest 4.1.11), so a type difference can show up there.
- Majors of shared dev tools (for example TypeScript 7, Vitest 5) come
as one pull request across all providers. It can need code changes
before it passes. That is the same as the root entry.
- Pi and Kimi get no version updates. Every direct dependency there is a
pinned provider release, and the pins feed the provider's ACPX
`commandDigest`. The Runner checks compare the Pi lock with
`PI_DISTRIBUTION_PINS`, and the Kimi build compares the lock SHA-256
with a pin. So any Dependabot change there fails CI (see #18998), and a
real upgrade starts saved sessions fresh. GitHub security alerts still
cover both lockfiles.
- The Pi zod `3.25.76` pin is still affected by GHSA-mrhx-c7xc-m3w5
(open alert). It is deliberate: `pi-acp@0.0.33` has `zod ^3.25.0` as a
direct dependency, not a peer. A fix needs a `pi-acp` release that
allows zod 4. This pull request does not change it.
- `packages/plugins/examples/plugin-orchestration-smoke-example` is also
outside the root workspace. It uses `workspace:*` dependencies, so it
cannot install alone, and this pull request does not cover it.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code with tool use (shell, file edits, local Docker runs of the
Dependabot CLI).

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 11:08:39 -08:00
Dotta 65aee998a3 fix(server): settle empty environment cleanup notifications (#19316)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Environment workers retain cleanup duties and retry them safely.
> - A metadata change can notify the worker without adding cleanup debt.
> - The pending-cleanup worker kept a work hold during its thirty-second
spacing window even when its owner reported no debt.
> - Historical cleanup already checks that condition without advancing a
provider sweep.
> - This PR applies the same owner check to pending cleanup and
preserves later notifications.
> - An empty notification can settle while real cleanup and uncertain
work still block sleep.

## Linked Issues or Issue Description

Refs #18646. That change covers empty historical-cleanup notifications.
This follow-up covers the pending-cleanup queue. Related runtime work:
#18988.

A search of open and closed PRs and issues found no duplicate
pending-queue fix. This repairs an existing worker under the roadmap's
Cloud deployment work.

**What happened?**

A normal metadata update on a released lease left one tracked work
intent and a scheduled pass. The actual cleanup deadline was null before
and after the update. The worker kept the hold until its spacing window
ended.

**Expected behavior**

The worker can clear an empty notification through its existing owner
predicate after a normal sweep and deadline read complete. It must
retain new debt, unresolved transactions, failed sweeps, failed reads
and later notifications. Provider sweep spacing and cleanup retry
deadlines must stay unchanged.

**Steps to reproduce**

1. Create and release a fixture cleanup lease through the normal
services.
2. Start the registered worker and confirm that its cleanup deadline is
null.
3. Change the released lease's metadata before the thirty-second spacing
window ends.
4. Run the new test. The original worker keeps an active intent even
though eligibility remains empty.

**Paperclip version or commit**

Candidate: `69aaee95cd73322a37e0270354d4a06bf5512ba7`. It normally
merges actual master `cd3832d2357eab481a861331ba1bc944a0b951f4`. The
focused retry repair follows the original two-file fix. Every other tree
blob still matches that master. The diff has two files.

**Deployment mode**

Source checkout with external PostgreSQL 17 and controlled coordinator
time. No deployed host or database suspension result is claimed.

## What Changed

- Apply the existing cheap deadline check to both registered cleanup
queues during provider spacing.
- Clear only the captured notification generation when the owner returns
no deadline and the latest normal sweep completed successfully.
- Invalidate that coverage before every full sweep. Startup and later
sweep failures keep their existing retry even if a metadata notification
has no database deadline.
- Add real PostgreSQL controls for empty metadata changes, concurrent
debt, unresolved transactions, failed reads through drain/resume, and
unread-spool recovery after startup and later sweep failures.
- Retain the original provider sweep, retry, deferred-buffer, creation,
startup, historical and shutdown controls.

## Verification

**Real PostgreSQL red/green qualification passed.** The original
empty-notification counterfactual failed at active work one, expected
zero, after actual null eligibility reads (16.706s). Review then found a
missing failed-sweep combination. Two additional cases using the real
malformed spool and ordinary metadata notifications reproduced lost
retries on the earlier published head, both at the intended pending
predicate (42.144s). One case fails at startup; the other first
completes a successful sweep, then fails a later sweep. Failed fixtures
and original logs remain retained.

At measured source `1ccd227c51e03ab4306d7cb85cf81efb0e3d3678`, all **29
owner cases passed in 113.061s**, without filters or skips. The tests
repair the actual spool, recover the ordinary cleanup lease, perform
provider cleanup once, preserve the later-sweep backoff, and return to
quiet. Normal migrations used a fresh owned external database; each pool
was capped at four with one worker. The first test-fixture attempt
accidentally replaced the real spool driver; that setup failure is
preserved separately and was corrected before the counterfactual. No
final cleanup result or receipt was seeded.

Run the full owner suite against a fresh, owned external test database
with normal migrations applied:

```sh
pnpm --filter @paperclipai/server exec vitest run src/__tests__/environment-cleanup-work.test.ts --maxWorkers=1 --no-file-parallelism
```

Set `PAPERCLIP_ENVIRONMENT_CLEANUP_TEST_DATABASE_URL` to that fixture.
Local qualification uses one worker and a pool of four.

Current native TypeScript checks passed: normal production declaration
layers (44.127s) and the affected test entry using the repository's
actual CI options and references (3.922s). Own dependency builds passed.
A frozen install first found stale upstream ACPX patch metadata;
normally carrying #19294 corrected the hash references for the same
package version. No lock policy was relaxed.

Full local workspace typecheck, tests and build were not run in this
bounded external-PG lease. Fresh checks on the current head
`69aaee95cd73322a37e0270354d4a06bf5512ba7` completed with **53
successes, one neutral Pony review and two skips**, with no pending or
failed gates. Its [PR
workflow](https://github.com/paperclipai/paperclip/actions/runs/42104787089),
aggregate verification and commitperclip review succeeded. The complete
latest edited Greptile and Pony summaries both gave **5/5**. Both review
threads are resolved, and all root and nested thread pages were
inspected.

The current head only removes four changed code-comment lines to follow
CONTRIBUTING.md. Its emitted JavaScript is byte-identical to the
measured parent `1ccd227c51e03ab4306d7cb85cf81efb0e3d3678`; tests,
configuration and dependencies are unchanged. The local 29-case evidence
therefore carries to this head without a duplicate local run. The remote
results above are fresh for the current head.
On the earlier head, Runner CI failed an omitted-context
checkpoint-settlement case. The fresh Runner job on this head passed
without a rerun. The original failure logs are preserved and its cause
remains unproven; current success is recovery evidence, not a causal
explanation.

The recorded test and migration command PIDs were reaped; an independent
audit verifies zero owned database clients before normal cleanup. Failed
fixtures remain retained. The process audit covers recorded command
PIDs, not an exhaustive history of child processes.

The long quiet assertion uses controlled coordinator time and actual SQL
counters. It does not prove a real twenty-minute quiet window, provider
behavior or deployed suspension.

## Risks

The empty read must not erase a notification that arrives while it runs.
The captured generation and transaction controls preserve that boundary.
An unknown or failed read keeps work pending. Empty deadline rows cannot
cover a failed sweep or an unread local spool; only a successful normal
sweep and deadline read establish that coverage. No cleanup row,
resource claim, provider action, authorization rule, sweep spacing or
retry deadline is removed.

## Model Used

OpenAI `gpt-6.1-sol` produced the delegated regression tests and source
review. OpenAI Codex, based on GPT-6, implemented the small worker
change and managed integration and verification. Both used repository
tools and code execution. Exact served backend 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 (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either linked an existing public issue or described the
issue using the issue template
- [x] I have not referenced internal or instance-local Paperclip issues
or links
- [x] My branch name describes the change and contains no internal issue
ID
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have checked that no documentation change is needed for this
internal worker fix
- [x] I have considered and documented risks
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open findings
- [x] I will address all reviewer comments before requesting merge
2026-11-06 13:04:40 -06:00
DottaandPaperclip f513f0efd9 fix(recovery): admit certified process-loss preparation retries (#19552)
## Thinking Path

> - Paperclip keeps task work durable across server restarts.
> - Restart recovery records a certificate when a Runner preparation
ended before provider work started.
> - Automatic recovery already verifies that certificate together with
saved files and stopped leases.
> - Explicit Retry called a preparation helper that only reached this
proof for status `interrupted`.
> - A certified `failed/process_lost` source therefore stayed blocked
after its files were recovered.
> - This pull request makes the helper reuse the same existing proof.
> - A queued user Retry can then admit one new turn while all current
safety and policy gates remain effective.

## Linked Issues or Issue Description

**What happened?**

A task had a certified process-lost preparation, verified
saved-workspace receipts, a destroyed source sandbox, and one or two
later attempts refused before dispatch. Its accepted Retry request
stayed deferred with `execution_recovery` and created no successor.

The explicit admission helper excluded this source before it could use
the preparation proof added in #19464. This also prevented the
refused-attempt continuation path from #19466 from advancing.

**Expected behavior**

Explicit Retry must use the same positive pre-provider proof as restart
recovery. It must preserve source history, refuse contradictory
execution evidence, honor current policy gates, and admit at most one
successor for the existing request.

**Steps to reproduce**

Persist a legacy preparing run as failed with `process_lost` and the
host bootstrap certificate that provider work did not start. Record its
canonical committed snapshot, recovery/finalization operations, clean
workspace authority, and exact destroyed-lease receipt. Add one or two
unstarted cancelled descendants with the dispatcher's execution-wait
receipt. Accept one Retry request and resume it concurrently twice.
Before this fix, no successor exists and the same request remains
deferred.

**Paperclip version or commit**

Reproduced on master `a6c22806308e8a3c7b21145f80fb471756bb443d`. Related
repairs: #19464 and #19466.

## What Changed

- Reuse `isInterruptedNativePreparation` before the existing
failed-preparation checks. The existing no-coordinator and
historical-ancestor restrictions remain.
- Add real-database regressions for one and two refused descendants,
duplicate clicks, concurrent resume, continuation dispatch, preserved
history, and unchanged recovery attempt counts.
- Add late-gate checks for provider work, provider events, a newer
native checkpoint, missing bootstrap proof, missing stop proof, a
changed snapshot, formal approval, user Stop, agent/company pause, and a
spent budget.
- Clarify that explicit admission uses the existing certified
preparation proof.

## Verification

- Red: both real-database cases passed the saved-file,
interrupted-preparation, and refused-dispatch proof checks, but the
original successor-count assertion found zero queued runs instead of
one. The accepted requests retained `execution_recovery`.
- Green: the same cases admit one successor after two concurrent resume
calls. Continuation dispatch resolves to the latest refused attempt.
Source and cancelled-attempt history stay intact.
- Complete relevant local suites: **456/456 passed** before rebase.
Command from `server`: `../node_modules/.bin/vitest run --config
vitest.config.ts --fsModuleCache src/services/task-retry.test.ts
src/services/explicit-native-continuation.test.ts`.
- Final head `9788382c019df57899af06e81211e7c5165e7f0d`: the thirteen
new cases passed **13/13** again after rebase. Required CI and review
remain pending.
- Environment: macOS ARM64, Node 26, Vitest 5.0.3, disposable real
PostgreSQL. Tests exercise production request, admission, and
continuation services. No provider executes and no hosted task is
changed.
- Hosted recovery still requires rollout and verification through the
original queued requests.

Final reviewed head: `9788382c019df57899af06e81211e7c5165e7f0d`.
[Required
CI](https://github.com/paperclipai/paperclip/actions/runs/42273663624)
passed (56 successful checks, four expected skips, none pending or
failed). Greptile and Paperclip Review both rated this head 5/5 with no
findings; all review threads are resolved.

## Risks

The change accepts only the existing verified preparation contract. An
error label alone is insufficient. Provider-started or contradictory
state remains blocked. Saved-file, lease, approval, Stop, pause, and
budget checks remain effective. No schema, retry limit, provider
setting, account selection, or production scheduler changes.

## Model Used

OpenAI GPT-6 family via Codex, with coding and tool use. The exact model
ID and context window are unavailable in this session's metadata.

## Checklist

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

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 13:03:21 -06:00
DottaandClaude Opus 5.5 71313207eb fix(runner): continue after a restore-lost turn the checkpoint never bound (#19539)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native Codex and ACPX runs survive control-plane restarts: the
runner restores the provider conversation, and the control plane
continues the task from its checkpoint
> - When the runner restores a conversation after process loss, it
reports the turn that was active as lost on restore
(`provider_turn_lost_on_restore`)
> - A controller can die after the provider accepted a turn but before
that turn reached the checkpoint. Then the next recovery does not know
the turn that the runner reports as lost
> - In a restart chaos wave with two hard kills about 20 s apart, this
made the second recovery fail with "Provider turn terminal did not name
the active turn". The session close then became unrecoverable
> - This pull request lets recovery continue: the transport no longer
stalls a new turn start behind that report, and the Codex session
records the report as restored history
> - The benefit is that a task continues automatically after
back-to-back restarts, instead of failing and needing an operator

## Linked Issues or Issue Description

**What happened?**
A Codex task was resumed after a hard restart. A second hard restart
came about 20 s later. The second recovery failed at once with
`NativeProviderTerminalFailure: Provider turn terminal did not name the
active turn.`, and automatic recovery stopped. The session close could
not settle, which quarantined later work (that blast radius is fixed
separately in #19532).

**Steps to reproduce**
1. Start a native Codex task. Kill the control plane (SIGKILL) shortly
after the provider accepts a turn and before the next checkpoint is
saved.
2. Let restart recovery resume the run. Kill the control plane again
while the resumed run is active.
3. Observe the second recovery.

Durable evidence from the wave (runner journal of the run):
- Restart 1 (17:32:34.568): the provider accepted continuation turn T2.
The controller was killed 170 ms later, before T2 reached the
checkpoint.
- Recovery 1 started a new turn T3. The runner command result named T3
(seq 15). The restored runner then reported T2 as lost on restore (seq
20) before `turn.started` for T3 (seq 23). The transport held the T2
terminal while the T3 start was pending, and stopped pumping at it. So
T3 ran in the provider (items, a tool call) but the control plane never
accepted or checkpointed it.
- Restart 2: recovery found no active turn. The restored runner reported
T3 as lost on restore. The Codex session had never bound T3, so it
failed the protocol (`turn_binding_mismatch`).

**Expected behavior**
Recovery continues the task after each restart (R1).

**Actual behavior**
The run failed with a protocol error. Its close could not prove
settlement, so cleanup needed an operator.

## What Changed

- `runnerd-codex-transport.ts`: while a turn start is pending, the event
pump holds a terminal only when the terminal can belong to that turn.
When the durable command result names the new provider turn and the
terminal names a different turn, the pump delivers it and continues to
the new `turn.started`. A terminal that names no turn, or any terminal
before the command result, is still held.
- `codex-session-notifications.ts`: a `turn/completed` with status
`failed` and a recoverable `provider_turn_lost_on_restore` error, for a
turn that this session never bound (not active and not terminal), is
recorded as restored history with a `harness.diagnostic`
(`unbound_restored_turn_lost`). The session keeps its own turn. The
runner reports this cause only after it proved that the old provider
process ended and restored the conversation, so that turn can produce no
further work or result.
- Other terminals for an unbound turn (a completion, an ordinary
failure) still fail with `turn_binding_mismatch`.

## Verification

- Red before the fix, real `paperclip-runnerd` and fake Codex app
server: `pnpm --filter @paperclipai/paperclip-runner exec vitest run
src/live/runnerd-restart-continuation.test.ts -t "never bound"` failed
with `NativeProviderTerminalFailure: Provider turn terminal did not name
the active turn.` at `codex-harness-session.ts:207` via
`native-session-runtime.ts` (the same stack as the wave).
- Green after the fix: that test completes the run on `provider-turn-2`
with no `session.failed` and no `turn.failed`.
- New driver tests in `codex-app-server-driver.terminal-replay.test.ts`:
the restore-lost unbound turn is recorded (also on replay) and the
session's own turn completes. A completion or ordinary failure for an
unbound turn still fails the protocol.
- New transport unit test for the hold rule (`does not hold a pending
turn start behind another turn's restore-lost terminal`).
- Existing suites pass: `src/drivers/codex/`,
`src/native-session-runtime.test.ts`,
`src/live/runnerd-restart-continuation.test.ts`,
`src/live/runnerd-controller-outage.test.ts`,
`src/live/runnerd-codex-transport-warm-attachment.test.ts`,
`src/live/runnerd-codex-transport.test.ts` (turn-start cases). Three
tests timed out once at host load 51 and passed when rerun alone.
- `pnpm --filter @paperclipai/paperclip-runner typecheck:typescript`
passes.
- Live replay of the wave task: results are added in a comment below.

## Risks

- The transport change applies only when the command result already
named the new turn and the terminal names a different turn. A terminal
for the new turn keeps the existing fence that puts `turn.accepted`
first.
- The session change accepts only the runner's restart-interruption
cause. A lost turn's work is already in the restored conversation. The
continuation turn tells the agent to reconcile unfinished work.
- No profile, schema or pack contract changes. ACPX sessions that use
the same Codex harness session get the same behavior.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, 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 <noreply@anthropic.com>
2026-11-06 13:02:25 -06:00
DottaandClaude Opus 5.5 c6eecac1e0 fix(heartbeat): keep a shutdown-suspended native recovery for the next boot (#19530)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The Runner Full-Stack E2E workflow tests the Runner in Daytona
sandboxes from a signed, content-addressed image
> - Every change to an audited Runner input needs a new image before a
Daytona cell can test it
> - The image job took 10 to 16.5 minutes today, and a restart chaos
campaign waits on it after each runner-side fix
> - Measurement shows two serial waits: a full Rust dependency compile
(about 5 of 6.6 minutes) and a 3.5 to 4 minute download of cached base
layers that starts only after the builder stages finish
> - There is also no way to build only the image: every dispatch reaches
a paid gate that the operator must reject
> - This pull request removes both serial waits and adds an `image_only`
dispatch input
> - The benefit is a shorter fix-to-Daytona-test loop, with every image
security check unchanged

## Linked Issues or Issue Description

**What happened?**

The `Publish verified Daytona image` job is on the critical path of the
control-plane restart chaos campaign. Builds today took 16.5 min (run
42240434011), 15 min (42253920727), 10.5 min (42259118276) and 16 min
(42265637650). Run 42264611573 failed: its RunsOn runner lost
communication with GitHub 7.6 minutes into the build step, during the
Rust compile. GitHub has no log for that job.

**Expected behavior**

A Runner source change should not recompile the locked third-party Rust
graph. Cached image layers should download while the builder stages
compile. An operator should be able to publish only the image, without a
paid gate.

**Steps to reproduce**

Read the BuildKit timeline in the logs of job 127970125495 (run
42240434011). See the stage table below.

## What Changed

- `docker/daytona-runner/Dockerfile`: compile the locked Rust dependency
graph in its own layer against placeholder targets. The layer depends
only on `Cargo.toml`, `Cargo.lock` and the crate manifest. The step
deletes the placeholder outputs and fingerprints, so the next step
always compiles the real Runner sources. The final stage still receives
only the stripped executable.
- `docker/daytona-runner/Dockerfile`: move the content-ID /
source-revision check above the two `COPY --from` steps. It does not
depend on either builder stage, so BuildKit now downloads the cached
base and CLI layers in parallel with the compile. Both copies use `COPY
--link`.
- `.github/workflows/runner-full-stack-e2e.yml`: add the boolean
`image_only` dispatch input. `authorize`, `target_lock`, `catalog` and
`daytona_image` run unchanged. Every other campaign job gets `&&
inputs.image_only != true`. `authorize` rejects `image_only` together
with TestOps inputs.
- `tests/runner-e2e/workflow-image-only.test.ts` (new): pin the job
classification, the top-level guard on each campaign job, the guard on
every protected environment, and the TestOps rejection.
- `tests/runner-e2e/README.md`: document the layer layout and the
`image_only` command.

## Verification

Measured on the same RunsOn `m7i-flex.xlarge` (4 vCPU, 16 GiB, native
x64) runner class. "Before" is master job 127970125495 (run
42240434011). "After (cold)" is this branch, job 128077687214 (run
42271236140), with no dependency layer in the registry cache yet. Its
paid gate was rejected.

| Stage | Before | After (cold dependency cache) |
|---|---|---|
| Whole image job | 16.5 min | **11.6 min** |
| Rust dependency graph | 311 s (inside one 399 s step) | 395 s (own
layer; reused from cache after a master build) |
| `paperclip-runner-core` compile | ~87 s | 65 s |
| Cached base and CLI layer download | 236 s, **after** cargo finished |
257 s, **in parallel** with cargo |
| Copy runnerd + provider pack | 20 s + 50 s | 9 s (`--link`) |
| Provider pack stage | 143 s (parallel) | 152 s (parallel) |
| Export + push | 82 s + 10 s | 81 s + 11 s |
| Dependency layer size | n/a | `target` 852 MB + registry 333 MB
(builder stage only) |

With a warm dependency cache, a Runner source change compiles only the
65 s crate step. The critical path then becomes the ~4 min cached-layer
download or the provider pack. The expected job time is about 7 to 8
min. I measure this after merge with an `image_only` dispatch (master
first, to write the cache, then a Rust-only branch change).

Disk: the instance starts with 38.76 GiB free. The dependency layer adds
about 1.2 GB uncompressed to builder storage. The final image does not
change.


- `vitest run tests/runner-e2e/workflow-image-only.test.ts
tests/runner-e2e/workflow-security.test.ts
tests/runner-e2e/daytona-image.test.ts`: the new test and all image
tests pass. Three tests in `workflow-security.test.ts` (`gates every
provider-secret job with stable actor IDs`, `keeps provider credentials
inside explicitly gated paid workflows`, `isolates private archive trust
from workflows that execute target-branch code`) fail on `origin/master`
without this change, because of `chat-catalog-testops-case.yml`. This
pull request does not change them.
- `node --test tests/runner-e2e/product-regression-testops.test.mjs`:
15/15 pass.
- No local Docker build. The Dockerfile is measured with a
branch-targeted CI dispatch whose paid gate was rejected.

## Risks

- The dependency layer keeps the Rust `target` and registry trees in a
builder-stage layer, and default-branch builds export it to the registry
cache. This adds builder disk and cache upload when `Cargo.lock`
changes. The build prints the size (`du -sk target
/usr/local/cargo/registry`).
- The dependency cache helps only after a default-branch build writes
it. Branch builds read it but never write it. This is the existing cache
policy.
- `image_only` changes the workflow, so it works only after merge to
master.
- No check is removed. Lockfile verification, cosign sign and verify,
the anonymous pull check, the provider-pack manifest match, the
default-branch-only cache write and the actor allowlist are unchanged.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, through Claude Code
with tool use (shell, GitHub CLI). Extended reasoning on.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 13:01:16 -06:00
DottaandClaude Opus 5.5 7afb57c022 test(runner-e2e): repair restart-cell harness drift (#19538)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The Runner Product E2E suite proves that agent work survives a
control-plane restart
> - A restart chaos campaign found three restart cells that fail in the
test harness, not in the product
> - A harness failure hides the real restart result, so the campaign
cannot classify these cells
> - This pull request repairs the harness drift in those cells and names
the one failure the harness cannot prevent
> - The benefit is that these cells now test restart behavior, or fail
with a clear cause

## Linked Issues or Issue Description

Refs #14811 (connector setup became one screen). Refs #19475
(completion-update restart fixture).

**What happened?**
1. `lifecycle-baseline.runner-codex.local.tool-review-restart` clicked a
"Save and continue" button. #14811 removed that button. The cell failed
during setup. After that fix, the cell also failed after the restart:
the expanded review card does not survive the post-restart reload, so
"Approve & run" was not visible.
2.
`local-session-integrity.runner-acpx-claude.local.structured-question-restart-resume`
failed with `page.goto: net::ERR_ABORTED` after the controller restart.
The old document's Vite client reloads when the replacement server
starts, and this aborts the test navigation. The code tolerated only
`TimeoutError`.
3.
`completion-updates.runner-acpx-claude.local.handoff-completion-restart`
waited for the source agent to run an instructed wait command. Claude
replied without the command (2/2 attempts), so the restart never
occurred. The cell then failed with a generic timeout.

**Expected behavior**
Each cell reaches its restart boundary and tests restart behavior. If
the boundary cannot be reached, the cell names the cause.

**Steps to reproduce**
Run `pnpm test:e2e:runner -- --max-parallel 1 --max-automatic-retries 0
--id <cell>` for each cell above on `master`.

## What Changed

- `tests/runner-e2e/connection-reviews.ts`: accept the one-screen "Check
link" step and the older "Save and continue" step. This uses the same
regex approach as `connection-flow.ts`.
- `tests/runner-e2e/runner.spec.ts` (tool review): click "Review
request" after the restart reload, on the document that acts on the
card.
- `tests/runner-e2e/chat-restart.ts`: add `gotoAfterServerRestart`. It
retries only `net::ERR_ABORTED`, within the caller's deadline and at
most 5 attempts. It still accepts `TimeoutError`. It rethrows all other
errors. The structured-question restart uses it. The existing sentinel
and durable-state assertions still prove the new document.
- `tests/runner-e2e/completion-updates.ts` and
`completion-update-flow.ts`: add `completionRestartGateSkipped` and
`completionRestartGateTimeoutDetail`. The gate poll stops at once with
`restart boundary not reached (agent skipped gate)` when no completion
turn is active and the turn that carries this task's Done facts
(`chatCompletionUpdates`, as in `completionDelivery`) succeeded or
published its reply before the gate reports `waiting`. A failed or
cancelled turn is not blamed on the agent, because delivery can retry
it; the timeout detail names it instead. The post-restart chat
navigation now uses `gotoAfterServerRestart`.
- A fixture-level gate is not possible without a product hook. The
native run finalizer publishes the completion reply inside its own
transaction, through the issue service closure. The isolated-server
entrypoint can patch only class prototypes and HTTP responses, and the
reply uses no tool call or HTTP request.
- `tests/runner-e2e/README.md`: document the skipped-gate failure.
- Docs: use `pnpm --filter @paperclipai/paperclip-runner run
build:runner-binaries` in `tests/runner-e2e/README.md`,
`tests/restart-chaos/README.md`, `tests/restart-chaos/main.ts` and
`doc/DEVELOPING.md`.
- No product code changes.

## Verification

- Unit: `pnpm exec vitest run --config tests/runner-e2e/vitest.config.ts
tests/runner-e2e/chat-restart.test.ts
tests/runner-e2e/completion-updates.test.ts
tests/runner-e2e/connection-reviews.test.ts` (36 passed).
- Typecheck: `node scripts/typecheck-test-projects.mjs tests/runner-e2e`
passed.
- Live cells, local, `--max-parallel 1 --max-automatic-retries 0`,
macOS, load average 35–65:
- `lifecycle-baseline.runner-codex.local.tool-review-restart`: PASS
(first run found the review-card reload drift, fixed in the second
commit).
-
`local-session-integrity.runner-acpx-claude.local.structured-question-restart-resume`:
PASS. A first attempt failed before the restart with `PRP command
session.open timed out` at load ~36 (environment).
-
`completion-updates.runner-acpx-claude.local.handoff-completion-restart`:
FAIL as designed, twice (before and after the review fix), with `restart
boundary not reached (agent skipped gate): completion turn … ended
succeeded without the reference gate reporting waiting`. Claude skipped
the wait both times, so this cell still does not exercise a restart.

## Risks

- Low risk. Test harness and docs only.
- The navigation retry could hide a real abort. It is limited to
`net::ERR_ABORTED`, 5 attempts and the deadline, and the test still
asserts that a new document loaded.
- The ACPX Claude completion restart cell still does not exercise a
restart when the agent skips the gate. It now reports this cause by
name.

## Model Used

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 12:59:54 -06:00
Devin FoleyandPaperclip fd4b197f83 fix(ui): hide runner selection during agent setup (#19517)
## Thinking Path

> - Paperclip lets people manage AI agents for work.
> - Agent setup connects a harness, model, account, and computer.
> - The system already selects a qualified runner for a new agent.
> - Setup still asks users to select that internal runner.
> - The runner dropdown can be the only field in Advanced.
> - This PR removes the dropdown and hides an empty Advanced section.
> - Users keep their saved execution settings and see fewer setup controls.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

Agent creation, onboarding, built-in agent setup, company import, and agent configuration.

**Subsystem affected**

The UI, setup error messages, tests, and Storybook fixtures.

**Current behavior**

Users can select a runner from a dropdown. Some setup screens place only that dropdown in Advanced. Errors can direct users to that control.

**Proposed behavior**

Remove runner dropdowns from setup and configuration. Use the existing automatic selection for new agents. Retain saved runner settings. Show Advanced only when other fields remain.

**Reason and benefit**

Users can configure agents without an extra internal execution choice. Setup also avoids an empty Advanced section.

**Breaking changes**

Users can no longer select a runner through these UI dropdowns. The API and CLI retain their runner options. Existing agents keep their saved execution settings.

**Additional context**

Related: [Default new agents to the qualified runner for their harness](https://github.com/paperclipai/paperclip/pull/15422). This PR changes the existing setup UI. It adds no harness or runtime default. The master merge retains the custom gateway placement from [#19510](https://github.com/paperclipai/paperclip/pull/19510).

## What Changed

- Remove the shared runner dropdown and its setup/configuration callers.
- Hide Advanced when it has no remaining fields. Keep command and argument settings where supported. Preserve custom gateway setup under Advanced on the Claude and OpenAI API key paths.
- Keep setup failures visible and keep the retry action outside the removed control.
- Update errors and documentation that referred to the removed dropdown.
- Update component tests and Storybook fixtures for the new UI states.
- Make Legacy first-task evals resume an API-configured setup record. Record that path separately from fresh automatic onboarding.

## Verification

- Latest master integration (`187c0f6d6e5`): 276 onboarding/provider-connection tests passed across six files. UI typecheck and build passed. Token gates, query-error rendering checks, and the Storybook catalog check passed. The conflict resolution retains master's bounded connection tests and cancellation while removing the obsolete runner instruction. The first test command used the wrong working directory and failed before collecting tests; the corrected invocation from `ui/` passed.
- Earlier branch verification passed: full workspace build and typecheck, 1,037 focused UI/service tests, 49 heartbeat provider contract tests, eval typecheck, and 30 map/catalog tests.
- The full local `pnpm test:run` run passed all six chat integration shards (1,931 tests). It was stopped for the master merge before the complete suite finished. The [complete CI run](https://github.com/paperclipai/paperclip/actions/runs/42264662642) passed on the previous head `72e1699`.
- Current head `a097c8369d079c839e78661a2206cfa62c5210e0`: [full CI](https://github.com/paperclipai/paperclip/actions/runs/42273168268) passed. PR checks total 55 passed and 3 skipped, with none failed or pending. Greptile reviewed this head at 5/5 with no actionable findings or unresolved review threads.
- Live browser/provider evals have not run. The Legacy eval change has no live qualification claim.

## Risks

- Removing the dropdown changes the setup UI. Existing API and CLI runner choices remain available.
- Conditional Advanced sections must retain valid command and argument fields.
- Legacy first-task evals now prove setup resumption. They do not prove the fresh-agent automatic default.
- No schema change or data migration is included.

## Model Used

OpenAI `gpt-6.1-sol`, through Codex in T3 Code. Reasoning effort: `xhigh`. The model used file editing, shell commands, and test tools. The harness does not expose the context window size.

## Checklist

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

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-11-06 10:58:42 -08:00
Devin FoleyandPaperclip f0006c7783 fix(workspaces): contain inventory deadline aborts (#19543)
A capture deadline can expire while file identity validation awaits the filesystem. Recheck cancellation before creating the inventory read stream so the expired capture rejects without terminating the server.

All 224 affected capture and archive tests pass, including real Node subprocess cancellation regressions. Final-head CI passes 54 checks, and Greptile scores 5/5 with no unresolved findings.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-11-06 10:45:47 -08:00
DottaandClaude Opus 5.5 187c0f6d6e fix(runner-e2e): prepare the artifact oracle for every suite that runs it (#19518)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The Runner Full-Stack E2E workflow runs paid Daytona restart cells
for the control-plane restart chaos campaign
> - Manual run 42243751291 failed
`daytona-restart-continuity.runner-codex.daytona.recover-controller`
before task creation: `docker version` timed out after 10 seconds in the
artifact oracle preflight
> - The everyday flow requires the Python artifact oracle by case alone
(`requiresEverydayArtifactOracle(caseId)`). The workflow step that
starts Docker and pulls the oracle image also filters by suite, so
suites that reuse `recover-controller` skip it
> - On a cold RunsOn host the first `docker version` takes about 20
seconds (the everyday cell in the same run shows this), so the 10-second
preflight fails when the workflow step did not run first
> - This pull request gates the oracle step by case only, which matches
the runtime requirement
> - The benefit is that the Daytona restart cell reaches task creation
instead of failing in setup

## Linked Issues or Issue Description

**What happened?**

Run https://github.com/paperclipai/paperclip/actions/runs/42243751291
job `daytona-restart-continuity.runner-codex.daytona.recover-controller`
failed with `Artifact sandbox qualification failed before task creation:
... docker version ... timed out after 10 seconds`. The job log shows no
"Prepare pinned Python artifact oracle image" step.

**Expected behavior**

Every cell whose flow runs the artifact oracle prepares Docker and the
pinned oracle image before paid execution.

**Steps to reproduce**

Enumerate `runnerMatrix` cells where
`requiresEverydayArtifactOracle(task.id)` is true. The cells in
`daytona-restart-continuity`, `rolling-web-deployment`,
`rolling-web-deployment-warm` and `remote-workspace-storage` do not
match the old workflow condition.

## What Changed

- `.github/workflows/runner-full-stack-e2e.yml`: the "Prepare pinned
Python artifact oracle image" step is now gated by `matrix.caseId` only.
The suite filter is removed.
- `tests/runner-e2e/codex-ci-sandbox.test.ts`: the oracle gate test now
checks every catalog cell, not only `everyday-workflows` cells, and
rejects a suite filter.
- `tests/runner-e2e/workflow-security.test.ts`: the pinned condition
string follows the new gate.

## Verification

- Red: with only the test change, `npx vitest run --config
tests/runner-e2e/vitest.config.ts
tests/runner-e2e/codex-ci-sandbox.test.ts` fails on `prepares the
artifact oracle for exactly the stories that execute downloads`.
- Green: with the workflow change, the same command passes (13/13).
- `tests/runner-e2e/workflow-security.test.ts` locally shows the same 5
failures before and after this change (`pr-trusted.yml` step counts and
`chat-catalog-testops-case.yml` credential checks). They do not involve
this step.
- Live proof: a narrow manual dispatch after merge.

## Risks

- Low risk. Cells in four more suites now run a Docker version check, a
pinned image pull and an inspect before paid execution. This adds about
20 to 30 seconds per cell. The image is pinned by digest. The step has
no secrets.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 12:40:12 -06:00
DottaandClaude Opus 5.5 d4d3383c97 fix(runner): scope cleanup quarantine to the session's task and conversation (#19532)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs (Codex, ACPX) execute through the runner's
`executeNativeSession`, which gates each new session behind cleanup of
earlier sessions
> - When a session close fails in a way that needs operator recovery
(`NativeSessionCloseUnrecoverableError`), the runner keeps a quarantine
entry for it
> - The admission gate keyed that quarantine by `[companyId, backend
kind, backend name]`, so one bad close blocked every later run of the
same backend in the whole company
> - In a restart chaos wave (two hard kills about 20 s apart), one Codex
session's close became unrecoverable. After that, every Codex run and
every Retry in the company failed in about 1 s with
`native_session_cleanup_quarantined`
> - This pull request scopes the gate to the resources that a retained
owner can still hold: its task (issue) and its provider conversation
(normalized session)
> - The benefit is that one failed close no longer stops unrelated
tasks, while the task and conversation that are really at risk stay
protected

## Linked Issues or Issue Description

**What happened?**
After two hard control-plane restarts about 20 s apart, one Codex
session close became unrecoverable. From then on, every Codex execution
in the company failed at admission with
`native_session_cleanup_quarantined: prior session cleanup requires
operator recovery…` (`retryQuarantinedSessionCleanups`). Board Retries
for four unrelated tasks were accepted and then failed at once.

**Steps to reproduce**
1. Run two native Codex tasks in one company (local runner).
2. Make one session's close throw `NativeSessionCloseUnrecoverableError`
(in the wave: a protocol failure after a second hard kill, then an
unsettled runner suspension).
3. Start or retry a different Codex task in the same company.

**Expected behavior**
Only work that shares a resource with the retained owner waits. Other
tasks run normally.

**Actual behavior**
Every later Codex run in the company fails within about 1 s, until the
server process restarts.

## What Changed

- `packages/paperclip-runner/src/native-session-runtime.ts`: the cleanup
domain is now `{ key, resources }`. `key` is the old backend key
(company, backend kind and name, optional remote cleanup scope).
`resources` are the session's issue and its normalized session (or its
run when it has no normalized session).
- The admission gate (`retryQuarantinedSessionCleanups`) and its
retained close owners block a new session only when the backend key
matches and at least one resource matches.
- The exact retirement paths (`completeRetainedNativeSessionCleanup`,
`completeTerminatedRemoteNativeSessionCleanup`,
`completeTerminatedLocalNativeSessionCleanup`) read the unchanged
backend key, so their behavior does not change.
- New regression test: an unrecoverable close on session A does not
block session B of another task. A session with the same issue, or with
the same normalized session, stays blocked.

Why this scope is safe: the resources that a retained owner can still
hold are its runner state root and its workspace. The server derives the
runner state root from the session scope (company, agent, workspace,
provider, normalized session). The workspace belongs to the task.
Different tasks already run concurrently on the same backend, so a
different task shares no resource with the retained owner that normal
concurrency does not already share.

## Verification

- Red before the fix: `pnpm --filter @paperclipai/paperclip-runner exec
vitest run src/native-session-runtime.test.ts -t "unrelated tasks of the
same backend"` failed with `native_session_cleanup_quarantined` for the
unrelated task (the RC-20 error).
- Green after the fix: the same test passes. The full file passes
(211/211).
- `pnpm --filter @paperclipai/paperclip-runner typecheck` passes.

## Risks

- A retained owner can no longer block a different task that shares a
project workspace. Concurrent tasks in a shared workspace are already
allowed, so this adds no new kind of overlap.
- The same task and the same conversation stay blocked. A Retry of the
task whose close failed can still be accepted and then fail at
admission. The trigger of that close is handled separately.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, 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 <noreply@anthropic.com>
2026-11-06 12:39:31 -06:00
DottaandClaude Opus 5.5 07ddf9a5c2 fix(native): keep accepted results when runner close cannot prove suspension (#19525)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native runs persist an accepted result first. The Runner then closes
and suspends its session.
> - Under heavy host load, the Runner close can fail to prove a durable
suspension after the result is accepted.
> - The executor then kept its finalization lease. The heartbeat
finalizer got `native_finalization_lease_busy`, and the run ended
`failed/adapter_failed`.
> - The provider work was complete. Only Runner bookkeeping failed, so
the run must not fail.
> - This pull request hands the exact lease to finalization and
completes the run from its accepted result.
> - The benefit is that an accepted result stays accepted, independent
of host load.

## Linked Issues or Issue Description

**What happened?**

A native ACPX Claude run asked a structured question. The control plane
accepted its result (`Waiting for…`) and recorded `run.terminal` with
`runTerminalState=succeeded`. The Runner close then took about 8.6 s.
Its `runner.suspend` command ended `failed`, but the durable Runner
state was `suspended` with a matching identity. The close threw
`NativeSessionCloseUnrecoverableError`. The executor emitted
`native.post_result_failure` (`category: runner_settlement_incomplete`,
`resultAlreadyAccepted: true`) and rethrew `native_session_lease_lost`.
Its finalization lease stayed held (its TTL is 20 minutes).
`finalizeNativeRun` got `native_finalization_lease_busy`, and the run
ended `failed/adapter_failed`. No restart was in progress. Host load
average was about 40.

**Expected behavior**

After the control plane accepts a result, finalization bookkeeping must
not fail the run. The run settles from its accepted result. Repair of
the Runner checkpoint stays with the existing next-run admission rules.

**Steps to reproduce**

Run `pnpm exec vitest run
src/services/heartbeat/runtime-execution.test.ts -t "accepted local
result"` in `server/`. The test uses the real executor, the real
control-plane result commit, and embedded PostgreSQL. The provider
boundary accepts a result and then throws
`NativeSessionCloseUnrecoverableError`. On the base revision, the
finalization row keeps the executor lease (`phase:
workspace_finalizing`) and the finalizer reports `lease_busy`.

**Paperclip version or commit**

Base `ff1d1dea5d`. The incident was on `819420a60a`.

## What Changed

- `native-session-executor.ts`: after the rejected failure write proves
an accepted result under the exact, unexpired lease, the executor
releases that lease to finalization. It uses the same exact-authority
predicates as the normal release. Before, the lease stayed held until
its TTL expired.
- `runtime-execution.ts`: for `runner_settlement_incomplete` on a local
workspace (no workspace sync and no remote workspace authority),
Paperclip records `workspace_finalize` as succeeded, finalizes the
accepted result, and returns it to the ordinary completion path.
- New `native-accepted-result-settlement.ts`: rebuilds the adapter
result from the durable accepted result row, its terminal, the run's
native session, and the accepted usage checkpoint. The result carries
the closed `nativePostResultFailure` diagnostic. It contains no provider
text.
- Other post-result categories (for example `io_not_found`) keep their
workspace-failure handling. They now also reach finalization at once,
instead of after the lease TTL.
- `doc/execution-semantics.md`: describes the settlement.

## Verification

- Red, then green: `settles an accepted local result when runner close
cannot prove suspension` and `keeps workspace failure handling for other
accepted-result failures on a local workspace` in
`server/src/services/heartbeat/runtime-execution.test.ts`. Both failed
on the base revision with the executor lease still held. Both pass with
this change.
- Safety case `settles an accepted local result ...
(workspace-check-failed)`: when the `workspace_finalize` check fails,
the run keeps the existing failure path. Case `terminal-unreadable`:
when the accepted result cannot be rebuilt, only a failed
`workspace_finalize` record is written.
- `pnpm exec vitest run
src/services/heartbeat/runtime-execution.test.ts`: all pass (some cases
time out at 30 s only under host load above 40; they pass on a narrow
rerun). This includes the 14 existing post-result diagnostic and
authority cases (expired, replaced owner, generation, attempt, boot,
pid, process start, stopped, lock busy, and others).
- `pnpm --filter @paperclipai/server typecheck`: pass.
- Live cell
`local-session-integrity.runner-acpx-claude.local.structured-question-restart-resume`
(real ACPX Claude, local runner): 3 runs at host load average about 40.
Runs 1 and 2 passed. Run 3 hit the fault naturally: the first run
emitted `native.post_result_failure` (`runner_settlement_incomplete`)
and now ended `run succeeded` (before this change it ended
`failed/adapter_failed`). The cell then failed later, in the resume run
after the answer: `runner_state_identity_mismatch:
prior_owner_terminal_state_indeterminate;
retirement_refused=target_not_sandbox_runner`. That is a separate
follow-up (see Risks).

## Risks

- A changed lease handoff. The executor releases the lease only after
the rejected failure write proves an accepted result under the exact
owner, attempt, controller generation, controller identity, and an
unexpired lease. A stale or replaced owner does not release anything. A
failed release is bounded and best effort. The old behavior then
applies.
- The run now completes as `succeeded` while the local Runner checkpoint
is not proven suspended. The next run on the same session then meets the
existing fence: the continuity-break retirement
(`retireFinishedPerTurnRunnerdOwner`) accepts only sandbox Runners, so a
local Runner is refused with `target_not_sandbox_runner` and its state
root is quarantined. This PR does not change that fence. Extending the
retirement to local Runners needs its own proof that the local Runner
process stopped, and is a follow-up.
- Settlement happens only when the accepted result and its terminal are
rebuilt first, and the `workspace_finalize` record then succeeds. A
failed branch or plugin workspace check keeps the failure path.
- Synced and remote workspaces are not changed. They keep their
copy-back retry path.

## Model Used

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

## Checklist

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

Related: #19126 added the closed post-result diagnostic that this change
uses.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 12:36:34 -06:00
Nicky LeachandClaude Opus 5.5 1206373a95 fix(environments): close Apex findings on managed plugin environments (#19526)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A managed instance gets its instance-level environments from the
`PAPERCLIP_MANAGED_CONFIG` document, and #19508 added plugin environment
entries to it
> - Greptile's Apex review of #19508 arrived after it merged, with three
findings
> - A credential inside an array nested in an array passed the secret
check and the response redaction
> - Recovery of an archived environment listened on one worker handle,
which a restart replaces, and its attempts were not tracked through idle
checks and shutdown
> - This pull request closes all three findings
> - The benefit is that credentials never pass the check, an environment
comes back after any worker restart, and shutdown never cuts off a
recovery write

## Linked Issues or Issue Description

Refs #19508 (the Apex findings this pull request fixes)

**Subsystem affected**
Server environments: the managed-config parser, the environment routes'
response floor, the managed environment bootstrap, and the plugin worker
manager.

**Problem or motivation**
Greptile's Apex review of #19508 found three problems. (1)
`findSecretLikeConfigKey` and `redactSecretLikeConfigKeys` skip array
elements that are arrays, so `{"groups":[[{"apiToken":"..."}]]}` passes
the boot check and is echoed to restricted readers. (2) Recovery
subscribes to `ready` on one worker handle. A restart such as a settings
save replaces that handle, so the new worker's `ready` never reaches the
listener, and the environment stays archived. (3) A recovery attempt is
fire-and-forget. The worker manager's work token ends when the worker
replies, but the database write continues, so an idle check or the
database shutdown can overlap it.

**Proposed solution**
Walk arrays at any depth in both functions. Add a manager-wide
`onWorkerReady` subscription and use it for recovery. Run every recovery
attempt through a controller that tracks it as idle work and that
shutdown stops and awaits before the database closes.

**Alternatives considered**
Re-subscribing on each new handle after a restart. That needs a hook in
every restart path. The manager-wide subscription covers all of them in
one place.

**Roadmap alignment**
This only fixes code that #19508 added or touched. It adds no product
surface.

## What Changed

- `server/src/services/managed-config.ts`: `findSecretLikeConfigKey`
walks arrays at any depth (new `findSecretLikeConfigKeyInValue`). This
covers sandbox `config` and plugin `driverConfig`.
- `server/src/routes/environments.ts`: `redactSecretLikeConfigKeys`
walks arrays at any depth (new `redactSecretLikeConfigValue`).
- `server/src/services/plugin-worker-manager.ts`: new optional
`onWorkerReady(listener)` on `PluginWorkerManager`. It fires on every
worker `ready`, including a replacement handle from a restart. A
listener error is logged and does not affect the worker.
- `server/src/services/managed-environments.ts`:
- Sandbox and plugin recovery subscribe through `subscribeWorkerReady`.
It prefers `onWorkerReady` and falls back to the handle when the manager
has no such method.
- New `createManagedEnvironmentRecovery()` controller. Each attempt runs
inside `trackIdleWork` for its whole duration. `stop()` removes every
`ready` subscription, refuses new attempts, and awaits unfinished ones.
- `server/src/index.ts`: creates the controller, passes it to
`applyManagedEnvironments`, and awaits `stop()` during shutdown, after
the delivery workers stop and before the application teardown and
database close.

## Verification

- `cd server && npx vitest run src/__tests__/managed-config.test.ts`: a
secret-like key in an array nested in an array refuses boot, for plugin
`driverConfig` and sandbox `config`.
- `npx vitest run src/__tests__/environment-routes.test.ts`: a
nested-array credential is removed from the response, and other nested
values stay.
- `npx vitest run
src/services/managed-environments-plugin-entries.test.ts`:
- With `onWorkerReady`, recovery never consults the handle. It ignores
another plugin's `ready`, re-ensures on this plugin's `ready`, and then
unsubscribes.
- An attempt counts in `idleWorkSnapshot()` until its ensure settles.
`stop()` unsubscribes, waits for the unfinished attempt, and later
`ready` events start nothing.
- `npx vitest run src/services/managed-environments.test.ts
src/__tests__/managed-plugin-environments.test.ts`: pass.
- `pnpm --filter @paperclipai/server typecheck` passes.
- I did not run the full `pnpm test:run`, `pnpm -r typecheck` or `pnpm
build`. CI covers them.

## Risks

- The redaction now also walks nested arrays in sandbox config
responses. Only credential-shaped keys are removed; other values are
unchanged.
- The boot check now refuses a document that hides a secret-like key in
a nested array. A valid document has no such key.
- `onWorkerReady` is optional, so other `PluginWorkerManager`
implementations and test fakes keep working.
- Shutdown now waits for an unfinished recovery attempt. An attempt is
one worker call plus one short transaction.

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

## Model Used

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 10:35:57 -08:00
Nicky LeachandClaude Opus 5.5 3b3037aab5 fix(ci): run the sandbox provider tests in required CI (#19533)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Sandbox provider plugins (Daytona, E2B, Cloudflare, Kubernetes,
Modal, and others) run agents in remote sandboxes
> - The providers are outside the root pnpm workspace, so `pnpm
test:run` and the CI test lanes never run their Vitest suites
> - paperclipai/paperclip#18839 typechecks those tests, but a test that
fails at runtime still fails nothing
> - The providers have about 1,040 tests, and they finish in about 15
seconds on Linux
> - This pull request adds a required `Sandbox provider tests` job that
runs them when a change can affect them
> - The benefit is that a broken provider, plugin SDK, or shared
contract fails CI on the pull request instead of in a release or a paid
end-to-end run

## Linked Issues or Issue Description

Refs #18839

**What happened?**

No CI job runs the sandbox provider tests. `runner-full-stack-e2e.yml`
builds the Daytona plugin for a paid nightly run, but it does not run
any provider's `pnpm test`.

**Expected behavior**

A pull request that can break a provider runs that provider's tests, and
a failure blocks `ci / verify`.

**Steps to reproduce**

Add `expect(1).toBe(2)` to a test in
`packages/plugins/sandbox-providers/e2b/src`. Open a pull request. CI
stays green.

## What Changed

- Added `scripts/test-sandbox-providers.mjs` and its tests. The script
installs the providers through `typecheckTestPrograms` with no programs,
because that function already owns the provider install and the plugin
SDK link. Then it runs each provider's `pnpm test`, four at a time, and
prints each provider's output as one block. It fails if a suite fails or
if no provider has a test script.
- The script skips `cloudflare/bridge-template`, because Cloudflare's
Vitest config already runs those tests.
- Added the `sandbox_provider_tests` job to
`.github/workflows/pr-trusted.yml`, and made `ci / verify` require its
result.
- Added the `//#ci:sandbox-providers` task to `turbo.json` and its lane
to `.github/ci-task-matrix.json`. The CI planner runs the job only when
a change touches `packages/plugins/sandbox-providers/**`,
`packages/plugins/sdk/**`, or `packages/shared/**`. `packages/shared` is
the plugin SDK's only workspace dependency. Global CI inputs, such as
`scripts/**`, `.github/**`, `pnpm-lock.yaml`, or any `package.json`,
still select every job.
- The job turns off core dumps. A Daytona capacity test kills a process
at a file-size limit inside the exported directory. If core dumps are
on, the core file joins the export, and the test sees 12 files instead
of 11.
- Updated the job lists in `scripts/__tests__/e2e-shard.test.mjs` and
the job count in `.github/scripts/tests/ci-profile.test.mjs`. Added
planner tests to `.github/scripts/tests/ci-plan.test.mjs`. One of these
tests fails if the plugin SDK gets a new workspace dependency that is
not in the job's inputs.
- Added the new test file to `test:release-registry`, and documented the
script in `doc/DEVELOPING.md`.

## Verification

- `node scripts/test-sandbox-providers.mjs` on macOS: 8 of 9 suites pass
in 31.6 s, including the install. Kubernetes fails 12 `file-sync` tests,
because macOS has no `/proc/self/fd`. Some Daytona tests skip, because
they need GNU `tar` and coreutils.
- The same script in a Linux container (`node:24`, a non-root user, core
dumps off): all 9 suites pass, with 1,012 tests passed and 26 skipped.
The script took 15 s after the install.
  - Kubernetes: 205 passed.
  - Daytona: 590 passed and 16 skipped.
- With core dumps on, Daytona's `preserve literal capacity repair` test
fails. The export includes a `core` file. With `--ulimit core=0`, it
passes.
- To look for timing failures under load, I ran the whole script 12 more
times in the container, limited to 2 CPUs (8 runs) and 1 CPU (4 runs).
All 12 runs passed. They took 45 to 76 s on 2 CPUs and 73 to 102 s on 1
CPU.
- `ubuntu-latest` has `zstd`, and my container did not, so some Daytona
tests skipped in those runs. With `zstd` installed, Daytona passed with
598 tests and 8 skipped, and only the live tests skipped. One earlier
`zstd` run failed while the stress runs used the same CPUs, and I did
not capture its output. The next run passed.
- `node --test` on every test that reads `pr-trusted.yml`, the CI
planner, or the task matrix: 816 passed. These include
`.github/scripts/tests/*.test.mjs`,
`scripts/__tests__/e2e-shard.test.mjs`,
`scripts/ci/test-selection-workflow.test.mjs`, and the new tests.
- `pnpm run test:release-registry` on macOS: 251 of 255 pass, including
the new tests. The 4 failures are `wait_for_npm_package_versions` tests
in `scripts/release-lib.test.mjs`. They fail the same way on master on
the same machine, and CI runs this suite on Linux.
- `actionlint` 1.7.7 reports no errors on the changed workflow.
- I called `planTasks` from `.github/scripts/ci-plan.mjs` with one
changed file at a time. `server/src/index.ts` and `ui/src/App.tsx` leave
`sandbox_provider_tests` off.
`packages/plugins/sandbox-providers/e2b/src/plugin.ts` and
`packages/shared/src/index.ts` turn it on.

This pull request cannot run its own new job. `pr.yml` calls
`pr-trusted.yml@master`, so the job first runs on pull requests after
this one merges. After merge, check the first pull request that changes
a provider or `packages/shared`.

## Risks

- **First real run is after merge.** I measured the time on Linux in
Docker, not on a CI runner. CI runners are about 2 times slower than my
machine, so I expect about 2 to 3 minutes for the job, including the
dependency restore. The job runs in parallel with the other jobs, and
the slowest required job takes about 13 minutes. So the job should not
make CI slower.
- **Open provider pull requests.** After this merges, open pull requests
that change a provider run its tests. If those tests fail, `ci / verify`
blocks them until they are fixed.
- **`packages/shared` changes often.** Any change there runs this job. A
Linux-only provider failure would block those pull requests. All suites
pass on Linux today.
- **One unexplained Daytona failure.** One Daytona run with `zstd`
failed under CPU contention, and I did not capture its output. Some
Daytona tests have short deadlines. If the job is flaky after merge,
quarantine the failing test or revert the job, and do not leave `ci /
verify` red.
- **Path rules can be too narrow.** If a provider starts to import
another workspace package, a change to that package does not run this
job. The plugin SDK dependency test covers the SDK. It does not cover a
provider that imports a workspace package directly. Today no provider
does.
- **Overlap with paperclipai/paperclip#19489 and
paperclipai/paperclip#19522.** Both pull requests change the provider
install in `scripts/typecheck-test-programs.mjs`. This pull request does
not edit that file. It calls the same install function, so it uses their
install changes when they merge. All three pull requests edit the same
section of `doc/DEVELOPING.md`, so a small text conflict is possible.

> 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`), in Claude Code with
tool use (shell, file edits, GitHub CLI, Docker). Extended reasoning was
on.

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 10:34:03 -08:00
DottaandClaude Opus 5.5 874fed09a4 test(restart-chaos): enable isolated workspaces for repo tasks (#19536)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness (`tests/restart-chaos`) restarts the
control plane while agents work, then grades the result
> - Rule R2 says files an agent wrote before a restart must still be
present afterwards
> - Repo workloads failed R2 with `pre_fault_files_lost`. The first run
worked in the shared project checkout, not in its own worktree
> - The seed sets a `git_worktree` project policy, but the server
applies that policy only when the instance `enableIsolatedWorkspaces`
setting is on. The seed did not turn it on
> - This pull request makes the seed turn on the setting (as users do on
the Experimental page, and as `tests/runner-e2e` does) and fail if the
server does not confirm it
> - The benefit is that repo tasks get their own worktree, so one task's
files cannot appear in another task's R2 snapshot

## Linked Issues or Issue Description

Refs #19477

**What happened?**
In a restart chaos wave, two repo tasks (B1 and B5) ran at the same
time. Both ran in the project primary checkout (`strategyType:
project_primary`). Task B1's `NOTES-*.md` file and its `cli.py` edit
appeared in the B5 task's workspace before any fault. After a Retry, the
B5 task moved to a fresh attempt directory, and R2 reported the B1 file
as lost from B5.

**Expected behavior**
Each repo task gets its own git worktree, as the harness project policy
asks. The R2 snapshot of a task contains only that task's files.

**Steps to reproduce**
1. Run `tests/restart-chaos/main.ts --env local --adapters codex
--workloads B1,B5 --concurrency 2`.
2. Read `currentExecutionWorkspace` for each task. It shows
`project_primary` and the fixture checkout as `cwd`.
3. Read `GET /api/instance/settings/experimental`.
`enableIsolatedWorkspaces` is `false`.

**Root cause**
`server/src/services/heartbeat.ts` reads `enableIsolatedWorkspaces` and
`gateProjectExecutionWorkspacePolicy`
(`server/src/services/execution-workspace-policy.ts`) returns `null`
when the setting is off. The project policy is then ignored, and the
task resolves to the shared primary checkout.
`tests/restart-chaos/seed.ts` created the project policy but did not
turn on the setting.

## What Changed

- `tests/restart-chaos/seed.ts`: before it creates the project, the seed
sends `PATCH /api/instance/settings/experimental` with
`enableIsolatedWorkspaces: true`. If the response does not confirm the
setting, the seed stops with an error. It records a
`seed_isolated_workspaces` timeline entry.
- `tests/restart-chaos/seed.test.ts`: new unit tests. One test checks
that the seed turns on the setting before it creates the `git_worktree`
project. One test checks that the seed refuses to continue when the
server does not confirm the setting.
- `tests/restart-chaos/README.md`: the `seed.ts` row says that the seed
turns on the setting, and why.

## Verification

- Red before the fix: `pnpm exec vitest run --config
tests/restart-chaos/vitest.config.ts seed.test.ts` failed 2/2 (`expected
-1 to be greater than or equal to 0`).
- Green after the fix: the full restart-chaos unit suite passes, 5 files
and 42 tests.
- Live wave on this branch (`16f5c8c5d7`, local runner, native Codex,
macOS, load average about 40): `tests/restart-chaos/main.ts --env local
--adapters codex --workloads B1 --tasks 3 --concurrency 1 --faults K1
--seed 21` (wave `w-20261106T181436-s21`). The timeline shows
`seed_isolated_workspaces`. Result: 3/3 pass. Each task crossed a
SIGKILL restart. R1, R2, R4, R5, R6 and the workload oracle passed for
each task, with no lost files.
- Each task's pre-fault file list now has a `.git` file (a git worktree,
not the primary checkout) and only its own `NOTES-*.md` file. In the
failing wave, the B5 task's list had the B1 task's `NOTES-*.md` file.
- Not exercised: no task needed a Retry in this wave, so the
Retry-to-fresh-attempt path did not run.
- Ledger preflight: install and runner builds passed. The provider-free
restart suite had 2 failures in
`runnerd-codex-transport-warm-attachment.test.ts` at load average 44; a
rerun of that file passed 18/18.

## Risks

- Low risk. The change is in the test harness only. It changes no
product code.
- The harness instance is temporary, so the experimental setting does
not reach a real instance.
- Repo tasks now run in git worktrees. Earlier waves ran in the shared
checkout, so R2 results for repo workloads from earlier waves are not
comparable with new waves.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with
tool use (shell, file edits). Standard context window; 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
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 12:33:13 -06:00
Devin FoleyandPaperclip e3d8f42e19 fix(ui): recover stalled onboarding connection checks (#19505)
Bound the complete onboarding connection check to two minutes, abort its
requests on timeout, and ignore late results. Keep the saved account
available for retry and resume its matching unfinished setup after refresh.

Verified 242 focused tests, workspace typecheck and build, and the complete
hosted CI check set. Greptile is 5/5 with no unresolved review comments.
Local full-UI timing failure and interrupted CI runner attempts remain
documented in the PR evidence.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-11-06 10:31:18 -08:00
DottaandClaude Opus 5.5 beaa9a622d fix(native): re-bootstrap a runner killed before session.open (#19524)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs start a local `paperclip-runnerd` process. The control
plane keeps a durable state root for each session.
> - When the control plane restarts, restart recovery classifies each
live native run. A run whose runner died before any provider identity
existed gets the `bootstrap_incomplete` disposition. Recovery then
quarantines the old state root and starts the bootstrap again.
> - The executor accepts that disposition only when the durable state
root proves that no provider work started. The old proof required
`connectionCount === 0` and every command `pending`.
> - A restart chaos wave killed the server about 2 s after runnerd
started. The runner had connected and completed `run.prepare`, but
`session.open` was still pending. No event was committed, so no provider
session or turn existed.
> - The proof rejected that root. The run failed with
`runner_state_identity_mismatch: bootstrap_not_proven_incomplete`, and
the task stopped with `native_continuation_requires_reconciliation`.
Nothing continued automatically.
> - This pull request widens the proof to the exact window where
provider work cannot have started, and keeps every other shape
fail-closed.
> - The benefit is that a restart during native runner bootstrap
continues automatically for every native provider.

## Linked Issues or Issue Description

**What happened?**
A control plane restart that lands after the native runner connected and
acknowledged `run.prepare`, but before `session.open` completed, fails
the run with `adapter_failed` and `runner_state_identity_mismatch:
bootstrap_not_proven_incomplete`. Startup recovery chose
`bootstrap_incomplete` (controller generations 2 and 3), but the
executor proof in `migrateRunnerdStateRootForExecution` refused it. The
durable control-plane state had `connectionCount: 1`, `run.prepare`
completed, `session.open` pending, no committed events, and
`ackedSourceSeq: 0`. The runner journal had `nextSourceSeq: 1`, an empty
outbox, and the provider state was `prepared` with no provider session
ID.

**Steps to reproduce**
1. Start a native task (seen with OpenCode on a local runner; the window
applies to every native provider).
2. Kill the control plane about 2 s after runnerd starts, after
`run.prepare` completes and before `session.open` completes.
3. Let the control plane restart.

**Expected behavior**
Recovery quarantines the state root and starts the bootstrap again. The
task continues without a user action.

**Actual behavior**
The run fails and the task is blocked with
`native_continuation_requires_reconciliation`.

## What Changed

- `runnerdStateIsIncompleteBootstrap` (native-session-executor.ts) now
also accepts a connected root when all of these are true: no committed
event, `ackedSourceSeq === 0`, only `run.prepare` and `session.open`
commands, `session.open` not completed, `run.prepare` pending or
completed, no warm transition or staged attachment, and the bootstrap
was not adopted.
- When `runner/runner-state.json` exists, the runner journal must agree:
empty outbox, `nextSourceSeq === 1`, `ackedSourceSeq === 0`, and no
completed `session.open` or other processed command. An unreadable
runner journal stays fail-closed. This covers events that the runner
emitted but the controller never committed.
- The remote "claim an untouched session" path keeps the old
never-connected proof (`runnerdStateIsUnconnectedBootstrap`), so its
behavior does not change.
- Tests: a red test with the production state shape (connected,
`run.prepare` completed, `session.open` pending), plus fail-closed
counter-tests for a completed `session.open`, committed or acknowledged
events, a turn command, a failed `run.prepare`, an adopted bootstrap, a
warm transition, an uncommitted runner event, a runner-completed
`session.open`, a runner-processed turn, and a torn runner journal.

## Verification

- Red before the fix: `pnpm exec vitest run
src/services/native-runtime/native-session-executor.test.ts -t "native
incomplete-bootstrap evidence"` failed on "allows a fresh bootstrap
after connect and run.prepare when session.open never completed"
(`expected false to be true`). Green after the fix: 13/13.
- The proof run against the kept durable state root from the failing
wave (read only) returns `true` after the fix.
- Related suites: `native-session-executor.test.ts` and
`native-restart-recovery.test.ts` filtered to
`bootstrap|restart|remote|unstarted|untouched`: 301 passed.
- `tsc --noEmit -p server/tsconfig.json` passes.
- Live restart chaos proof: see the comment on this PR.

## Risks

- The proof now accepts a root where the runner received `session.open`
but did not complete it. The runner can have started a provider process
or created an empty provider session in that window. Recovery only
proceeds when restart classification already proved that the runner
process and its group are dead and no known provider process is alive.
No provider turn can exist, because the controller never issued a turn
command. The worst case is an orphaned empty provider session, not
repeated work.
- Every state with committed or acknowledged events, a completed
`session.open`, a turn, a warm transition, an adopted bootstrap, or an
unreadable runner journal still fails closed.
- Out of scope: when the proof fails legitimately, the task offers the
existing execution-blocker Retry. In the failing wave the issue
interruption also showed a budget wait (`userAction: budget`). That is
the RC-06 accounting issue and belongs to a separate fix.

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

## Model Used

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

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 12:30:50 -06:00
DottaandClaude Opus 5.5 a6c2280630 fix(ui): dock the announcement card in the sidebar so it never covers the task thread (#19511)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A new user lands on their first task after onboarding. The task
thread and the composer are the core of that screen.
> - The app shows an optional in-app announcement (now "Introducing
Connectors"). On desktop the card floats at the bottom-left, 22rem wide,
but the sidebar is only 240px wide.
> - In the Nov 5 cloud onboarding test, the tester said: "onboarding
popup blocks core real estate".
> - At 1280x800 the card covers the left part of the composer. At
1440x900 and 1280x800 it covers the task thread area and the sidebar
account menu.
> - This pull request docks the card inside the desktop sidebar column,
in normal layout flow, above the account menu.
> - The benefit is that the card stays visible and dismissible, but it
can never cover the thread or the composer.

## Linked Issues or Issue Description

**What happened?**
On a new company's first task, the "Introducing Connectors" announcement
card floats over the task thread. At 1280x800 it covers the left part of
the composer.

**Steps to reproduce**
1. Create a new company and open its first task at 1280x800.
2. Wait 3 seconds for the announcement card to show.
3. See the card on top of the composer and the account menu.

**Expected behavior**
Optional announcements do not cover the task thread or the composer.

**Evidence**
The staging test company (Claude subscription path) has a local
dismissal for `2026-09-14-connectors`. The live feed at
`pages.paperclip.ing/announcements/v1/current.json` serves that
announcement. No other first-use overlay showed on the first task or the
dashboard. Toasts and the task-detail error notice are transient and are
not first-use popups.

## What Changed

- `AnnouncementWell` has a `placement` prop: `sidebar` (in-flow, no
`fixed`) or `floating` (the earlier fixed placement).
- `Layout` renders the `sidebar` placement inside the desktop
`SidebarShell`, above the account menu. It renders the `floating`
placement only on mobile, where it stays above the bottom nav.
- While the desktop sidebar is collapsed to a rail or closed, the card
waits. It shows again when the sidebar is expanded.
- In the sidebar, the card is capped at half the window height
(`--announcement-sidebar-max-height: 50dvh`) and scrolls. It uses the
existing compact image height token, so the sidebar navigation keeps
most of its height.
- New e2e spec `tests/e2e/announcement-placement.spec.ts`. It makes a
new company with a team lead and a first task. It serves the live
connectors announcement through the app's announcement API. At 1440x900
and 1280x800 it checks that the card does not overlap the composer or
`#main-content`, that the card is fully in the viewport and dismissible,
and that it does not overlap the account menu trigger and the Account
dialog opens.
- Unit test for the two placements in `AnnouncementWell.test.tsx`.

**Product decision for Dotta:** this PR does not hide announcements on
first use. Another option is to hold all announcements until the user
has used the product for some time (for example, after the first task is
done or after the first day). That is a one-line gate in
`AnnouncementWell`'s `enabled` condition, if we want it.

## Verification

Red on `master` (819420a60a), same spec, before the fix:

```
[1/1] [chromium] › tests/e2e/announcement-placement.spec.ts › Announcement placement on a new company's first task › the announcement does not cover the task thread or composer
    Error: 1440x900: announcement overlaps task thread
    Expected: 0
    Received: 42904
    Error: 1280x800: announcement overlaps composer
    Expected: 0
    Received: 8568
    Error: 1280x800: announcement overlaps task thread
    Expected: 0
    Received: 42904
  1 failed
```

The spec also checks the account menu (requested by the N-13
connectors-upsell fix). Red on `master` with the same spec:

```
    Error: 1440x900: announcement overlaps task thread
    Received: 42904
    Error: 1440x900: announcement overlaps account menu
    Received: 5312
    Error: locator.click: Test ended.   (the card intercepts the "Open account menu" click)
```

Green after the fix:

```
[1/1] [chromium] › tests/e2e/announcement-placement.spec.ts › Announcement placement on a new company's first task › the announcement does not cover the task thread or composer
  1 passed (45.0s)
```

Review follow-up (Greptile/Ponytail: a long announcement in a 208px
sidebar had no height cap). A second spec uses a 400-character
description, a 208px sidebar, and a 1280x720 window. Red without the
cap:

```
    Error: docked card keeps at most half the window height
    Received:    732
    Error: expect(locator).toBeInViewport() failed   (sidebar "Tasks" link)
    Received: viewport ratio 0
    Error: expect(locator).toBeInViewport() failed   (account menu)
    Received: viewport ratio 0
```

The spec sets the sidebar width key for both the classic and the
Coordinator workspace, and asserts the rendered width is 208px.

Green with the cap (both specs):

```
  2 passed (57.8s)
```

Neighbouring suites:

```
cd ui && npx vitest run src/components/AnnouncementWell.test.tsx src/components/AnnouncementCard src/hooks/useAnnouncement.test.tsx src/components/Layout.test.tsx
 Test Files  4 passed (4)
      Tests  57 passed (57)
```

- `pnpm check:token-gates`: All gates clean.
- `pnpm --filter @paperclipai/ui typecheck`: pass.
- Screenshots: the spec writes `announcement-1440x900.png` and
`announcement-1280x800.png` to its test output. Before: the card (352px
wide) sits over the composer at x 296–364 and over the account menu.
After: the card is 216px wide inside the sidebar, above the account
menu. The thread and composer are clear.

Run it:

```
npx playwright test --config tests/e2e/playwright.config.ts tests/e2e/announcement-placement.spec.ts
```

## Risks

- Low risk. The card now takes vertical space in the sidebar while it
shows. The sidebar navigation scrolls as before. The card goes away when
the user dismisses it.
- Users who keep the desktop sidebar collapsed do not see the
announcement until they expand the sidebar.
- Mobile behavior does not change.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, in Claude Code with
tool use (shell, browser, Playwright).

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
canary/v2026.1106.0-canary.18
2026-11-06 12:19:46 -06:00
Nicky LeachandClaude Opus 5.5 35280516c0 fix(ci): set up pnpm in the Vitest selection job like other trusted jobs (#19527)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Trusted CI workflows set up pnpm in a fixed way. Node comes first,
and npm audit, fund and update-notifier calls are off.
> - A policy test in the Runner support suite checks every pnpm setup in
`pr-trusted.yml` for this shape.
> - PR #18348 added a pnpm setup to the new Vitest selection job. That
setup did not have the npm overrides, and setup-node was not directly
before it.
> - PR CI does not run the Runner support suite. So the policy test
failed on master, and nobody saw it.
> - This pull request gives the selection job the same pnpm setup as the
other eight jobs.
> - The benefit is that every trusted pnpm setup follows one rule again,
and the policy test passes.

## Linked Issues or Issue Description

Refs #18348 (added the job). Refs #18625 (its description noted the
"nine bootstraps where eight are expected" failure). Related: #19229
also edits the pnpm steps in `pr-trusted.yml`.

**What happened?**

`pnpm test:e2e:runner:unit` fails on master in two tests: "keeps pnpm
bootstrap registry telemetry out of trusted workflow setup" and
"installs a modern Node runtime before every trusted pnpm bootstrap".
`pr-trusted.yml` has nine pnpm setups. The ninth, in `test_selection`,
has no `NPM_CONFIG_AUDIT`, `NPM_CONFIG_FUND` or
`NPM_CONFIG_UPDATE_NOTIFIER` override. The step directly before it is
the eligibility check, not setup-node.

**Expected behavior**

Every trusted pnpm setup has the three overrides, and setup-node is the
step directly before it.

**Steps to reproduce**

Run `pnpm test:e2e:runner:unit` on master. Read the two failures above.

## What Changed

- `pr-trusted.yml`, job `test_selection`: add "Setup Node.js for pnpm
bootstrap" directly before "Setup pnpm". Both steps use the existing
eligibility condition. The pnpm step now sets the three npm overrides.
Both actions keep their pinned SHAs, with version comments added.
- `tests/runner-e2e/workflow-security.test.ts`: the expected count of
pnpm setups is now set per workflow: 8 for `runner-full-stack-e2e.yml`
and 9 for `pr-trusted.yml`. The checks on each setup do not change.

## Verification

- On this branch, the two pnpm tests in `workflow-security.test.ts`
pass. Before this change, they failed with "expected 8, got 9".
- The other three failures in that file are fixed separately in the
companion PR that repairs the rest of the suite.
- `node --test scripts/ci/test-selection-workflow.test.mjs`: 4 passed.
- The workflow parses as YAML. The `test_selection` steps are now:
checkout, setup-node, eligibility check, setup-node for pnpm, setup
pnpm, restore dependencies, select, upload, publish.
- `pr.yml` calls `pr-trusted.yml@master`. So this PR's own CI runs the
master copy of this workflow. The change runs in CI only after merge.

## Risks

- Low risk. The new setup-node step runs again with the same Node 24
version. It adds about one second when selection is eligible. When
selection is not eligible, both steps are skipped, as before.
- `test_selection` has `continue-on-error: true`. If the new steps fail,
the original test matrix still runs.
- #19229 removes `version: 9.15.4` from every pnpm step in this file. If
both PRs merge, the second one needs a small rebase on this step.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), by Anthropic, in Claude Code with
tool use (shell, file edits, git, GitHub CLI).

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 10:14:44 -08:00
Devin FoleyandPaperclip b85557a122 Fix OpenAI subscription waiting labels (#19521)
Show “Waiting for auth…” while OpenAI subscription sign-in completes in onboarding and the shared provider connection screen. Preserve Claude’s code-entry label and align connection previews and existing regression expectations.

Verified 246 component tests, workspace typechecks, the full production build, token gates, and all 54 applicable CI checks. Greptile scored the change 5/5 with no unresolved comments.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-11-06 10:12:12 -08:00
github-actions[bot]andlockfile-bot 17b4f972bf chore(lockfile): refresh pnpm lockfiles (#19534)
## Thinking Path

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

## Linked Issues or Issue Description

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

Run a frozen install before and after this lock refresh.

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

Co-authored-by: lockfile-bot <lockfile-bot@users.noreply.github.com>
2026-11-06 18:09:43 +00:00
Nicky LeachandClaude Opus 5.5 e3041f5481 fix(ci): let the lockfile bot own the sandbox provider lockfiles (#19522)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The repository has a supply-chain rule: contributors do not commit
lockfiles. A bot and verified Dependabot commits write them.
> - The rule checks only the root `pnpm-lock.yaml`.
> - The sandbox providers are outside the root workspace, so they have
two more pnpm lockfiles. Pull requests edited them by hand, for example
#12791, #18839 and #19489.
> - This pull request makes the same rule and the same bot cover those
two lockfiles.
> - The benefit is one lockfile rule for every managed pnpm lockfile,
and no lockfile edits in contributor pull requests.

## Linked Issues or Issue Description

Refs #19489

**What existing behavior does this improve?**

The lockfile policy and the lockfile refresh bot.

**Subsystem affected**

CI: `.github/scripts/lockfile-trust.mjs`,
`.github/workflows/refresh-lockfile.yml`, and the sandbox provider
installs.

**Current behavior**

`lockfile-trust.mjs` blocks contributor edits to `pnpm-lock.yaml` only.
A contributor can edit
`scripts/typecheck-sandbox-providers/pnpm-lock.yaml` or
`packages/plugins/sandbox-providers/daytona/pnpm-lock.yaml`, and CI
passes. The refresh bot does not update these files.
`typecheck:test-programs` fails on a stale provider lockfile and tells
the contributor to commit a new one.

**Proposed behavior**

The policy blocks contributor edits to all three lockfiles. The refresh
bot regenerates all three after merge. CI resolves a stale provider
lockfile in place for the run, as it does for the root lockfile.

**Reason and benefit**

Lockfile content comes only from the bot or from verified Dependabot
commits. Contributors do not need to run pnpm in special folders.

**Breaking changes**

None. A pull request that edits a provider lockfile now fails the policy
check. Remove the lockfile change from the pull request. The bot commits
it after merge.

## What Changed

- `lockfile-trust.mjs` now exports `LOCKFILES`: the root lockfile, the
shared sandbox provider lockfile, and the Daytona lockfile. The policy
blocks contributor edits to each of them. A Dependabot pull request must
match a verified Dependabot commit for each lockfile that it changes.
The `merged` command and the reporting-lock proof still use only the
root lockfile.
- `check-pr-lockfile.mjs` (advisory check) uses the same list.
- `refresh-lockfile.yml`:
- A job-level `LOCKFILES` env lists the same three files. A test checks
that it equals the policy list.
- A new step regenerates the two provider lockfiles with
`--resolution-only --ignore-scripts --no-frozen-lockfile`.
- The unexpected-changes check, the change check, and `git add` cover
all three lockfiles.
  - The release-age gate checks the added versions in every lockfile.
- A held refresh saves all three lockfiles at their repository paths,
with a `source-locks.sha256` file. The next run reuses them only if
every master lockfile is unchanged. An artifact in the old format is not
reused.
  - The PR title is now `chore(lockfile): refresh pnpm lockfiles`.
- `check-lockfile-release-age.mjs` accepts more than one
`--base`/`--head` pair.
- `scripts/typecheck-test-programs.mjs`: if the frozen install of the
provider workspace fails, the script resolves the lockfile in place and
installs frozen again.
- `scripts/build-standalone-public-packages.mjs`: the same fallback
applies to a package with a committed lockfile (Daytona). `release.sh`
already restores tracked files when it exits.
- `doc/DEVELOPING.md` and `doc/RELEASE-AUTOMATION-SETUP.md` describe the
managed lockfiles. They also say that the Pi and Kimi distribution
lockfiles stay hand-managed, because they hold attested provider pins.

## Verification

- `node --test .github/scripts/tests/*.test.mjs
scripts/__tests__/build-standalone-concurrency.test.mjs`: 731 tests
pass. New tests cover:
- the policy for provider lockfiles, including a real git history with a
Dependabot commit and a later hand edit
  - the workflow and policy lists match
  - the unexpected-changes check, run with bash against a fixture
- a held refresh round trip, and its invalidation when a provider
lockfile changes on master
  - the release-age gate across lockfile pairs
  - the inline fallback in both install scripts
- `node --test scripts/typecheck-test-programs.test.mjs`: the install
tests pass. Two tests fail locally only because this worktree has no
root `node_modules` (no `tsc`). They also fail without this change.
- I ran the new bot commands on master with pnpm 9.15.4. No package
version changed. pnpm 9.15.4 removes `libc:` fields and adds
`deprecated:` notes, so the first refresh is a metadata-only change. The
release-age gate passes it because it adds no versions.
- I ran the release-age and held-lock steps locally with bash against
this commit. The gate reported `eligible=true`, and `sha256sum --check`
accepted the saved hashes.
- The workflow YAML parses, and `bash -n` accepts every `run` block.
- Not testable before merge: the policy job loads `lockfile-trust.mjs`
from the base branch, so the new rule applies only to pull requests
opened after this merges. The refresh workflow runs only on master.

## Risks

- The release build now resolves a stale Daytona lockfile in place, from
the registry. The root release install already resolves without a frozen
lockfile, so this is the same level of trust.
- The paid runner e2e job still installs Daytona with
`--frozen-lockfile`. After a Daytona manifest change merges, its Daytona
cases fail until the bot's refresh merges. This matches the rule that
paid jobs refuse a new root lockfile until its refresh merges.
- Dependabot pull requests for the provider entries in #19489 change a
provider lockfile. They pass only if each lockfile matches a verified
Dependabot commit, the same as for the root lockfile.
- Low risk otherwise: the bot keeps its existing token, environment, and
merge rules.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with tool use
(shell, file edits, and git).

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 10:07:57 -08:00
DottaandClaude Opus 5.5 1485d3de1b fix(ui): hold announcements until the onboarding first task is finished (#19514)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A new user's first session is the onboarding first task. The agent
asks its onboarding questions in that task thread.
> - The app also shows an optional in-app announcement. The live feed
now serves "Introducing Connectors" (`2026-09-14-connectors`).
> - The announcement well only waits while the onboarding wizard dialog
is open. It shows 3 seconds after the user lands on the first task.
> - In the Nov 5 cloud onboarding test (Claude API path), the tester
said: "Don't show the connectors upsell until the second session. It's
blocking the user flyout and potentially the onboarding questions."
> - This pull request holds announcements until the company's onboarding
first task is done or cancelled.
> - The benefit is that a new user sees the onboarding questions and the
account menu without an upsell on top of them.

## Linked Issues or Issue Description

Refs #19511 (places the card in the desktop sidebar, above the account
menu. That PR keeps the card off the account menu after onboarding. This
PR adds the first-session gate that #19511 leaves open.)

**What happened?**
On a new company's onboarding first task, the "Introducing Connectors"
card shows 3 seconds after the page loads. At 1440x900 the card (352x346
px, bottom-left, z-40) covers the sidebar account menu trigger and the
left edge of the task composer.

**Expected behavior**
Optional announcements do not show in the first session, while the
onboarding first task is open.

**Steps to reproduce**
1. Create a new company through onboarding. The wizard creates the
onboarding first task.
2. Open the first task at 1440x900 and wait 3 seconds.
3. See the announcement card on top of the account menu and the
composer.

**Paperclip version or commit**
`master` at 819420a60a.

## What Changed

- `AnnouncementWell` reads the company's onboarding first task
(`originKind=onboarding_first_task`, `limit=1`) with React Query under
`["issues", companyId, "onboarding-first-task"]`. Issue live-update
invalidation of `["issues", companyId]` refreshes it.
- The well stays disabled while that task is not `done` or `cancelled`.
While the read is loading or after it fails, the optional card stays
hidden. A company that never had an onboarding first task is not gated.
- "First session" uses the existing `origin_kind` marker. There is no
schema change.
- New e2e spec `tests/e2e/announcement-first-session.spec.ts` with a
mocked feed that copies the live connectors entry. It checks that the
card is absent while the onboarding first task is open. Then it marks
the task done on the same page and checks that the card appears without
a reload (a positive control for the feed mock and the live refresh
path).
- New unit test `AnnouncementWell.onboarding.test.tsx`. It covers open,
finished, no first task, read failure, and the open-to-done transition
after `["issues", companyId]` is invalidated.
`AnnouncementWell.test.tsx` now gives the well a `QueryClient` with a
finished onboarding state.

## Verification

Red on `master` (819420a60a), before the fix. Vitest:

```
cd ui && npx vitest run src/components/AnnouncementWell.onboarding.test.tsx
     × stays hidden while the onboarding first task is open
     × stays hidden when onboarding state cannot be read
      Tests  2 failed | 1 passed (3)
AssertionError: expected true to be false
```

Red on `master`, Playwright at 1440x900 (an earlier draft of the spec
also had a placement check, recorded here as evidence for the flyout
part of the report):

```
✘ announcement-first-session.spec.ts › a brand-new company with its onboarding task open does not show the upsell
    Error: expect(locator).toHaveCount(expected) failed
    Locator:  getByRole('complementary', { name: 'Paperclip announcements' })
    Expected: 0
    Received: 1
✘ … where the upsell shows, it does not cover the account menu or its flyout
    Error: well {"x":12,"y":542,"width":352,"height":346} covers account trigger {"x":20,"y":856,"width":166,"height":36}
```

Green after the fix, merged with current `master` (f7070d68b5), with the
in-page positive control:

```
✓ announcement-first-session.spec.ts › a brand-new company with its onboarding task open does not show the upsell (10.1s)
  1 passed

cd ui && npx vitest run src/components/AnnouncementWell.onboarding.test.tsx src/components/AnnouncementWell.test.tsx
      Tests  6 passed (6)

cd ui && npx vitest run src/components/AnnouncementWell.onboarding.test.tsx src/components/AnnouncementWell.test.tsx \
  src/components/AnnouncementCard.test.tsx src/hooks/useAnnouncement.test.tsx src/components/SidebarAccountMenu.test.tsx src/components/Layout.test.tsx
 Test Files  6 passed (6)
      Tests  95 passed (95)
```

- `pnpm check:token-gates`: All gates clean.
- UI `tsc --noEmit`: no errors in app code. The only errors are the
git-ignored, generated Storybook catalog files.
- CI note: the first CI run had three jobs fail with "The self-hosted
runner lost communication with the server" (server tests 12/12, Runner
vitest package, e2e shard 5/8). These are infra flakes. I reran them.
- Local e2e note: two runs could not start the server because embedded
PostgreSQL ports were in use by parallel e2e runs on the same machine. A
rerun passed. This is not related to the change.

Before (master): the card covers the account menu and the composer on
the onboarding first task.

![before: onboarding first
task](https://raw.githubusercontent.com/paperclipai/paperclip/c97519868225b0c206cf95fa3fb7150287c88116/n13-before-onboarding-task.png)

After: no card on the onboarding first task. The account menu and the
composer are clear.

![after: onboarding first
task](https://raw.githubusercontent.com/paperclipai/paperclip/c97519868225b0c206cf95fa3fb7150287c88116/n13-after-onboarding-task.png)

## Risks

- Low risk. The change only hides optional content.
- A user who never finishes or cancels the onboarding first task does
not see announcements in that company. This is the intended trade-off
for "not in the first session". Closing the task shows them again.
- The well makes one extra small issues read (`limit=1`) per company. It
is cached for 60 seconds.
- `AnnouncementWell.tsx` also changes in #19511. Whichever PR lands
second resolves a small conflict in the `enabled` condition and the
`<aside>` placement.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 12:00:50 -06:00
DottaandPaperclip 5d25117ffa fix(heartbeat): recover deferred dependency completion wakes (#19457)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Tasks wait for other tasks through saved dependency edges and wake
requests.
> - A dependency can finish while its assignee answers a question about
the blocked task.
> - Paperclip saves the completion wake behind that active turn.
> - Recovery did not find that saved wake unless it also contained a
comment or an interaction response.
> - This pull request restores that wake through the existing release
and dispatch checks.
> - The task can then continue after the active turn and its cleanup
finish.

## Linked Issues or Issue Description

**What happened?**

A task stayed blocked after its dependency finished. The completion wake
was saved while an audit turn was active. The wake stayed deferred after
that turn ended. It had no comment or interaction response, so both
recovery queries and the input guard excluded it.

**Expected behavior**

Recover the saved dependency wake when the prior turn and its cleanup
finish. Start one dependency continuation under the current execution
rules.

**Steps to reproduce**

1. Run a question response on a dependency-blocked task.
2. Finish the dependency while the response is active.
3. Save the completion wake behind that response.
4. Finish the response and its environment cleanup, or restart the
workers.
5. Observe that the saved wake has no run.

**Paperclip version or commit**

Reproduced against `70d23612dd`. Rechecked against `2f35eb1080` with the
final regression fixture.

**Deployment mode**

Source checkout on Linux x64, Node 25.9.0, pnpm 9.15.4, and isolated
embedded PostgreSQL.

Related work: #16673 added recovery for saved messages. #17288 and
#16966 address assignment wakes after cleanup. This change covers
dependency completion receipts with no message.

## What Changed

- Include saved `issue_blockers_resolved` receipts in worker
reconstruction and stranded queue selection.
- Let these receipts pass the input guard without a comment or
interaction response.
- Use the existing release transaction and dispatch checks. Add no
timer, migration, or direct task status change.
- Add process-backed regression cases for restart, cleanup release,
cancellation, agent pause, pending cleanup, and unresolved blockers.
- Update the dependency wake recovery document.

- Share the dependency-wake payload predicate and SQL fragment across
reconstruction and stranded receipt admission, so these eligibility
checks have one owner.

## Verification

- The final regression fails on the base code: the receipt stays
`deferred_issue_execution` and has no successor. The same test passes
with the fix.
- Six focused cases pass. They use real PostgreSQL and a real process
adapter. They check saved output, one dependency continuation, clean
task locks, repeat recovery, and execution gates. They do not call a
model provider.
- Three nearby suites pass: 125 tests across queue wiring, dependency
scheduling, and dependency recovery.
- Final-head server layered typechecking passes with
`PAPERCLIP_TYPECHECK_LOW_MEMORY=1`. Whitespace and module boundary
checks pass. Two parallel checker attempts reached the sandbox memory
limit; the completed low-memory check covers all source and test layers.
- The full deferred-receipt suite passes: 45 tests. Combined with the
nearby suites, 170 local tests pass. These results cover head
`8f9d3aa6ac19a6d2532bd869eb8adda7cf122a5a`. Required CI remains in
progress.
- Local workspace typechecking and build stop at the Runner Rust step
because Cargo is absent. The full local test suite was not run. Required
CI must supply broad coverage.
- The hosted incident has not been recovered or deployed from this
branch.

- Review follow-up at `182385e9bc433adac06440997de7e4cfbf889a72`: normal
merge of master `0d03610d2f73b7cd799547e43929bb2303319024`; no conflict
or history rewrite. All 54 receipt/helper cases pass on macOS arm64,
Node 26.4.0, real embedded PostgreSQL and process adapters. The six
dependency continuation and safety cases also pass separately. Module
boundaries and whitespace checks pass. No assertion, timeout, execution
gate or retry policy changed for this extraction.
- Current shared, SDK and adapter-utils source builds pass. The first
broad local server typecheck read stale borrowed adapter declarations;
rebuilding them exposed nominal type duplication between the borrowed
workspace and this checkout. It is not counted as a server typecheck
pass. All 30 SDK and 169 adapter runtime JavaScript outputs match the
previously tested artifacts. Final revision
`182385e9bc433adac06440997de7e4cfbf889a72` now passes all 55 applicable
CI checks (three skipped, one neutral review), including clean-workspace
typecheck, build and broad tests. [CI
result](https://github.com/paperclipai/paperclip/actions/runs/42262173960).
Greptile and Paperclip review both give 5/5 with no open findings.

## Risks

- Recovery now considers a saved dependency receipt with no message.
Current ownership, source finalization, cleanup, pause, cancellation,
and dependency checks still control execution.
- The scan remains bounded and uses existing transaction notifications
and restart reconstruction. The integration checks verify that it
becomes quiet after settlement.

## Model Used

- OpenAI Codex, based on GPT-6. The exact provider model ID and context
size are not exposed in this session. Used reasoning, tool calls,
TypeScript edits, and test 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>
2026-11-06 11:49:38 -06:00
DottaandPaperclip e3fb4bc73a fix(native): recover saved listener startup failures (#19479)
## Thinking Path

> - Paperclip manages AI agents and their work.
> - Native recovery must preserve saved work and respect current task
authority.
> - A closed local API listener can stop startup before a provider
starts.
> - #19462 gives new failures a bounded bootstrap retry.
> - Older saved failures still have a generic terminal code and a
recovery hold.
> - This pull request verifies the old producer outcome and uses the
existing safe replacement path.
> - Eligible tasks continue once. Unknown provider outcomes and user
holds remain blocked.

## Linked Issues or Issue Description

Refs: #19462

**What happened?**

A restart can leave an old listener startup failure in
`terminal_failure` with `native_session_interrupted`. Startup recovery
does not claim it after #19462. A completed file recovery can also
retain its no-replay hold.

**Expected behavior**

Continue automatically when durable records prove that no provider
started, the source stopped, and the current files were saved. Keep the
existing attempt budget and normal admission checks.

**Steps to reproduce**

1. Close the actual API listener after a fresh native coordinator claim.
2. Persist the old executor's generic failure. Save the workspace and
confirm source termination.
3. Restart recovery. The baseline schedules no successor. The repair
schedules one successor after all receipts pass.

**Paperclip version or commit**

Baseline: `0252421d5c`. Old failure producer: `2f35eb1080`.

**Deployment mode**

Built from source. Local macOS arm64, Node 26.4.0, real embedded
PostgreSQL, independent filesystem execution. No external AI service is
used by the controlled acceptance.

## What Changed

- Recognize the exact saved local-listener guard through one proof
predicate shared by discovery and the locked transaction.
- Require a fresh first attempt, matching execution bindings, no prior
provider lineage, no provider events or tool calls, no checkpoint or
admission receipt, and no cancellation.
- Reuse current snapshot, exact stop, source block, attempt budget, and
successor idempotency checks.
- Wait for completed source file-recovery receipts. Retire only that
same source's verified preservation hold in the successor transaction.
- Bound discovery to 100 candidates per pass, keep completed history
quiet, and wake through existing transactional producer notifications.
- Retain retries after transient database and final ownership-proof
errors; advance the bounded cursor and force a rescan so a failing page
cannot hide later work.
- Let only the replacement duty discharge a proved-empty notification
during drain through its existing read-only readiness check, once per
notification generation. Keep real work and concurrent changes
outstanding without running replacement during drain.
- Wake replacement at canonical local-stop proof writes and the existing
dispatch eligibility boundary; these hints grant no new execution
authority.
- Add database regressions, scheduler quietness checks, real controller
handoff coverage, and an opt-in controlled Runner continuation.

## Verification

Final production head: `0df41b2fef2644195d1224d36984af3827da3e3b`.

Local environment: macOS arm64, Node 26.4.0, TypeScript 7.0.2, Vitest
5.0.3, frozen-lockfile dependencies. Tests execute freshly;
`--fsModuleCache` reuses only content-validated module transforms.

- Native safe replacement: 62/62 PASS on isolated PostgreSQL 17 (63.18
seconds), including clean per-suite database disposal. All 62 assertions
also passed on embedded PostgreSQL 18 beta, but its Darwin cleanup hook
timed out; that invocation is not counted as a pass. Docker's disk was
full, so the disposable PostgreSQL 17 data used a size-limited RAM
mount, with no host mounts. The owned container was removed afterward.
- Actual stopped-process producer suite: 31/31 PASS. A real exited child
produces the stop receipt, and its replacement notification settles only
after the outer transaction commits.
- Workspace and separate-process drain regressions: 33/33 PASS (75.90
seconds).
- Shared coordinator and execution-control lifecycle checks: 39/39 PASS
(16.43 seconds). Pending work, concurrent changes, cold startup,
shutdown, and passive drain boundaries remain covered.
- Actual suppressed Cloud routine/drain workflow: PASS (34.64 seconds),
using the unchanged test and timeouts. Its matching local RED reproduced
CI's 462-query, zero-pending-query, one-idle-hold timeout after an
admitted write committed during drain (69.46 seconds).
- Actual historical controller handoff: PASS (48.40 seconds). A separate
controller loads the original executor from `2f35eb1080`, writes its
generic listener failure, and exits. Recovery at the final head admits
exactly one successor. The real Rust Runner and controlled Codex process
complete governed `paperclip_finish`; attempt two commits and the saved
file remains intact. Only the child loads the old artifact, and both
isolated artifacts are restored afterward. Rust source subtree
`77961b70f177ec3c38d11abcaec92fd3d0e96ec0` matches the built binary. No
external AI service is used.
- Seven corrected existing fixtures pass: transactional stop-proof calls
(5 cases) and notification inventory (2 cases). Their earlier CI
failures exposed fixture calls outside the documented transaction and an
outdated expected queue set.
- Server typecheck: PASS.

Red/green proofs also cover the original saved failure on baseline
`0252421d5c` (zero successors instead of one), held-history quietness,
bounded forward scanning through 100 parked rows and concurrent wakes,
real snapshot/cleanup producer notifications, actual issue-row lock
timeout, and task/history eligibility notifications. An obsolete
prior-run question can be skipped to request another proof check; actual
skip while the task is paused or the source owns provider tool activity
schedules no successor.

The failed-page regression starts with 25 persistent verifier errors and
a healthy candidate 26. Baseline RED repeats the first page. Repair
GREEN advances the healthy candidate, revisits the original failures
after their fault clears, creates all 26 successors exactly once, and
becomes quiet.

Greptile is SUCCESS, 5/5 on the exact final head (2026-11-06 17:09:21
UTC). Final-head CI is GREEN: 56 successful checks, three intentional
skips, one neutral result, and no pending or failing checks. [Attempt
two and
results](https://github.com/paperclipai/paperclip/actions/runs/42258352876/attempts/2)
include passing build, typecheck, Rust, native server, package, browser
E2E, serialized server, general server/chat tests, and final
verification. Five jobs in the first attempt were interrupted together
at 17:13:12 UTC; the native-server log records an explicit runner
shutdown signal. The single infrastructure retry reran only those five
jobs and dependent verification, preserving the 49 successful checks on
the unchanged head. No timeout was weakened, and setup failures or
interrupted invocations are not counted as passes.

Focused commands from the repository root:

```sh
./server/node_modules/.bin/vitest run --project @paperclipai/server server/src/services/remote-workspaces.integration.test.ts server/src/services/native-runtime/native-runner-restart-preparation.integration.test.ts -t 'native safe-replacement decision|reconciles the drained remote backend' --fsModuleCache --reporter=dot
./server/node_modules/.bin/vitest run --project @paperclipai/server server/src/__tests__/execution-control-work.test.ts server/src/__tests__/delivery-work-coordinator.test.ts -t 'execution-control lifecycle|work coordinator' --fsModuleCache --reporter=dot
node node_modules/typescript/bin/tsc -p server/tsconfig.json --noEmit --pretty false
```

The historical acceptance sets `PAPERCLIP_TEST_HISTORICAL_LISTENER=1`
and uses an original-executor child artifact with a `try/finally`
restore. Normal regressions require no Rust binary.

## Risks

- Compatibility is deliberately limited to positively proved fresh
remote startup failures. Other old ambiguous failures remain held.
- Existing provider work, a changed task block, incomplete preservation,
or revoked authority must prevent execution. Normal retry admission
remains in control.
- This PR does not deploy the change or mutate a hosted task. Hosted
recovery still requires the repaired revision and all current proof
gates.

## Model Used

OpenAI GPT-6 through Codex, with reasoning, tool use, and code
execution. The exact backend model identifier 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
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 11:49:28 -06:00
DottaandClaude Opus 5.5 30a0b5982e fix(runner): let Codex agents generate images with their existing provider key (#19516)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The Paperclip runner launches Codex agents in an isolated app-server
with a sandboxed shell
> - On the Codex API path, the agent's OpenAI API key reaches only the
trusted app-server process, never the shell
> - The runner launched Codex with `--disable image_generation`, so
Codex's built-in image tool was not available
> - Codex then falls back to its `image_gen.py` script, which needs
`OPENAI_API_KEY` in the shell, so the agent asked the user for a second
key
> - This pull request keeps Codex's built-in image tool on, so it uses
the agent's existing provider auth, and lets the sandbox read only the
folder where Codex saves the images
> - The benefit is that a Codex agent can make images without a second
key, and the provider key still does not reach the shell

## Linked Issues or Issue Description

**What happened?**
Manual onboarding test on Paperclip Cloud staging (Codex API path):
"Open AI requests another api key for image model even though it has an
active api key powering the agent."

**Steps to reproduce**
1. Create a company with a Codex agent that uses an OpenAI API key.
2. Ask the agent to generate an image.
3. The agent asks for another OpenAI API key.

**Expected behavior**
The agent uses the OpenAI API key that already powers it.

**Root cause**
`createIsolatedCodexAppServerArgs` passed `--disable image_generation`
and the thread config set `features.image_generation: false`. The pinned
Codex 0.160.0 binary has two image paths. Its own instructions say the
built-in `image_gen` tool "Does not require `OPENAI_API_KEY`". The
fallback CLI path "Requires `OPENAI_API_KEY`". The runner removes
`OPENAI_API_KEY` from the shell on purpose
(`codex-shell-environment.ts`), so the fallback can never work and the
agent asks the user.

## What Changed

- `codex-security-config.ts`: remove `--disable image_generation` and
`features.image_generation: false`. Codex's built-in tool runs in the
trusted app-server with the agent's existing auth.
- `codex-security-config.ts`: in the workspace and plan permission
profiles, grant **read** on `CODEX_HOME/generated_images` only. Codex
saves images there and tells the agent to copy them into the workspace.
The rest of `CODEX_HOME` (for example `auth.json`) stays denied.
Preconfigured-home mode is unchanged (it does not deny `CODEX_HOME`).
- `eval-gateway-runtime.ts`: set `image_generation = false` in the
controller-gateway Codex config. That gateway admits no hosted tools
(`unqualified_hosted_tool`), so qualified evals keep the earlier
behavior.
- Tests: two new N-12 tests, and updates to the tests that asserted the
old disable flag.

## Verification

Red on `master` (819420a60a), same test file before the source change:

```
     × N-12: lets an API-key Codex agent generate images with its existing provider key instead of asking for another 5ms
     × N-12: the pinned Codex binary keeps built-in image generation enabled under the isolated launch 4400ms
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯
 FAIL  src/drivers/codex/codex-security-config.test.ts > Codex security configuration > N-12: lets an API-key Codex agent generate images with its existing provider key instead of asking for another
AssertionError: expected [ '-c', …(22) ] to not include 'image_generation'
    378|     expect(args).not.toContain("image_generation");
 FAIL  src/drivers/codex/codex-security-config.test.ts > Codex security configuration > N-12: the pinned Codex binary keeps built-in image generation enabled under the isolated launch
AssertionError: expected 'image_generation                     …' to match /\strue\s*$/
+ Received:
"image_generation                         stable             false"
      Tests  2 failed | 17 skipped (19)
```

Green after the fix:

```
      Tests  2 passed | 17 skipped (19)
```

Neighbouring suites (`src/drivers/codex`,
`src/cli/eval-gateway-runtime.test.ts`, `src/provider-events.test.ts`):

```
 Test Files  25 passed (25)
      Tests  429 passed (429)
```

Real sandbox check with the pinned Codex 0.160.0 binary (`codex sandbox`
with the generated permission profile, macOS seatbelt):

```
generated_images/t1/img.png status=0 "PNGDATA"     # agent can read the saved image
auth.json status=1 "cat: ...: Operation not permitted"  # rest of CODEX_HOME stays denied
cp status=0                                          # agent can copy the image into the workspace
```

- `pnpm exec tsc --noEmit -p packages/paperclip-runner`: no errors in
changed files (only pre-existing errors from the unbuilt
`@paperclipai/paperclip-eval-kernel` package).
- `pnpm verify` exits 2 (changes outside resolved test graphs); required
CI supplies the broad coverage.
- **Test scope.** The red/green tests assert the launch configuration:
the disable flag, the thread config, the sandbox grant, and the
effective `image_generation` state that the pinned Codex binary reports.
They do not drive a model. So they do not directly observe the reported
"asks for a second key" behavior. The link from the flag to that
behavior comes from the pinned Codex 0.160.0 bundled instructions: the
built-in `image_gen` tool "Does not require `OPENAI_API_KEY`", and the
fallback `scripts/image_gen.py` CLI "Requires `OPENAI_API_KEY`". The
runner removes `OPENAI_API_KEY` from the shell.
- Not run: a live paid image generation with an API-key Codex agent.
There is no Codex staging company.

## Risks

- Image generation uses the agent's OpenAI account. Images cost money on
API-key billing. This is the expected behavior when the user asks for an
image.
- The agent can read images it (or an earlier thread of the same Codex
home) generated. It cannot read other files in `CODEX_HOME`.
- Codex's built-in `view_image` tool was already enabled, so the trusted
process already reads image files for the model. This change does not
add a new kind of file access to the provider.
- If a future Codex release changes the image save folder, the agent can
still see the image inline but cannot copy it. The N-12 binary test pins
the feature state for the current Codex version.

### Security policy change (needs maintainer sign-off)

**Why it was disabled.** #12364 ("isolate Codex security configuration")
disabled host apps, plugins, memories, multi-agent behavior, and image
generation as one group. Its goal was a closed launch configuration that
does not depend on ambient Codex defaults. The PR records no specific
threat for image generation. This PR keeps the other features disabled.

**Why re-enabling it is safe.**
- The tool is a hosted tool. It runs at OpenAI inside the same model
request that the trusted app-server already makes. It adds no new
network endpoint, process, or shell capability.
- No credential moves. `OPENAI_API_KEY` stays out of the shell, argv,
and serialized config (the N-12 test asserts this).
- The new sandbox grant is read-only and covers only
`CODEX_HOME/generated_images`. The rest of `CODEX_HOME` (`auth.json`,
config, sessions) stays denied. The real `codex sandbox` check above
confirms this. In plan mode the grant is also read-only.
- Codex's `view_image` tool was already enabled. So the trusted process
already sends local image content to the provider.
- Controller-gateway evals keep the tool off (`image_generation =
false`), because that gateway admits no hosted tools.

**Credentials and billing.**
- API-key path: the hosted tool uses the agent's existing OpenAI API key
through the app-server. OpenAI bills image generation to that API
account at image-model rates.
- ChatGPT-subscription path: the tool uses the agent's existing ChatGPT
sign-in. Use counts against that ChatGPT plan's limits. There is no
separate API bill.
- These two points follow from how Codex sends hosted tools in the
provider request. I did not observe them in a live paid run.
- Open question: Paperclip cost tracking uses provider token usage. I
did not confirm that image-generation charges appear in Paperclip's
per-agent cost and budget totals. If they do not, budget hard-stops
could undercount image spend.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 11:48:54 -06:00
DottaandClaude Opus 5.5 19239c11d1 fix(onboarding): move Use a custom gateway under Advanced on the API key step (#19510)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - New users connect a model in the onboarding wizard's "Connect a
model" step.
> - On the Claude API and Codex API paths, the step shows an API key
card.
> - The "Use a custom gateway" button sat directly under the key field,
inside that card, with no gap.
> - A tester reported that it hugs the field and that it belongs under
Advanced.
> - A custom gateway is an alternative route, not the next step after
the key.
> - This pull request moves the button into the step's existing Advanced
disclosure.
> - The benefit is a cleaner key card, and the gateway route stays one
click away.

## Linked Issues or Issue Description

No public issue exists. A manual new-user onboarding test on a staging
cloud workspace found this bug. Related: #17253 added the custom gateway
route.

**What happened?**
On the onboarding "Connect a model" step, API key path (Claude API and
Codex/OpenAI API), the "Use a custom gateway" button is shown at the top
level, directly against the API key text field.

**Steps to reproduce**
1. Start onboarding and reach "Connect a model".
2. Choose "Use API key instead", then pick Claude (or OpenAI).
3. See "Use a custom gateway" directly under the "Enter API key here"
field.

**Expected behavior**
The custom gateway control is in the Advanced section of the step. It is
hidden until Advanced is opened.

Source: manual new-user onboarding test on Paperclip Cloud staging (item
N-16, P3, Claude API path).

## What Changed

- `ui/src/components/OnboardingWizard.tsx`: render
`OnboardingGatewaySetup` inside the connect step's existing `Advanced`
`<details>` (above the runner select), not inside the API key card. It
keeps the same gates as the card it left: the card is live and mounted,
no connect progress, API credential mode, a created company, and a
Claude or Codex adapter.
- `ui/src/components/onboarding/OnboardingGatewaySetup.tsx`: accept an
optional `className`. The wizard passes `-ml-3` so the ghost button's
label lines up with the Advanced summary.
- The gateway dialog and its save/select-model/connect flow are
unchanged.
- `ui/src/components/OnboardingWizard.test.tsx`: new test for the Claude
API and Codex API paths. It asserts the button is in a closed `Advanced`
disclosure, outside the key field's card, and that the gateway dialog
opens after Advanced is expanded.

## Verification

Red on unchanged source (`npx vitest run
src/components/OnboardingWizard.test.tsx -t "custom gateway"`):

```
 ❯ src/components/OnboardingWizard.test.tsx (212 tests | 2 failed | 208 skipped)
       × keeps Use a custom gateway in the closed Advanced disclosure on the Claude API key step
       × keeps Use a custom gateway in the closed Advanced disclosure on the Codex API key step
AssertionError: Use a custom gateway should live inside the Advanced disclosure: expected null to be truthy
 Test Files  1 failed (1)
      Tests  2 failed | 2 passed | 208 skipped (212)
```

Green after the fix (same command):

```
 Test Files  1 passed (1)
      Tests  4 passed | 208 skipped (212)
```

Neighbouring suites (`OnboardingWizard.test.tsx`,
`ai-connections/AiProviderSetup.test.tsx`, `components/onboarding/*`),
including the existing end-to-end custom gateway save/test/hire test:

```
 Test Files  8 passed (8)
      Tests  270 passed (270)
```

- `pnpm check:token-gates`: all gates clean.
- `tsc --noEmit` for `ui`: no new errors. The only errors are the
existing unbuilt `@paperclipai/plugin-sdk/ui` import in
`PluginOrganizationSwitcher`.

Screenshots (Storybook `Onboarding/Agent arc › Connect · Claude API
key`):

| Before (master) | After (Advanced closed) | After (Advanced open) |
| --- | --- | --- |
|
![before](https://raw.githubusercontent.com/paperclipai/paperclip/1dae9c09f9a4070c66cd313dbb64baf1636c54d0/n16-before.jpg)
| ![after
collapsed](https://raw.githubusercontent.com/paperclipai/paperclip/1dae9c09f9a4070c66cd313dbb64baf1636c54d0/n16-after-collapsed.jpg)
| ![after
expanded](https://raw.githubusercontent.com/paperclipai/paperclip/1dae9c09f9a4070c66cd313dbb64baf1636c54d0/n16-after-expanded.jpg)
|

The red "This item doesn't exist or was moved." line in the screenshots
comes from the story's runner fixture. It is present on master too and
is not related.

## Risks

- Low risk. This is a layout move of one control. The gateway flow and
its gates are unchanged.
- The control is now one click deeper. This is the intended outcome of
the report.
- After a gateway is saved, "Use a different API key" stays in the card,
so a user can still leave the gateway route without opening Advanced.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with
tool use (shell, file edits, browser for Storybook screenshots).

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 11:42:21 -06:00
DottaandClaude Opus 5.5 63e550b338 fix(ui): keep the audit hash out of the tool approval card (#19509)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents call connected-app tools (for example GitHub
`create_repository`) through the tool gateway, and a write call waits
for a human approval card
> - The pending approval card showed an "Arguments and audit hash"
disclosure. The disclosure printed the 64-character SHA-256 of the
reviewed arguments
> - A new user in onboarding testing called this "a long hash string in
the review approval card" that is confusing and unapproachable
> - The hash identifies the exact arguments for audit. The approver does
not need to read it to decide
> - This pull request removes the raw hash from the card text and keeps
it behind a "Copy audit hash" action
> - The benefit is a plain approval card, with the audit value still
available to people who need it

## Linked Issues or Issue Description

**What happened?**
On the pending tool-approval card, the "Arguments and audit hash"
disclosure printed the raw SHA-256 of the reviewed arguments (for
example `8ca5312877235a…`, 64 hex characters) under the arguments JSON.

**Expected behavior**
The approval card uses plain language. Internal identifiers do not show
as card text. A person who needs the audit hash can still get it.

**Steps to reproduce**
1. Let an agent call a write tool on a connected app (for example GitHub
`create_repository`).
2. Open the pending "Approve tool action" card ("Review request").
3. Expand "Arguments and audit hash". The raw 64-character hash shows
under the arguments.

**Paperclip version or commit**
Current `master` (819420a60a) and the Paperclip Cloud staging build used
for onboarding tests.

## What Changed

- New `ToolActionAuditHashCopy` button
(`ui/src/components/ToolActionAuditHashCopy.tsx`). It copies the
arguments hash to the clipboard through the shared `useCopyAction` hook,
and shows "Copied" or "Couldn’t copy".
- `IssueThreadInteractionCard` (`RequestToolActionCard`): the disclosure
is now "Arguments". The raw hash `<p>` is replaced by the copy button.
- `TaskChatCompactInteractionCard`: the same change in both places that
printed `toolAction.argumentsHash`.
- If the clipboard is blocked, the button shows "Couldn’t copy" and the
hash then shows as selectable text, so the audit value stays reachable
(review feedback).
- Tests: a new regression test renders the review flow with the real
staging payload shape (GitHub `create_repository`, raw gallery tool id,
64-hex hash) and asserts that no `/[0-9a-f]{12,}/i` text shows before or
after the disclosure opens, and that the copy action writes the hash.
The existing long-proposal test now asserts that the hash is not in the
disclosure text. `ToolActionAuditHashCopy.test.tsx` covers copy success
and the blocked-clipboard fallback. The clipboard mock is restored after
the test.

## Verification

Red on `master` (new test only, before the fix):

```
 FAIL  src/components/task-chat/TaskChatInteractionCard.test.tsx > TaskChatInteractionCard > keeps raw tool ids and the audit hash out of the tool approval card text
AssertionError: expected 'Gcreate_repository · Name: brainrot-s…' not to match /[0-9a-f]{12,}/i
+ Received:
"Gcreate_repository · Name: brainrot-storefront-prototype · Private: trueArguments and audit hash{\"name\":\"brainrot-storefront-prototype\",\"private\":true}8ca5312877235a4c0f6e9b1d2a7c3e5f9081b2c4d6e8fa1b3c5d7e9f0a2b4c6dDeclineApprove & run"
 Test Files  1 failed (1)
      Tests  1 failed | 32 skipped (33)
```

Green after the fix (same test):

```
 Test Files  1 passed (1)
      Tests  1 passed | 32 skipped (33)
```

Neighbouring suites (`ui/src/components/task-chat`,
`IssueThreadInteractionCard.test.tsx`, `AttentionQueueRow.test.tsx`,
`transcript/RunTranscriptView.test.tsx`), after rebase on current
`master`:

```
 Test Files  62 passed (62)
      Tests  1047 passed (1047)
```

After the review fixes (`ToolActionAuditHashCopy.test.tsx`, `task-chat`,
`IssueThreadInteractionCard.test.tsx`):

```
 Test Files  61 passed (61)
      Tests  1004 passed (1004)
```

- `pnpm check:token-gates`: all gates clean.
- `tsc --noEmit` on `ui`: no errors in the changed files.
- Visual check in Storybook story `Issue Thread Interactions / Tool
Action Pending Write`, disclosure open:
- Before: the summary reads "Arguments and audit hash" and a mono line
`sha256:9f2c1a7be4d0c8a3` shows under the JSON.
- After: the summary reads "Arguments". A small ghost "Copy audit hash"
button shows under the JSON. No hash text shows.

## Risks

- Low risk. UI copy and layout change on one card type only. The server
payload, the approval decision, and the stored hash do not change.
- A person who read the hash off the card must now use the copy button.

> 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`), in Claude Code with
tool use (shell, file edits, browser). Default context window and
reasoning mode.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 11:39:44 -06:00
DottaandClaude Opus 5.5 a0cba70f18 fix(runner): continue restart-interrupted ACPX and OpenCode turns (#19501)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs keep their provider conversation when the control plane
restarts
> - A local Codex turn lost in a restart already continues once in the
same conversation (#15297)
> - ACPX (Claude and the other ACPX profiles) and OpenCode took a
different path: the runner closed the lost turn as a provider failure
> - The task then blocked with "Provider turn failed", and the user's
queued follow-up stayed unread
> - A restart must not need a user action when continuing is safe
> - This pull request gives ACPX and OpenCode the same recoverable
restart-loss cause and the same single continuation as Codex
> - The benefit is that work interrupted by a controller restart
finishes on its own

## Linked Issues or Issue Description

Refs #15297 (Codex continuation), #19399 (restart-interrupted turn
hook), #19471 (harness fix used for the live reruns).

**What happened?**

After a control-plane restart during an active native ACPX Claude turn,
the task ended `blocked` with `native_provider_terminal_failed: Provider
turn failed`. The queued follow-up comment was not consumed. No
continuation started. The next run then failed with
`native_session_cleanup_quarantined`. A native OpenCode run killed
mid-turn ended the same way (`turn.failed` with `error: null`).

**Expected behavior**

When the old provider process is gone and the conversation can be
restored, continue the task once in the same conversation. When that is
not safe, record an honest restart cause instead of a generic provider
failure.

**Steps to reproduce**

1. Start a native ACPX Claude task on a local runner.
2. Submit a follow-up comment while the turn is active.
3. Restart the control plane while the turn is still running.
4. Run `pnpm test:e2e:runner -- --max-automatic-retries 0 --id
everyday-workflows.runner-acpx-claude.local.recover-controller`.

**Root cause**

- `runner-core/src/acpx_provider_backend.rs`
`restore_session_if_needed`: a saved `turn_active` state always closed
the session. It recorded `turn.failed` with only a top-level `code:
acpx_active_turn_recovery_closed` and no `error` object. It also emitted
`run.terminal`.
- The runnerd transport forwards only `error`, so the controller saw
`error: null` and reported generic `provider_turn_failed`.
- `runner-core/src/provider_backend.rs` gave the recoverable
`provider_turn_lost_on_restore` cause only to `provider_label ==
"codex"`. OpenCode shares that backend.
- `nativeRestartInterruptedTurnId` accepted only `codex_app_server`. The
runtime continuation gate accepted only `provider.kind === "codex"`.
- Two follow-on defects appeared once the continuation ran live:
- The restored ACPX provider rejected the controller's result for the
lost turn's open semantic call. That call never settled.
- The restored provider's MCP request ids restart. The controller keys
semantic calls by `hash(runId, callId)`. The continuation's first
Paperclip tool call reused an earlier id, was treated as already
answered, and hung. After the result was accepted,
`runner_settlement_incomplete` failed the run.

## What Changed

- ACPX runner: for an active turn lost to a runner restart, prove that
the original provider lifetime lost its fence quorum. Then restore the
same conversation and record `turn.failed` with `error: { code:
"provider_turn_lost_on_restore", recoverable: true }`. Keep the run open
for the controller's continuation. An accepted result, an unanswered
runtime request, or an active goal keeps the old close behavior. So do
an unproven lifetime fence and a failed restore.
- ACPX runner fallback close: add a structured `error`
(`acpx_active_turn_recovery_closed`, not recoverable, with an honest
restart message), so the cause survives the transport.
- ACPX runner: track open semantic calls durably. On restore, retire the
lost turn's open calls. Acknowledge their controller results as
`retired` and do not send them to the new provider.
- ACPX sidecar: when the sidecar restores an existing session, prefix
forwarded semantic `callId`s with a per-process namespace
(`r<hex>:<id>`). Fresh sessions keep raw ids.
- OpenCode runner: give OpenCode the same recoverable cause and open run
as Codex (`restart_lost_turn_is_continuable`). Cursor Cloud stays
excluded because its agent runs remotely.
- Runner TS: `nativeRestartInterruptedTurnId` accepts `acpx_runtime` and
`opencode_server`. The runtime continuation gate accepts `acpx` and
`opencode`. All other Codex safety rules are unchanged: one
continuation, admitted dead-runner restart only, no compaction, goal,
semantic result, or pending runtime request.
- Docs: `doc/execution-semantics.md` and `doc/run-log-events.md`.

## Verification

Red, then green, at each layer:

- Rust `tests/acpx_warm_attachment.rs`
`restores_the_conversation_after_losing_an_active_turn_to_a_runner_restart`.
Red: status `closed`. Then red on the lost call's result: "tool result
does not match a pending provider call". Green after the fix.
- Rust `tests/native_provider_backend.rs`
`restart_lost_opencode_turn_records_a_recoverable_cause`, with the
qualified OpenCode launch fixture. Red: `error` was `null`. Green after
the fix.
- Rust unit test
`restart_lost_turns_continue_only_for_locally_restored_providers`
(Codex, OpenCode yes; Cursor Cloud no).
- TS `native-session-runtime.test.ts` "continues a restart-lost
acpx_runtime / opencode_server turn once". Red:
`NativeProviderTerminalFailure provider_turn_lost_on_restore`. Green:
exactly one continuation, which does not resend the task.
- TS `acpx-runtime-sidecar.test.ts`: a restored provider's call ids are
namespaced and resolve.
- Server `native-runner-restart-recovery.integration.test.ts`: a
checkpointed `acpx_runtime` or `opencode_server` restart interruption is
claimed as `resume_dead_runner`. Red without the driver gate. Green with
it.
- Two `native_provider_backend.rs` tests asserted the old
close-on-restore result. They now assert the new contract. Their real
invariant still holds: the delivered response is never replayed.

Suites run locally on the final revision:

- runner-core: `cargo test --no-fail-fast acpx`, `--test
native_provider_backend` (38), `--test acpx_warm_attachment`, `--test
codex_provider restart_lost`
- runner TS: `native-session-runtime`, `runnerd-restart-continuation`,
Codex driver recovery, contracts, and `acpx-runtime-sidecar` (all pass)
- server: `native-runner-restart-recovery.integration.test.ts` (80
passed)
- `pnpm --filter "@paperclipai/paperclip-runner..." build` (all
generated-contract checks pass)

Live runner E2E (local, real Claude via ACPX, macOS arm64):

- `everyday-workflows.runner-acpx-claude.local.recover-controller`: PASS
twice in a row on the final runner code. The restart crossed an active
turn. The task finished `done`, the queued follow-up was consumed, the
ZIP download was verified, and there were no failure codes.
- `stock-harness.runner-acpx-claude.local.continuity-restart`
regression: PASS.
- Earlier failing reruns are kept as evidence of the follow-on defects:
one harness `kill EPERM` while stopping the server (unrelated to this
change), one hang on the colliding call id, and one
`runner_settlement_incomplete`.

## Risks

- A continuation can run more work in the restored conversation. Its
prompt requires inspecting state before repeating an uncertain action,
as for Codex.
- The ACPX restore waits up to 5 s for the original provider lifetime
fence before it falls back to the old close.
- The ACPX provider state file gains two fields with defaults. Older
saved states still load. A downgraded runner cannot read state that a
newer runner wrote with these fields.
- Restored ACPX sessions now forward prefixed call ids. Only fresh
sessions keep raw ids. Live proof covers Claude. Codex-ACP, Pi, Cursor,
Copilot, Grok, Kimi, and Antigravity share this path, but this PR proves
them only by unit tests and code review.
- OpenCode is covered by the runner fixture and gate tests. The live
OpenCode restart-chaos cell was not rerun here.
- `runner-core`
`unconfirmed_suspension_state_becomes_recoverable_only_after_cleanup_and_suspend`
is flaky when run in parallel. It also fails with the new path disabled,
so it predates this change.

## Model Used

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 11:39:29 -06:00
Nicky LeachandClaude Opus 5.5 98ebeefbc9 feat(environments): let managed config declare plugin environments (#19508)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A managed instance gets its instance-level environments from the
`PAPERCLIP_MANAGED_CONFIG` document
> - #19490 added the plugin environment rows, their marker index and
write floor, but nothing creates them yet
> - The managed config still accepts only one sandbox entry, so a
managed instance cannot get a plugin-provided environment without manual
setup
> - This pull request lets the managed config declare plugin entries
beside the sandbox entry, and provisions one locked row for each at boot
> - The benefit is that a managed instance gets its plugin environments
with no manual setup, and the rows recover by themselves when their
plugin restarts

## Linked Issues or Issue Description

Refs #19490 (the plugin environment rows this pull request provisions)

**Subsystem affected**
Server environments: the managed-config parser, the managed environment
bootstrap, and the environment routes' write floor.

**Problem or motivation**
A managed instance cannot get a plugin environment from its managed
config. The `environments` section accepts one entry, and that entry
always provisions the managed sandbox. Company settings cannot create a
plugin environment either. So each managed instance that needs one
requires manual setup.

**Proposed solution**
Let an `environments` entry declare `driver: "plugin"`. Boot provisions
one locked row for each plugin entry through the row layer from #19490.
A later pull request adds a one-time instance default for one plugin
entry, and advertises the new managed-config capability.

**Alternatives considered**
Manual setup on each instance, and a new control-plane endpoint. Both
need a step outside the deploy. The managed config already delivers the
sandbox entry.

**Roadmap alignment**
This extends the existing managed-config `environments` section, which
already provisions the managed sandbox. It adds no new product surface.

## What Changed

- `server/src/services/managed-config.ts`: an `environments` entry may
set `driver` to `"sandbox"` or `"plugin"`. An entry without `driver` is
still the sandbox entry, so today's documents parse unchanged.
- A plugin entry has `name`, optional `description`, `pluginKey` (the
plugin's manifest id), `driverKey` and optional `driverConfig`.
- At most one sandbox entry. Names are unique across the section, and
`pluginKey`–`driverKey` pairs are unique. Secret-like keys in
`driverConfig` refuse startup, as for sandbox `config`. An unknown key
or driver still refuses startup, and that includes `instanceDefault`
until the follow-up pull request.
- Plugin entries parse into a new `pluginEnvironments` list. The
`environments` list keeps only the sandbox entry, so its consumers do
not change.
  - `MANAGED_ENVIRONMENT_DRIVERS` lists the accepted drivers.
- `server/src/services/managed-environments.ts`
(`applyManagedEnvironments`):
- Boot refuses to start unless an auto-installed catalog entry installs
each plugin entry's `pluginKey`. `plugins.autoInstall` holds catalog
keys, not manifest ids, so the parser cannot check this. Boot uses the
manifest ids `createApp` already resolves from the bundled and
distribution catalogs (`app.locals.managedBundledPluginKeys`, passed in
by `server/src/index.ts`). Without that list it resolves the catalog
itself.
- Plugin entries run after the sandbox entry and reuse its readiness
handling. Boot awaits `pluginsReady`, then requires the plugin `ready`,
declaring the entry's driver, and running a worker. Otherwise boot
archives the row and re-ensures it on the worker's `ready`.
- Recovery re-ensures on every worker `ready` until one ensure succeeds
or the plugin rejects the config (HTTP 422).
- If the ensure fails because the worker died, boot archives the row and
waits for `ready`. If the worker is still running (for example the call
timed out), boot keeps the row as it is, retries once at once and on the
next `ready`, and otherwise at the next boot. No timer is added.
  - Removing an entry never archives its row.
- A plugin-only document does not conflict with
`PAPERCLIP_EXECUTION_MODE`, because plugin rows do not use the sandbox
slot.
- Plugin entries never change the instance default. The sandbox stamp
still fills only an unset or local default, so it never replaces a
plugin default.
- `server/src/routes/environments.ts`: `isPlatformSlotEnvironment`
treats a marked plugin row as a live slot while the document declares
its entry. So the marker-clear escape hatch is refused then, and allowed
again after the entry leaves the document.
- `doc/DEVELOPING.md`: new "Managed environments" section.

## Verification

- `cd server && npx vitest run src/__tests__/managed-config.test.ts`:
old documents parse unchanged. Sandbox plus plugin parses. Each of these
refuses startup: two sandbox entries, `driver: null`, duplicate names, a
duplicate pair, a secret-like key, an unknown key (including
`instanceDefault`), an unknown driver, a malformed `driverKey` or
`driverConfig`.
- `npx vitest run
src/services/managed-environments-plugin-entries.test.ts` (the
sandbox-entry tests stay in `managed-environments.test.ts`; shared
fixtures are in `managed-environments.test-support.ts`):
- A plugin entry is ensured with the validator's worker manager, after
the sandbox entry.
- Boot refuses a plugin entry whose `pluginKey` no auto-installed
catalog entry installs, both from the passed list and from its own
catalog lookup.
- A plugin that is down archives the row, and worker recovery re-ensures
it. A missing plugin, a plugin without the driver, or a worker manager
without `call` counts as unavailable.
- Recovery stays armed after a failed attempt and stops after one
success or a permanent config rejection (422). A `ready` during an
attempt queues one more.
- A worker that dies during the boot ensure archives the row and arms
recovery. A failure while the worker keeps running leaves the row as it
is and retries.
- Removing an entry does not archive. Plugin entries and the sandbox
stamp never change a plugin default.
  - An ensure failure is counted, not thrown.
- `npx vitest run src/__tests__/managed-plugin-environments.test.ts`
(embedded Postgres, real `applyManagedEnvironments`): the first boot
creates the row and the second changes nothing. A plugin that is down
archives the row, and recovery brings it back. Removing the entry does
not archive the row. The instance default stays unset. The row tests
from #19490 still pass.
- `npx vitest run src/__tests__/environment-routes.test.ts`: the marker
clear is refused while the document declares the entry, and allowed
after it leaves.
- `npx vitest run src/__tests__/instance-settings-service.test.ts
src/__tests__/instance-settings-cloud-defaults.test.ts`: pass with the
new `pluginEnvironments` field.
- `pnpm --filter @paperclipai/server typecheck` passes.
- I did not run the full `pnpm test:run`, `pnpm -r typecheck` or `pnpm
build`. CI covers them.

## Risks

- No schema change.
- A managed-config document with a plugin entry now provisions a row at
boot. A document without one behaves as before.
- Adoption renames an unmarked row for the same plugin and driver to the
declared name and resets its driver config to the declared one. The row
keeps its id, agents and other metadata, and it is then locked like the
managed sandbox.
- Boot now refuses to start when a plugin entry names a plugin that no
auto-installed catalog entry provides. This is the same fail-closed
posture as an unknown sandbox provider.

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 17:35:13 +00:00
DottaandClaude Opus 5.5 543aa4d331 fix(tasks): show a pending native same-run resume instead of a competing Retry (#19503)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - When a native run cannot reattach to its provider after a
control-plane restart, the native coordinator resumes the same run by
itself after about 30 seconds, up to three attempts
> - The run projection shows this correctly as "Retry scheduled" with
only `inspect_run` permitted
> - The task interruption shown on the issue read only system-recovery
evidence and `scheduledRetryAt`. It never read the coordinator's
`nextAttemptAt`, so it showed `state: failed, canRetry: true` and
offered a manual Retry while the automatic resume was still pending
> - The two surfaces disagree, and a user click can race the automatic
resume. That is one way a Retry gets parked behind "execution checks"
> - This pull request makes the task interruption read the coordinator's
pending resume time
> - The benefit is one consistent story: while the automatic resume is
pending, the task shows it with its time and no competing Retry. If
recovery ends, exactly one Retry returns

## Linked Issues or Issue Description

Refs #19487, #19497, #19426.

**What happened?**
A native run failed with `native_session_interrupted` after a restart.
Its finalization row had `phase: retryable_failure` and `nextAttemptAt`
30 seconds later. The run projection said "Retry scheduled". The issue
interruption said `state: failed, canRetry: true, nextRetryAt: null`.

**What did you expect?**
While the automatic resume is pending, the task shows that it is
resuming, with the time, and no Retry. When the resume fails for good or
never runs, exactly one Retry that works on the first click.

**Steps to reproduce**
1. Run a native task and restart the control plane so the native attach
fails.
2. Read `GET /api/issues/:id` while the coordinator's resume is pending.
3. Observe `interruption.canRetry: true` while `execution.phase` is
`retry_scheduled`.

## What Changed

- `task-interruption-presentation.ts`: the batched run read selects the
native coordinator's `next_attempt_at` when the native run failed, has
no result, and its finalization phase is `retryable_failure`.
- A pending resume in the future shows `retry_scheduled` with its time
and `canRetry: false`.
- A due resume shows `working` for up to two minutes while its dispatch
runs. After that, or once recovery is terminal, the task falls through
to the existing single Retry.

## Verification

- `task-interruption-presentation.test.ts`: new case for a future
resume, a due resume, a stale resume, and no resume. It fails before the
change (the task offered Retry) and passes after.
- `task-interruption-query.test.ts` (embedded Postgres): the real
batched query shows the pending resume for both list and detail reads,
and returns one Retry after the coordinator becomes `terminal_failure`.
- All 40 presentation tests and 17 query tests pass. Server typecheck
passes.

## Risks

- Low risk. This changes only the read model for tasks whose native
coordinator holds a pending same-run resume.
- If a resume never dispatches, Retry is hidden for up to two minutes
after its due time before it returns.
- The `POST /issues/:id/retry` endpoint is unchanged. A client that
ignores `canRetry` can still submit a Retry during the resume.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with
tool use (shell, file editing, embedded Postgres tests).

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 11:33:46 -06:00
Nicky LeachandClaude Opus 5.5 ff1d1dea5d fix(tools): reject non-tool connections before any write (#19492)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Apps and tools connect to Paperclip through tool connections
> - `POST /api/companies/:companyId/tools/connections` creates a tool
connection through `createConnection`
> - The request schema accepts a channel purpose and the chat and voice
transports, but `createConnection` cannot store them
> - The database then rejects the row, so the caller gets a 500 and a
stray tool application stays behind
> - This pull request rejects those requests with a 422 before any
write, and stores `config` as `transportConfig` when the caller leaves
`transportConfig` out
> - The benefit is a clear error, no leftover rows, and consistent
connection rows

## Linked Issues or Issue Description

No issue exists. The new test typecheck in #18739 found this problem.
#18739 listed it as a possible product problem and did not change it.

**What happened?**

1. `createConnection` never writes `input.connectionPurpose`, so every
row gets the database default `tool`.
2. The schema allows `transport: "chat_sdk"` and `transport: "voice"`.
The database check `tool_connections_channel_transport_check` does not
allow these transports on a `tool` row.
3. No error mapping exists for a check violation, so the route returns
500.
4. When the request has no `applicationId`, the service inserts a
`tool_applications` row first, outside the connection transaction. That
row stays after the failure.
5. The schema gave `transportConfig` a default of `{}`. The service
stores `input.transportConfig ?? config`, so it stored `{}` instead of a
copy of `config`.

**Expected behavior**

The route returns 422 for a purpose or transport that it cannot create,
and writes no rows. When the request leaves `transportConfig` out, the
row stores `config` there.

**Steps to reproduce**

1. Send `POST /api/companies/:companyId/tools/connections` with `{
"name": "x", "transport": "chat_sdk" }`.
2. See the 500 response.
3. See the new `tool_applications` row for the company.

## What Changed

- `server/src/services/tool-access.ts`: `createConnection` rejects a
purpose other than `tool`, and the `chat_sdk` and `voice` transports,
with 422. The check runs before any lookup or write. Channel connections
still come from their own services.
- `packages/shared/src/validators/tool-access.ts`: `transportConfig` is
optional and has no default. The update schema already removed defaults,
so it does not change.
- `server/src/__tests__/tool-access-service.test.ts`: added tests for
the three rejected inputs (no rows written), and for a request that
sends only `config`.

## Verification

- `pnpm exec vitest run server/src/__tests__/tool-access-service.test.ts
server/src/__tests__/generic-mcp-connection.test.ts
server/src/__tests__/tool-connection-health-work.test.ts
server/src/__tests__/openapi-routes.test.ts
server/src/__tests__/tool-access-routes.test.ts
packages/shared/src/validators`: 27 files, 995 tests pass.
- `cd server && pnpm typecheck`: passes. This includes the source layers
and the three test-tree shards.
- Not run locally: the full `pnpm test:run`, the root `pnpm build`, and
the UI typecheck. Required CI covers them.

## Risks

- Low risk. A client that sends a channel purpose or a chat or voice
transport to this route now gets 422 instead of 500. The request failed
before this change too.
- A client that sends only `config` now gets the same value in
`transportConfig`. Every reader checks `config` first, so this changes
no behavior.
- The OpenAPI output for this body no longer shows a default for
`transportConfig`.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with tool use
(shell, file edits) and extended thinking.

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 17:30:24 +00:00
DottaandClaude Opus 5.5 f5de197536 fix(ui): keep a busy-workspace wait as a quiet status, not a notice (#19507)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The task page tells the user what an agent is doing and what needs
their attention
> - When another task uses the same workspace, the agent waits and
Paperclip starts the work automatically
> - A tester reported that the "Waiting for workspace" notice is not
helpful, because the user cannot do anything about it
> - On `master`, the task page shows this wait four times: a pinned top
banner, a conversation banner, the composer strip and the Workspace
property row
> - This pull request removes the two banners and keeps the two quiet
status surfaces
> - The benefit is that a transient system wait no longer looks like
something that needs the user

## Linked Issues or Issue Description

Refs #19369

**What happened?**
During new-user onboarding tests on Paperclip Cloud staging (Claude
subscription path), the tester saw a "Waiting for workspace" notice and
reported: "this isn't helpful and the user can't do anything about it."

**Steps to reproduce**
1. Start work on a task while another task uses the same workspace.
2. Open the waiting task.
3. See the pinned "Waiting for workspace" banner under the title and a
second "Waiting for workspace" banner in the conversation.

**Expected behavior**
A busy workspace is a transient system wait with no user action. It
shows only as a quiet status on the task. It does not show as a banner
or notice.

**Investigation**
- No server inbox, Needs-you item or activity entry is created for a
workspace wait. The live-updates toast already skips `workspace_busy`
(`LiveUpdatesProvider.tsx`), and the chat thread already hides the
cancelled admission attempt.
- The remaining notices are UI banners: `IssueMonitorBanner` (pinned top
banner) and `IssueScheduledRetryCard` (conversation banner).
- #19369 stopped showing resource waits as failed attempts, but it kept
these banners.

## What Changed

- `IssueMonitorBanner`: render nothing for a workspace wait. The
composer strip still shows "Waiting for workspace" and "You can keep
sending instructions while the agent waits."
- `IssueScheduledRetryCard`: render nothing for `workspace_busy`. The
Workspace property row still shows "Waiting for workspace".
- `DESIGN.md`: add the rule that a busy-workspace wait is a quiet status
only, never a pinned banner, conversation notice or toast.
- Tests: replace the old "shows the banner" assertions with N-14
regression tests.

## Verification

Red on `master` (`819420a60a`), test change only:

```
cd ui && npx vitest run src/components/IssueMonitorBanner.test.tsx src/components/IssueScheduledRetryCard.test.tsx
     × does not raise a pinned banner for a workspace wait the user cannot act on (N-14) 7ms
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯
 FAIL  src/components/IssueMonitorBanner.test.tsx > IssueMonitorBanner / IssueMonitorComposerStrip rendering > does not raise a pinned banner for a workspace wait the user cannot act on (N-14)
AssertionError: expected 'Waiting for workspaceAnother task is …' to be '' // Object.is equality
 FAIL  src/components/IssueScheduledRetryCard.test.tsx > IssueScheduledRetryCard > raises no conversation notice for workspace contention the user cannot act on (N-14)
AssertionError: expected 'Waiting for workspaceAnother task is …' to be '' // Object.is equality
      Tests  2 failed | 32 passed (34)
```

Green after the fix, same command:

```
 Test Files  2 passed (2)
      Tests  34 passed (34)
```

Neighbouring suites:

```
cd ui && npx vitest run src/components/TaskChatThread.test.tsx src/pages/IssueDetail src/components/issue-properties src/lib/issue-monitor
 Test Files  7 passed (7)
      Tests  687 passed | 1 skipped (688)
```

- `pnpm check:token-gates`: all gates clean.
- `tsc --noEmit -p ui`: no errors in the changed files. Local errors
come only from unbuilt workspace packages (`@paperclipai/plugin-sdk/ui`,
Storybook feature map). CI typecheck covers the full build.

## Risks

- Low risk. UI-only change. Two banners no longer render for
`workspace_busy`. Other scheduled-retry reasons (failures, pool
exhaustion, continuations) are unchanged.
- A long workspace wait now has no prominent notice. No actionable next
step exists in the current data (the API does not name the task that
holds the workspace). A persist-past-threshold escalation with a link to
that task can follow when the data exists.

## Model Used

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

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 11:30:10 -06:00