Commit Graph
6734 Commits
Author SHA1 Message Date
DottaandPaperclip c66b4a55ef fix(ui): explain monitor waits during active runs (#19519)
## Thinking Path

> - Paperclip manages work performed by AI agents.
> - Task monitors schedule later checks through execution admission.
> - A native run must save its work before a monitor starts another run.
> - The monitor banner can show an overdue warning during this valid
wait.
> - It also promises the next tick and offers a check that cannot start.
> - This change explains the active run hold on both monitor surfaces.
> - Users can see why the check waits without changing execution guards.

## Linked Issues or Issue Description

Refs #16975 and #17863. The branch also preserves the quiet
workspace-wait behavior from #19507. These changes added the native
settlement guard and monitor deadline worker. A search found no open PR
for this display case.

**What happened?**

A monitor became due while its native run was still saving work. The
banner showed an overdue warning and “Fires on next tick.” The check
later ran. The warning did not explain the execution hold.

**Expected behavior**

Show the active run wait. Keep the saved check time. Restore the normal
display when the run clears.

**Steps to reproduce**

1. Keep a task run active and set a monitor on that task.
2. Let the saved check time pass.
3. Inspect the monitor banner and composer strip.

**Paperclip version or commit**

Reproduced in mounted production components at a434edf991. The branch
also includes current master.

**Deployment mode**

Source checkout on Linux. PostgreSQL tests use an isolated embedded
database. No staging deployment is part of this PR.

## What Changed

- Pass the existing task live-run state to both monitor surfaces.
- Explain the current run wait. Keep its tone neutral and its saved
check time visible.
- Hide Check now during the hold. Restore it when the run clears.
- Keep pending GitHub PR links visible during the hold. Restore Check
status after the run clears.
- Preserve blocked-task, retry, and idle-overdue display behavior.
- Add rendered regressions and PostgreSQL finalization-release tests.
- Document the distinction between a deadline and admission.

The monitor display owns this change. The page already reads task run
state, so this adds no API request. The scheduler and its guards are
unchanged. The PostgreSQL tests confirm existing behavior. They are not
evidence of a repaired scheduler fault.

## Verification

- Before the repair, all three new rendered cases failed: future,
due-now, and two-minute-overdue monitors during an active run.
- After the repair, 50 UI tests pass across IssueMonitorBanner and
issue-monitor. The same tests check return to normal display when the
run clears.
- Deadline, scheduled-work, and delivery-coordinator tests pass: 50
tests. The combined five-file run before the final safety assertion
passed 99 tests.
- PostgreSQL regressions cover release of committed and terminal_failure
holds, rollback, one admitted wake, and quiet workers after settlement.
- UI typecheck, token gates, and git diff --check pass.
- A frozen install and plugin SDK dependency build were required for
server test collection. The first collection attempt could not resolve
the unbuilt SDK. That attempt had no test verdict.
- At initial head ee030bdf22, the focused
three-file run passes 52 tests: 50 UI tests plus both PostgreSQL
finalization-release cases. UI typecheck and token gates pass again
after the master merge.
- The complete PostgreSQL scheduling suite passes all 56 tests before
the master merge. The earlier 54-test baseline also passes.
- A real local task page passes in Chromium with fresh embedded
PostgreSQL and a process run. Both monitor surfaces explain the hold.
Check now stays hidden. Advancing the client clock past the deadline
keeps the neutral wait. Reload preserves it. No page errors occur.
- The first browser probe used an already-due monitor on a legacy
process fixture. That path consumed the schedule before the page loaded,
so the expected banner was absent. The corrected probe saves a future
monitor and advances only the client clock. It verifies the display; it
does not claim native scheduling acceptance.
- [CI run
42260573923](https://github.com/paperclipai/paperclip/actions/runs/42260573923)
passed for ee030bdf22. A later review found hidden PR links during an
active run; c3604bf4b4 repairs that case. That revision later needed a
master conflict repair before build/test CI could start. Final head is
7fe2417e4005378a8c2aee9451ae14f6f49c54ff; its required checks have
completed successfully.

The incident data establishes an active-run hold at the first due time.
It does not fully explain the later interval between terminal status and
monitor admission. This PR fixes the confirmed display defect. It does
not claim to repair an unproven lost wake.

- The review regression failed first in two cases with no PR links:
future and overdue GitHub monitors during an active run. After the
repair, 52 UI tests pass. They cover links on both surfaces, held check
actions, return to Check status, and removal of merged PR links. UI
typecheck and token gates also pass.

- Current master was merged at 7fe2417e40. Both the PR-link tests and
master’s quiet workspace-wait tests are retained. All 65 tests across
IssueMonitorBanner, issue-monitor, and IssueScheduledRetryCard pass. UI
typecheck, token gates, and diff checks pass. [Final-head
CI](https://github.com/paperclipai/paperclip/actions/runs/42267525907)
completed successfully: 55 successful checks, three skipped, and one
neutral. Build, Typecheck + Release Registry, and ci / verify passed.
Greptile reviewed this exact head at 5/5 with no actionable findings.
The prior reviewer thread is resolved; no review threads remain open.
The real Chromium acceptance above was performed at ee030bdf22; the
final link handling and merged workspace-wait cases are covered by the
mounted component tests and fresh CI.

## Risks

- The wait display depends on the page’s existing live-run query. A
stale query can briefly keep the wait visible.
- A monitor with no active run still shows an overdue warning. A blocked
task keeps its existing pause controls.
- No schema, scheduling, ownership, or authorization change.

## Model Used

OpenAI Codex, GPT-6 family, with repository inspection, code editing,
shell tools, and test execution. The exact serving snapshot 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 `#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 16:16:58 -06:00
DottaandPaperclip a08f9494ea Remove duplicate workflow target fixture defaults (#19628)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Product evaluation workflows need repeatable tests for source
selection.
> - The workflow test supplies explicit default inputs to the real Bash
step.
> - Two merged changes added the same defaults to one environment
object.
> - TypeScript rejects duplicate object keys, so this blocks unrelated
PR checks.
> - This pull request removes the second copy and keeps the intended
defaults.
> - The benefit is a valid fixture with unchanged workflow behavior.

## Linked Issues or Issue Description

Refs #19602 and #19596.

**What happened?**

The merged workflow target fixture contains two copies of IMAGE_ONLY and
TESTOPS_REQUESTED. TypeScript reports TS1117 for both keys. The
Typecheck + Release Registry job for #19624 reproduces the failure.

**Expected behavior**

The fixture supplies each default once and all nine workflow target
cases pass.

**Steps to reproduce**

Typecheck the current fixture at base a7d3d4b78c. The compiler reports
two TS1117 errors at line 22. Remove the second copy and run the same
check plus the actual workflow target suite.

**Paperclip version or commit**

a7d3d4b78c.

## What Changed

- Remove the duplicate environment properties from the workflow target
test.
- Keep one IMAGE_ONLY=false and TESTOPS_REQUESTED=false default.
Production workflow code does not change.

## Verification

- RED: `node node_modules/typescript/bin/tsc --ignoreConfig --noEmit
--skipLibCheck --target ES2022 --module NodeNext --moduleResolution
NodeNext --types node --typeRoots server/node_modules/@types
tests/runner-e2e/workflow-target.test.ts` reports exactly two TS1117
duplicate-key errors before the edit.
- GREEN: the exact typecheck command exits 0 after the edit.
- GREEN: `pnpm exec vitest run --config
tests/runner-e2e/vitest.config.ts
tests/runner-e2e/workflow-target.test.ts`: all 9 actual cases pass in
486 ms.
- Repository agent preflight completed in 54.716 seconds. Local
environment: macOS arm64, Node 26.4.0, pinned pnpm 9.15.4, Vitest 5.0.3.
- Prior broad CI attempt: one unrelated Slack owner deadline assertion
failed in server7, with 2,791 other assertions passing in that shard.
The unchanged focused real-DB case passes. A separate diagnostic proves
the actual owner retains the intent at due-minus-1ms and settles it at
due. A bounded real Node timer sample observed 2 of 200 callbacks fire
1ms early. The original CI did not record the precise clock, so this
supports a timer-boundary explanation without proving that exact
timestamp. One authorized replay of only the failed server7 job is
running; the original failure remains recorded. No Slack source or
assertion changed.
- Exact tested head: `aebe1d76ba38bca8fbbc9864004871f662d365e4`. [Fresh
current-head
CI](https://github.com/paperclipai/paperclip/actions/runs/42297519184)
is running. Greptile is 5/5 at this exact head with no review threads.
No manual deployment is part of this PR.

## Risks

- Low risk. The removed keys have the same literal values as the
retained keys. This changes no production code or workflow behavior.

## Model Used

- OpenAI Codex (GPT-6 family), with code execution and repository tools.
The runtime does not expose the exact serving model identifier or
context window size.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] 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 16:00:26 -06:00
ee6c101854 Reject failover backups behind the accepted event cursor (#19613)
A verified failover backup can still trail events already accepted by the controller. Compare the backup produced cursor with the registered controller state, or the supported local journal before registration, and reject stale files before remote sync. Keep the permanent replay-conflict classification ahead of generic Runner errors.

Validation: 1,192 native session and execution-driver tests and 10 workflow tests pass locally, together with the maintained low-memory Runner test project typecheck and fresh server declarations. All applicable checks on cd8c9cd07c are green (56 success, three skipped); exact-head Apex review is 5/5 with no unresolved findings. One e2e shard was retried after GitHub reported lost self-hosted runner communication, and the same-head rerun passed. No acknowledgement was lowered, provider replay authorized, or recovery files discarded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Paperclip <Paperclip-Paperclip@users.noreply.github.com>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-11-06 13:55:43 -08:00
DottaandPaperclip 44c60c7221 Verify default skill sync through two real cold sleep cycles (#19191)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Automatic skill sources check upstream every five minutes.
> - The domain deadline contract can hand future work to a durable wake
alarm.
> - A saved deadline or a worker unit test does not prove that a stopped
tenant resumes the real producer.
> - This pull request checks the default cadence through the actual app
and a matching service checkout.
> - It checks two cold starts and three distinct persisted skill
revisions with real clocks.
> - The benefit is repeatable local evidence without changing production
timing or authority.

## Linked Issues or Issue Description

Refs: #19001, #17008, #17923.

This is a test and documentation follow-up to #19001. #19001 is now
merged; this PR targets master. Feature code remains in that merged
foundation. #17008 covers the earlier source worker change. #17923
covers separate skill-cache retention; this PR does not change that
behavior.

A scoped search of public PRs and issues found no duplicate
default-cadence cold-cycle acceptance test. No matching public issue was
found. The enhancement fields follow.

**Pre-submission checklist**

This improves checks for an existing feature. It adds no product
endpoint or execution authority. The public related-work search is
complete.

**What existing behavior does this improve?**

Verification of automatic skill-source updates across a durable idle
handoff and cold restart.

**Subsystem affected**

`server/` test fixtures and development documentation.

**Current behavior**

Domain tests cover claims, access and future deadlines. They do not
prove the full default-300-second path from ordinary source attachment
through signed tenant reports, durable alarms, stopped compute and a
changed persisted skill.

**Proposed behavior**

An opt-in launcher checks bootstrap first, then two full real-clock
cycles. Ordinary CI can run the transport controls without the host
fixture. The launcher-specific worker assertion runs only when prewire
is requested, and fails closed in that lane.

**Reason and benefit**

The test can detect an early refresh, a lost deadline, a stale proof, an
ACK after physical stop, a failed cold restart or a producer that does
not publish changed contents.

**Breaking changes**

None to product APIs, schema, cadence, authorization, claims or retry
policy. Existing maintenance-helper defaults stay unchanged.

**Additional context**

The opt-in host flow needs an authorized matching service checkout, its
normal compiled acceptance bridge, Node 22 and two owned PostgreSQL test
databases. The standalone controls do not need that service or
PostgreSQL. Roadmap review places this under existing Skills Manager and
Cloud deployment verification, not a new core feature.

## What Changed

- Add an opt-in default-cadence acceptance test and checked-in launcher.
- Reuse real server startup, managed tenant authority and the ordinary
project attachment route. Control only the remote Git snapshot boundary;
keep actual authorization, scanning, claims and publication.
- Use actual signed tenant routes and the matching service's gate, sleep
controller and PostgreSQL wake-alarm store. Stop and restart real app
children through a controlled local provider.
- Preload the Node transport guard before coordinator, worker and child
module work. Check worker-local and child PID, thread and source-hash
receipts. Keep caught unexpected transport attempts fatal.
- Require both persisted refresh times and actual upstream handler
timestamps to be at or after each original deadline.
- Join physical callbacks during bridge close, stop a late wake-created
child, and preserve the original cleanup budget. Add a real
IPC/HTTP-child counterfactual.
- Accept valid owned HTTP options paths without allowing Unix sockets or
unknown origins. Remove the unused App helper export.
- Add finite transport and launcher controls. Defer only the
launcher-specific parent assertion in vanilla CI. Keep all subprocess
controls default-runnable.
- Add optional child entry, preload and bounded pool settings to the
existing test helper. Preserve its defaults and the real observer test
body.
- Remove added explanatory comments under the contribution policy.
Preserve executable syntax, directives and literals.
- Document pinned setup, fresh owned databases, bootstrap and full
two-cycle commands in `doc/DEVELOPING.md`.

The diff against the intended base contains nine test/helper/launcher
files and this one document. It adds no production feature code or Cloud
source to the App repository.

## Verification

The latest two-file CI repair preserves both original failed jobs. A
newer master workflow introduced two required step inputs absent from
its extracted-step test environment; the faithful false/false inputs now
pass all five existing target-authorization cases, after an actual
missing-input counterfactual failed. A controlled socket case also
reproduces legal reuse of the released runner port; the fixture now
holds that reservation until the preview binds, releases it in finally,
and asserts distinct ports. All original retry/result/accounting
assertions and timeouts stay unchanged. The original Linux listener
owner remains unknown; the control proves this fixture collision
mechanism, not that original attribution. Fresh current-head full CI and
complete reviews are pending.


Current merge-integration candidate:
`f6f686103b7bfd0a4ab83085634c622af3896ecb`, a normal merge after #19001
landed, including actual master
`1c9767d323cf872a266962b9e6b4bd5cbcb57f1c`. All nine acceptance code
files are byte-identical to148a82e5; both parents' documentation
additions survive. Every other path equals actual master. The only
conflict was the old foundation's startup block; current master was
retained in full. Fresh exact-head CI and complete reviews are pending.
The local measurements below belong to their explicitly named revisions;
no fresh full614-second run is claimed.

Current candidate: `148a82e54c502759785407357001c6b50c1439dd`, normally
carrying the current base `8c912e1ba4fc66397a7341ade59373af8075068f`.
All nine acceptance code blobs remain identical to qualified repair
`e092746f4da181c8414c63be71474619a84b97e7`; documentation retains both
parents. The matching built service for the fresh bootstrap is
`389878364b42dd129835d263f91882c5774d5123`, using Node 22.23.4. The App
uses Node 24.22.2 and pnpm 9.15.4.

Use the exact controlled setup and commands in
[doc/DEVELOPING.md](doc/DEVELOPING.md#controlled-default-cadence-sleep-acceptance).
Fresh owned databases and the matching compiled service are
prerequisites; the launcher does not create a cluster or build
dependencies.

- **Current affected semantic check passed**, using actual native
TypeScript 7 and the current generated CI options/references (0.640
seconds). Normal declaration references passed in 11.726 seconds, with
source-matched SDK/shared/kernel/Runner prerequisites and the frozen
current lockfile.
- **Current guarded controls: 44 passed**, plus the same single explicit
real-PG observer deferral, in 4.790 seconds. The executing worker
checked its own global PID/thread/preload source hash and matching
receipt. Guard hash:
`7daf9ae7d2f26e8fcd389b1d560fac1d4e218a8304a2c58e49f646af108165bc`. No
unexpected attempts or PG sockets occurred. Vanilla controls on the
identical repair files passed 22 cases with one explicit launcher-parent
assertion deferral (2.044 seconds).
- **Real cleanup counterfactual:** unchanged old helper failed the
actual endpoint assertion in 1.568 seconds: its late HTTP child remained
reachable after close returned. The same fixed-helper case passed in
1.485 seconds: close waited, the child exited, the port was unreachable
and no IPC error occurred. The isolated driver records only the known
legacy closed-IPC error long enough to inspect cleanup; current helper
requires zero such errors. All recorded processes and ports were
independently checked empty.
- **Fresh current bootstrap passed once**, 16.159 seconds wall / 14.12
seconds Vitest / 12.237 seconds body. The ordinary enabled producer
persisted useful revision 1, cleared its lease and retained exactly
300,000 ms between success and original next deadline. A signed
current-generation report produced the matching
generation/operation-lock SQL ACK before physical child stop (ACK
06:54:02.020 UTC; stop signal 06:54:02.025 UTC). The alarm retained the
original D and D-120 eligibility. Both fixture databases ended with zero
sessions; the coordinator independently audited and normally removed
them. This bootstrap does not execute a due wake or the new v2/v3 timing
assertions.
- **Historical full two-cycle result remains 1/1 passed in 614.213
seconds wall**, at measured App
`74f7c012ca17ad0d7302d76403f5033affb1430b` and service
`2cb54f904031dcf69a32902cd853b43f74d6f301`. Two new processes published
distinct immutable v2/v3 at original D+115 ms and D+98 ms. Exact signed
SQL ACKs preceded both physical stops; real SQL D-120 claims caused both
cold starts. Features, claims and original business deadlines stayed
unchanged.
- Both historical 10.002-second quiet windows kept app SQL, upstream and
outgoing HTTP counts unchanged. Two awake PG sockets remained. Quiet
counts do not mean all connections closed.
- The stronger timing checks are additional assertions, not
retroactively executed results. Offline inspection of the original
receipts confirms persisted successes at D+115/+98 ms and earliest
upstream handler entries at D+44/+30 ms; all five requests in each
refresh group started after D. The exact new pre-sleep request counter
and dispatch-before-handler time were not independently recorded
historically.
- Source review found 19 critical automatic Git producer/runtime files
identical to the measured App. The current active cache helper change
does not run on automatic Git publication and adds no pool. The matching
service carries unchanged measured helper/sleep/controller/alarm seams
through its normal master merge. The current bootstrap and focused
controls qualify the changed cleanup/guard and parent integration. No
fresh current-head 614-second pass or whole-tree identity is claimed.
- Runtime is serialized: worker 1, per-pool limits at most 4, configured
App potential 10 + shared service pool 2 + one observer against server
max 40. Current bootstrap point samples observed at most 3 owned clients
plus the observer; those samples are not a continuous aggregate peak.
App/service guard peaks were 2 each, with zero remaining sockets at
cleanup.

Failures remain accounted for. The first historical bootstrap cleanup
masked an unknown primary error. Later attempts exposed missing
fixture-owned domain metadata and lost origin snapshots; test-only
ownership/cleanup corrections preserved the original assertions and
timeouts. The new causal driver first lacked a final observation, then
encountered sandbox bind EPERM, then exposed the old closed-IPC error;
only the final real endpoint failure above is labeled the cleanup
counterfactual. An offline dependency miss and a generator-cleared
ignored compiler config were retained and corrected without weakening
compiler options. The former inherited policy failure (397 findings) is
resolved by the genuine current-master base/scanner, with zero findings
and seven scanner tests; no unrelated UI site was edited and the gate
was not relaxed.

Historical real TCP observer coverage passed 1/1 in 3.688 seconds.
Comment-only and opt-in-selection carries remain documented in prior
evidence; self-hash changes are metadata exceptions with fresh
executing-worker checks.

The full workspace typecheck, test, build and browser commands were not
run locally for this test-only slice. The current base passes its full
exact-head CI and Greptile 5/5; this leaf now has all 56 exact-head
check runs terminal (54 success, one neutral, one skip), the additional
legacy security status passing, complete current Greptile 5/5 and
Ponytail 5/5 reviews, and all four review threads resolved across all
pages. No live provider or deployed host/database suspension was
exercised.
## Risks

- The host test requires a matching service checkout and compiled
bridge. An unrelated service revision is not a valid substitute.
- Controlled localhost Git, S3 and physical process operations do not
qualify deployed Railway or Neon suspension, external permissions or
provider uncertainty.
- The Node guard does not prove native subprocess transport isolation.
Only finite local Git metadata and Node version commands were needed.
- Two short SQL/HTTP quiet windows do not establish strict 90-second
host acceptance. Separate plugin-host LISTEN, MCP wake and other domain
gaps remain open.
- Explicit test URLs are destructive fixture inputs. Use fresh, owned
`_test` databases. The launcher does not start a cluster or choose a
database implicitly.
- Passing CI and review do not qualify deployed suspension; the current
proof boundaries above still apply.

> For core feature work, check `ROADMAP.md` first and discuss it in
`#dev` before opening the PR. This PR adds verification to the existing
feature in its base.

## Model Used

OpenAI `gpt-6.1-sol` assisted with implementation, repository review and
local code execution. OpenAI Codex, based on GPT-6, performed managing
review and integration. The context-window sizes and exact served
backend snapshots 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
- [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 — fresh merge-head gates pending
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups —
fresh merge-head review pending
- [x] I will address all Greptile and reviewer comments before
requesting merge


### Current master reconciliation (2026-11-06)

Merged actual master `e28158b353fd274f0322be76d62cd223e0df599f`
normally. All nine qualified acceptance code files and the held-port
repair remain byte-identical. Upstream independently landed the workflow
input fix in #19602; the fixture now uses that exact upstream version
once, avoiding duplicate object keys in a squash merge. All other master
changes are preserved. The earlier two-cycle local evidence remains
source-specific; no fresh 614-second run or deployed suspension is
claimed. This new head requires fresh CI and bot reviews.

### Actual combined-tree typecheck failure and repair

CI job `128156887548` failed with TS1117 at workflow-target.test.ts:22:
two independently landed master fixes declared the same `IMAGE_ONLY` and
`TESTOPS_REQUESTED` keys. The complete original log is retained. A
further normal master merge now removes the duplicate declarations,
retaining exactly one false/false pair and every original assertion. All
nine qualified acceptance code paths and the held-port repair remain
byte-identical; unrelated master changes are preserved. This is a
concrete compiler failure and source repair, with fresh exact-head
CI/reviews required.

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 15:55:05 -06:00
e482f0f632 fix(server): shed only retryable object-journal reads under load (#19176)
Client polling can exhaust the shared object journal queue and reject log persistence. Bound ordinary object-log and trace reads, keep persistence and internal verification queued, and return retryable 503 responses when reads are busy. Leave feedback exports unsent until every log page is available.

Validation: 72 latest-master affected tests and 10 workflow tests pass locally, with the workflow fixture passing strict TypeScript checks. The preceding broader run passed 280 server tests with two opt-in S3 skips. All applicable checks on d0a937768d are green (56 success, three skipped); exact-head Apex review is 5/5 with no unresolved findings. Background persistence still has no fixed queue bound during a prolonged storage outage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-11-06 13:47:25 -08:00
DottaandClaude Opus 5.5 8d525dba7d ci: run the agent build cache tests in CI (#19598)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents work in many fresh worktrees. PR #19555 added
`scripts/agent-preflight.mjs` and a shared content-keyed build cache
(`scripts/agent-build-cache.mjs`).
> - That PR also added 6 tests in
`scripts/__tests__/agent-build-cache.test.mjs`.
> - CI runs script tests only from the explicit file list in the root
`test:release-registry` script. The new test file is not in that list,
so CI does not run it.
> - A regression in the cache (for example, a scrub that keeps a
workspace crate, or a partial entry that reads back as complete) can
then merge without a failing check.
> - This pull request adds the test file to that list.
> - The benefit is that every PR now runs the build cache tests in the
required `pr-trusted` typecheck job.

## Linked Issues or Issue Description

Refs #19555

**What is the problem?**
The 6 build cache tests from #19555 do not run in CI. The
`test:release-registry` script lists script test files one by one, and
the new file is not listed.

**What is the expected behavior?**
CI runs the build cache tests on every PR, and a failure blocks the
merge.

**Why does it matter?**
The build cache decides which build output a fresh worktree reuses. An
untested regression can give agents stale or partial builds.

## What Changed

- Add `scripts/__tests__/agent-build-cache.test.mjs` to the
`test:release-registry` script in the root `package.json`. CI runs this
script in the "Verify release registry test coverage" step of
`.github/workflows/pr-trusted.yml`.
- No test changes. The tests are portable to Linux. Only `cloneInto`
touches copy-on-write. On macOS it tries `cp -cRp` (APFS clone) and then
`cp -Rp`. On Linux it tries `cp -a --reflink=auto` and then `cp -a`. The
clone test checks the copied content and that no temporary sibling is
left. It does not check block sharing, so it gives the same proof on a
filesystem without reflink. No test needs a skip.

**This PR changes `package.json`, so it needs CODEOWNER review.**

## Verification

- `pnpm run test:release-registry` on macOS (APFS), Node 24: 267 tests,
267 pass, 0 skipped. All 6 build cache tests are in the output.
- `node --test scripts/__tests__/agent-build-cache.test.mjs` in a
`node:24` Linux container (GNU coreutils `cp` 9.1, overlayfs with no
reflink support): 6 tests, 6 pass, 0 skipped.
- In CI: look for the 6 test names in the "Verify release registry test
coverage" step log of the `pr-trusted` typecheck job.

## Risks

- Low risk. The change adds about 0.2 s to one CI step.
- If a future CI runner has no GNU `cp` with `--reflink`, `cloneInto`
falls back to `cp -a`. The test still runs on that runner.

## Model Used

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

## 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 15:37:43 -06:00
DottaandPaperclip c09fb60ca6 fix(recovery): retain worker-stop audit in preparation retries (#19612)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Recovery must preserve saved files before it admits another
execution.
> - A failed preparation can need a verified stop of its previous
workspace worker.
> - Recovery keeps that verified stop receipt in the run profile for
audit.
> - The preparation retry guard treats the retained receipt as unknown
execution evidence.
> - This pull request recognizes a valid same-run audit receipt while it
retains all current execution and file checks.
> - A saved Retry can then continue once after recovery completes.

## Linked Issues or Issue Description

**What happened?**

A native preparation failed before provider dispatch. The recovery
worker verified that the previous workspace worker had stopped and saved
its files. The same HTTP Retry stayed deferred because the run profile
retained `nativeWorkspaceOwnerStopProof`.

**Expected behavior**

The saved Retry should continue once after all preparation, stop, file,
ownership, and authorization checks pass. The old run and its audit
receipt should remain unchanged.

**Steps to reproduce**

1. Fail native preparation with a retained remote workspace and no
provider activity.
2. Let the recovery worker acquire the old workspace owner through
verified platform stop evidence.
3. Submit one HTTP Retry and let preservation complete.
4. Observe that the old guard retains the retry hold even after
saved-file verification passes.

**Paperclip version or commit**

Reproduced against `d728be8fbf`. Related work: #18908, #19021, #19026,
and #19466. This change covers the retained workspace-worker stop audit
left by a later repair.

## What Changed

- Add a typed reader for a host-persisted same-run worker-stop audit
receipt. Keep signature and expiry validation at ownership acquisition.
- Recognize only that validated audit field in failed-preparation
settlement. Keep unknown profiles and active owners blocked.
- Exercise real preservation, HTTP Retry delivery, native dispatch,
saved files, accepted results, and task completion. Add malformed,
foreign-run, unstopped-worker, and active-owner cases.
- Document the distinction between retained audit evidence and current
execution authority.

## Verification

- Failing before the fix: the current-worker preservation case passed;
the stopped-worker case produced zero successor runs after the same
Retry, where one was expected.
- Passing after the fix: 47 recovery cases, 10 native
dispatch/preservation cases, and 6 platform-proof tests. The two
preservation success cases overlap between the first two groups.
- Server typecheck passed all seven layers after the final test changes.
The Runner and SDK dependencies were built in this isolated worktree.
- Final comment-free commit `44f8f33f8d917dddb51852283a479786102eab4d`:
a fresh combined run passed all 61 affected cases in 67.92s. Fresh
server typecheck passed all seven layers.
- The earlier `95d0376c` combined run passed 60 cases and hit one
`socket hang up` in the existing `catalog_provider_warning` HTTP test.
That case passed in a bounded isolated run (12.48s), and the final
61-case combined run passed. The socket failure remains recorded as an
intermittent HTTP harness limitation. No retry or weaker assertion was
added.
- Final commit `44f8f33f8d917dddb51852283a479786102eab4d` completed all
required CI gates: 57 checks succeeded, 3 intentional skips, and the
advisory Paperclip review is neutral with a substantive 5/5 verdict.
Greptile is 5/5 and there are no unresolved review threads. [Final CI
run](https://github.com/paperclipai/paperclip/actions/runs/42293625696).
The follow-up commit only removes six new code-comment lines to follow
CONTRIBUTING.
- The native provider and signed platform API are controlled test
boundaries. The tests use real PostgreSQL, recovery, ownership, file
persistence, retry admission, dispatch, and finalization services.
Deployed recovery remains unverified until normal rollout.

## Risks

- The main risk is mistaking an audit receipt for current stop
authority. The reader checks its exact shape and run binding. The
receipt cannot replace stopped leases, saved-file receipts, settled
preparation, current authorization, or the absence of an active owner or
provider evidence.
- The negative matrix runs with the audit field present. Provider
activity, unknown usage, bad file receipts, Stop, actor changes, and
unsettled leases still block continuation.
- No schema or migration change is required.

## Model Used

OpenAI GPT-6 in Codex. The exact deployment model ID and context-window
size are not exposed in this session. Used reasoning, code editing,
shell execution, and test tools.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [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 15:36:23 -06:00
DottaandClaude Opus 5.5 02a2299173 feat(codex): default Codex to gpt-6.1-sol, half the gpt-5.6-sol price (#19623)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Codex agents run through Paperclip Runner or the legacy
`codex_local` harness, and the runner E2E suites run paid Codex cells
> - The shared Codex default and many selections preferred `gpt-5.6-sol`
> - The reviewed OpenAI rate card in
`server/src/services/codex-pricing.ts` (`openai-standard-2026-09-30`)
prices `gpt-6.1-sol` at $2 input / $0.10 cached / $10 output per 1M
tokens, and `gpt-5.6-sol` at $4 / $0.40 / $20
> - So every default run and every paid Codex eval cell cost twice as
much as necessary
> - This pull request moves every place that prefers or selects
`gpt-5.6-sol` to `gpt-6.1-sol`, and keeps historical facts, rate cards
and compatibility fallbacks on `gpt-5.6-sol`
> - The benefit is half the token price for new default Codex agents and
for the Codex E2E and TestOps cells

## Linked Issues or Issue Description

**What existing behavior does this improve?**
The default Codex model for new agents and the Codex model that the
runner E2E suites and TestOps plans select.

**Subsystem affected**
Adapters (`adapter-utils`, `codex-local`), server native runtime
fallback, UI AI connection pools, Paperclip Runner devtools and live
evals, runner E2E catalog and TestOps mappings.

**Current behavior**
New Codex agents without an explicit model store `gpt-5.6-sol`. The
runner E2E `runner-codex` profile, the Reliability `queue-steer` plan
cell, the Product regression TestOps mapping and the ACPX Codex
qualification selection all use `gpt-5.6-sol`.

**Proposed behavior**
They use `gpt-6.1-sol`. Existing agents that store a model keep it.
`gpt-5.6-sol` stays selectable.

**Reason and benefit**
`gpt-6.1-sol` costs half as much per token on the in-repo reviewed rate
card. The current OpenAI pricing page (retrieved 2026-11-06) shows the
same short-context rates and also lists long-context rates of $4 / $0.20
/ $15 for `gpt-6.1-sol` against $8 / $0.80 / $30 for `gpt-5.6-sol`.

**Breaking changes**
None for stored configurations. See Risks.

## What Changed

Changed defaults and selections (all now `gpt-6.1-sol`):

- `PAPERCLIP_RUNNER_DEFAULT_MODELS.codex` in
`packages/adapter-utils/src/paperclip-runner-permissions.ts`. This also
changes `DEFAULT_CODEX_LOCAL_MODEL`,
`resolvePaperclipRunnerModel("codex", …)`, runner config normalization,
`codex_local` → Paperclip Runner conversion, the UI new-agent and
agent-config defaults, the composer default model and the public MCP
effort lookup.
- `codex-local` `models`: the 5.6 entry is now the literal
`gpt-5.6-sol`, because it used to be `DEFAULT_CODEX_LOCAL_MODEL`. The
list order and contents do not change.
- AI connection pool: a new OpenAI member profile uses
`PAPERCLIP_RUNNER_DEFAULT_MODELS.codex` instead of a hard-coded
`gpt-5.6-sol`.
- Paperclip Runner devtools ACPX Codex default and preset, the
`runner-codex-sol` local provider smoke, and the `acpx-codex-sol` live
workflow candidate.
- `tests/acpx-qualification-models.ts` Codex selection (runner E2E and
runner acceptance ACPX Codex profiles).
- Runner E2E: the `runner-codex` profile follows the default, so all
native Codex cells move. This includes the `runner-codex` cells among
the 52 standard `everyday-workflows` cells and the seven Codex
reliability cells. `runner-codex-mini` stays on `gpt-5.4-mini`. The
Reliability plan `queue-steer` `steeringProfile` is now
`['runner-codex', 'gpt-6.1-sol']`. Its executionId
(`everyday-workflows.runner-codex.daytona.queue-steer`), its fullName
and its TestOps case identity do not change. No separate profile is
necessary.
- `product-regression-testops.mjs`: the declared model for every
`runner-codex` case is now `gpt-6.1-sol` (it was already that for
`configured-repository`).
- `tests/runner-e2e/installed-cli.ts`: the credential-free installed
startup proof opens a session on the default model.
- `tests/runner-e2e/scheduled-prices.json`: add a `gpt-6.1-sol` price
(`openai-standard-2026-11-06`, including 30m/12h cache writes and
long-context rates from the OpenAI pricing page). The weekly scheduled
Codex cells need an exact price for their model, or admission fails
closed.
- Storybook: the composer model picker fixtures, the connection pool
stories and host, and the agent settings preview show `gpt-6.1-sol` as
the default.
- Docs: `doc/DEVELOPING.md` (the Codex conversion default) and
`tests/runner-e2e/EVERYDAY-WORKFLOWS.md` (local matrix, reliability
extras, `queue-steer` row).
- Tests that assert the default now expect `gpt-6.1-sol`. New
assertions: the default has the `0.159.0` CLI floor, the compatibility
fallback is floor-free, the `queue-steer` plan cell runs `runner-codex`
on `gpt-6.1-sol`, and all native Codex everyday cells (standard and
reliability) use `gpt-6.1-sol`.

Deliberately kept on `gpt-5.6-sol`:

- Rate cards: `server/src/services/codex-pricing.ts` and
`packages/paperclip-runner/src/evals/model-pricing.ts`. Old receipts
need them. (The runner eval rate card has no `gpt-6.1-sol` entry. An
unpriced candidate is reported as unpriced, not as zero. I did not add a
rate there because that card has its own version and source.)
- The remote Codex CLI compatibility fallback. A new
`CODEX_LOCAL_COMPATIBILITY_FALLBACK_MODEL = "gpt-5.6-sol"` replaces the
default in `codexModelFallbackCandidates`. `gpt-6.1-sol` needs Codex
0.159.0 or newer, so it cannot be the floor-free last resort. Fallback
behavior is unchanged.
- `tests/runner-e2e/provider-usage.ts`: the implicit request
cache-policy attribution stays qualified only for `gpt-5.6-sol`. I did
not verify that policy for `gpt-6.1-sol`. A `gpt-6.1-sol` response that
omits the write-TTL split keeps its cache-write cost unknown, which is
the existing behavior for unqualified models.
- `tests/runner-e2e/scheduled-measurements.json`: historical
`gpt-5.6-sol` measurements. They no longer match the current suite
definitions, so the forecast reports them as historical only. The
forecast tests now build their synthetic cohorts on the current
definition hash.
- Model catalogs that list `gpt-5.6-sol` as available (`codex-local`,
`cursor-local`, `opencode-local`, Speko voice diagnostics, Pi picker
fixture).
- The `gpt-5.6` → `gpt-5.6-sol` alias for old stored configs.
- Dated or evidence docs: `doc/plans/*`, `doc/adapter-model-audit-*`,
`QUALIFICATION-*`, `doc/connections/MEMORY.md`,
`doc/connections/TASK-REVIEWS-VERIFICATION.md`,
`tests/runner-e2e/CHAT-FEATURES.md`, the Slack eval evidence, and the
S49 soak signature text (the soak failure happened on `gpt-5.6-sol`).
- Test fixtures that parse, price, migrate or round-trip an existing
`gpt-5.6-sol` value (Rust runner tests, driver tests, pricing tests,
stored-config tests, stories that show an existing agent).

## Verification

- `npx vitest run` on every server, UI, adapter-utils and codex-local
test file that mentions `gpt-5.6-sol` or the default, plus all of
`packages/adapter-utils` and `packages/adapters/codex-local`: 5,483
passed. Three timeouts under load (`sandbox-managed-runtime`,
`acp.quota`) passed when rerun.
- `packages/paperclip-runner`: `vitest run` for `src/eval`, `src/evals`,
`src/drivers/acpx`, `src/drivers/codex`, `src/cli`, `src/contracts`,
`src/backends`: 2,641 passed.
- `node --test tests/runner-e2e/reliability-testops.test.mjs
tests/runner-e2e/product-regression-testops.test.mjs`: 28 passed.
- `node tests/runner-e2e/run-unit.mjs`: all model, catalog, reliability,
scheduled and TestOps tests pass. Failures remain only in
`copilot-protection-flow`, `denied-target-isolation-flow`,
`pi-controls-flow`, `workflow-target` and `unit-launcher`. These five
also fail on unmodified master in this local environment (filesystem
watch and git environment), and they do not touch models.
- `vitest run --config tests/runner-acceptance/vitest.config.ts`: 19
passed.
- `pnpm test:e2e:runner -- --list --suite everyday-workflows --group
reliability`: the seven `everyday-workflows.runner-codex.daytona.<case>`
cells show `gpt-6.1-sol` with `OPENAI_API_KEY,DAYTONA_API_KEY`.
`--matrix-json` gives 16 reliability cells with the same credentials as
before. `--list --all` shows no `gpt-5.6-sol` cell.
- Typecheck: `adapter-utils`, `adapter-codex-local`, `server`, `ui`,
`paperclip-runner` (TypeScript), and `pnpm test:e2e:runner:typecheck`.
- `pnpm check:token-gates`: clean.
- Not run: paid cells, Rust tests (no Rust change), the browser suites,
and the full `pnpm -r typecheck` / `pnpm build` (CI runs them).

## Risks

- Agents without an explicit model change model on their next save or
normalization, and Paperclip Runner rows with a blank model use
`gpt-6.1-sol` at execution. Stored models, including `gpt-5.6-sol`, are
never rewritten. No migration is necessary.
- `gpt-6.1-sol` needs Codex CLI 0.159.0 or newer with ChatGPT sign-in.
The runner pins `@openai/codex@0.160.0`. On a remote target with an
older image CLI, the existing compatibility check falls back to
`gpt-6-sol` and then `gpt-5.6-sol`, and that probe now runs for
default-model agents too. A legacy `codex_local` agent created with the
new default on a host with an older Codex CLI gets the existing
actionable "upgrade Codex" Test result.
- Qualification: the native Codex profile uses `adapter_constant` model
qualification, so it follows the default. No qualification gate is
dropped. The ACPX Codex qualification selection moves to `gpt-6.1-sol`.
Its next qualification run uses the new model. The provider-usage
cache-policy attribution stays limited to `gpt-5.6-sol`.
- Runner E2E history: the Codex cells change their suite definition
hashes, so earlier `gpt-5.6-sol` results are a separate cohort and do
not count as current.
- The `gpt-6.1-sol` scheduled price uses the file's existing `expiresAt`
(2026-11-19). It needs the same review as the other entries before then.
- No `.github/` change. The Reliability queue-steer cell keeps the
`runner-codex` profile id, so the existing workflow credential and setup
steps still match.

> 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`, running in
Claude Code with tool use (shell, file edits, web fetch of the OpenAI
pricing page).

## 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: #19604 (commits the Reliability TestOps identities; this PR
changes only `steeringProfile` in the same file).

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 15:35:34 -06:00
Nicky LeachandClaude Opus 5.5 05a19768ae fix(runner): require normalizedSessionId when validating PRP events (#19615)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The Paperclip Runner reports native session activity as PRP events
> - `validatePrpEvent` checks an event against the published event
schemas and then returns it as a `PrpEvent`
> - The `PrpEvent` type requires `normalizedSessionId`, but the schemas
leave it optional, so the validator accepts events that the type forbids
> - Every producer sets the field, and the consumers compare it to the
expected session
> - This pull request makes `validatePrpEvent` reject an event without
`normalizedSessionId`
> - The benefit is that a validated event now always matches its type

## Linked Issues or Issue Description

No issue exists. #18839 listed this under its product questions and did
not change it. #19495 fixes the other Runner contract types from that
list.

**What existing behavior does this improve?**

`validatePrpEvent` in
`packages/paperclip-runner/src/protocol/replay-contract.ts` returns `{
ok: true, event: PrpEvent }` for an event without `normalizedSessionId`.

**Current behavior**

- The TypeScript `PrpEvent` type requires the field. The Rust event
structs also require it (`normalized_session_id: String`).
- The published schemas (`protocol/schemas/event*.schema.json`) leave it
optional.
- Validated events go into event arrays, reducers and the control-plane
callback, and all of them expect the field. A plain type change to
optional shows 12 such places in the Runner.

**Proposed behavior**

After the schema check passes, `validatePrpEvent` rejects an event that
has no `normalizedSessionId`. The issue has the code `schema_validation`
and the path `/normalizedSessionId`. The schema still checks the field's
type and length when the field is present.

**Reason and benefit**

The validator, the type and both producers then agree. The published
schemas, their manifest hashes and the generated standalone validators
do not change.

**Breaking changes**

An event without `normalizedSessionId` now fails validation. No producer
in this repository creates such an event. The replay fixtures and golden
snapshots include the field.

## What Changed

- `packages/paperclip-runner/src/protocol/replay-contract.ts`:
`validatePrpEvent` requires `normalizedSessionId` after the schema
check.
- `packages/paperclip-runner/src/protocol/replay-contract.test.ts`: new
test. A fixture event passes, and the same event without
`normalizedSessionId` fails with the new issue.

## Verification

- Runner, in `packages/paperclip-runner` after `pnpm run
test:typescript:prep`:
- `pnpm exec vitest run` (the full Runner TypeScript suite): 299 of 309
files passed, and 16 tests in 9 files failed.
- I ran those 9 files again by themselves: 231 of 233 tests passed. The
2 remaining failures were 20-second timeouts in
`src/drivers/cursor-cloud/artifact-delivery.test.ts` and
`src/drivers/cursor-cloud/cursor-cloud-driver.test.ts`.
- I ran those 2 files again by themselves: all 117 tests passed. They
also pass with the master validator. So all 16 failures were load
timeouts, not results of this change.
- `pnpm run check:replay-goldens`: "Replay golden snapshots and parity
summaries are current."
- `tsc --noEmit` passes for the Runner `tsconfig.json`,
`tsconfig.surfaces.json` and `tsconfig.test.json`.
- Server:
  - `cd server && pnpm typecheck`: passes.
- I ran the 29 server test files that import `acknowledged-native-stop`
or `native-session-executor`. 27 files passed.
- `dot-runner.test.ts` failed at first because `cargo` was not on PATH.
With `cargo` on PATH, it passes.
- `heartbeat-process-recovery.test.ts` fails on my machine with
timeouts. These failures are not caused by this change: two of the
failing tests fail in the same way when I use the master
`replay-contract.ts`. No failure output mentions PRP events or
`schema_validation`. CI runs this file.
- `pnpm verify` selects a broad run for this change (exit 2). Required
CI supplies that coverage.

## Risks

- Medium-low risk. This is a runtime change to a protocol validator. The
server validates stored events again (`native-session-executor.ts`,
`acknowledged-native-stop.ts`). A stored event without the field would
now fail validation. The producers that wrote those events always set
the field, so I expect no such rows.
- The Rust event structs already reject an event without the field.

## 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
- [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:30:26 -08:00
Nicky LeachandClaude Opus 5.5 a7d3d4b78c fix(runner): rotate local and external authority through one resumable path (#19547)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The Paperclip Runner moves a native session from one run to the next
with an authority rotation, for Codex and for every ACPX provider
> - A rotation archives the prior run's controller state under
`authority-epochs/` and archives the Runner state, locally or on an
external Runner host
> - If the run fails after the rotation but before the new Runner
starts, retry preparation restores the prior run's controller checkpoint
into the session root
> - The retry then finds an active `control-plane` next to the archive
marker and fails with `native_runner_authority_archive_conflict`. Every
later run for the session fails the same way
> - The local and external rotations were two copies of the same steps,
with different retry rules, and the sandbox archive script had a third
set of rules
> - This pull request gives every provider and Runner placement one
resumable rotation path, with the same retry rules for controller state
and Runner state
> - The benefit is that one failed startup no longer makes a native
session unusable

## Linked Issues or Issue Description

No public issue exists. Bug description:

**What happened?**
A warm Codex session on an external (plugin) Runner failed its first
attempt after the authority rotation finished. The two automatic retries
both failed with `native_session_recovery_failed: Error:
native_runner_authority_archive_conflict`, and the run stopped. On disk,
the session root had
`authority-epochs/epoch-<key>/control-plane/control-plane-state.json`
and an active `control-plane/control-plane-state.json` with the same
identity (in this case, also the same sha256). The remote Launcher had
already archived the Runner state under the same key.

**Expected behavior**
A retry resumes the interrupted rotation. The rotation already supports
this when only the archive marker exists.

**Steps to reproduce**
1. Run a native Codex or ACPX session on an external Runner.
2. Start a new run for that session. Make the new Runner fail to start
after the rotation finishes.
3. Let the run retry. Retry preparation (`restoreWarmRunnerdController`
/ `restorePerTurnRunnerdController` →
`restoreNativeControllerProjections`) restores the prior run's
controller checkpoint into the session root.
4. The retry fails with `native_runner_authority_archive_conflict`. All
later runs for the session also fail.

The new `authority rotation after the prior controller state is
restored` tests reproduce steps 2–4 directly.

**Paperclip version or commit**
`master` at 35280516c0.

**Deployment mode**
Hosted, with a remote (plugin) Runner environment.

**Agent adapter(s) involved**
Codex (native Paperclip Runner). ACPX providers use the same rotation
code.

## What Changed

- **One rotation path.** `rotateAuthorityEpoch` replaces
`rotateLocalAuthorityEpoch` and `rotateExternalAuthorityEpoch`. The only
difference between Runner placements is a `RunnerStateArchive` (`read`,
`archive`):
- `localRunnerStateArchive(root)` for Runner state under `<root>/runner`
  - `#externalRunnerStateArchive()` for a plugin task or a sandbox host
- **One caller.** The transport chooses the rotation source (the live
control plane, or the latest archive), then makes one call. Locally
owned state is still quarantined on failure.
- **Committed rotation, normalized once.** Retry preparation restores
the prior run's controller checkpoint, but an earlier attempt may
already have archived that authority and its Runner state.
`takeCommittedRotation` runs right after the transport reads the control
plane. When the epoch archive for the active copy's exact identity
exists, it removes the restored copy and continues on the archive-only
path. Every later check (the warm-handoff evidence read, the
source-attachment settlement read, and the rotation) then sees the state
the rotation committed, instead of reading retired live Runner state. A
copy that still holds an unfinished warm transition, staged attachment,
or pending `run.attach` fails closed.
- **Restored controller state.** When the epoch archive exists, an
active control plane with the same schema and the exact archived
identity is a copy of authority that the rotation already retired. The
rotation removes it and resumes from the archived snapshot. This
compares identity, not bytes, because a checkpoint restore serializes
the state again and drops fields such as `ackedSourceSeq`. Any other
active state still fails closed.
- **Duplicate Runner state.** Local Runner state now follows the
computer Launcher's rules: move it, accept it already archived, drop a
byte-identical live copy beside the archive, or fail closed. The local
archive also checks that the live state is suspended before it moves it.
- **Sandbox archive script.** The script is now
`REMOTE_RUNNER_AUTHORITY_ARCHIVE_SCRIPT` and follows the same rules. It
uses `cmp` for the duplicate check and fails closed when `cmp` is not
available.
- **Sandbox host preparation after a resumed rotation.** A resumed
rotation archives the remote Runner state without preparing the host
first. When `archiveExternalRunnerState` succeeds, the server now
records the archived snapshot path. The launch's host preparation
(`inspectRemoteHarnessState`) validates that snapshot instead of the
missing live `runner-state.json`, with the same provider-state and
identity checks. Only these host preparation checks use the archived
snapshot; close checkpointing and backup-restore verification always
inspect the live `runner-state.json`. The SSH staging check treats it as
present, so retired state is not staged back.
- **Host preparation.** The external archive prepares its host only when
it reads live Runner state. Before, only one of the two caller branches
prepared the host. A resumed rotation finds the state already archived,
and preparing again could stage it a second time.

## Verification

- `npx vitest run src/live/runnerd-codex-transport.test.ts -t
"rotation"` in `packages/paperclip-runner`: 16 passed.
- External and local rotations with a byte-identical, a re-serialized,
and an other-authority restored control plane. The first two resume from
the archived snapshot. The third fails closed and keeps the copy.
- Local rotation with an identical duplicate Runner state (resumes) and
a different duplicate (fails closed and keeps it).
- The existing crash-before and crash-after remote archive retry cases.
- Before the source change, the byte-identical external case failed with
`native_runner_authority_archive_conflict`.
- `npx vitest run src/live/runnerd-codex-transport.test.ts -t "transport
retry after a committed rotation"` in `packages/paperclip-runner`: 4
passed. These go through the transport's real start path. A completed
warm handoff and a source attachment hook now resume from the archive;
before the normalization, both read the archived live Runner state and
failed. An unfinished warm transition and a pending attachment fail
closed; before, the pending attachment was silently dropped.
- `npx vitest run
src/services/native-runtime/native-session-executor.test.ts -t "remote
authority archiv"` in `server`: 6 passed. Five new cases run the real
archive script with `sh` against temporary files: a new archive, an
existing archive, an identical duplicate, a different duplicate, and no
state. The existing mocked archive test also passes.
- `npx vitest run
src/services/native-runtime/native-session-executor.test.ts -t "resumed
rotation archived"` in `server`: 2 passed. A resumed sandbox with a
retained backup goes through the real archive script and host
preparation. It failed with `runner_harness_state_mismatch:
resumed_unavailable` before the host preparation fix. A settled close
checkpoint with no live Runner state then fails closed with
`runner_remote_checkpoint_incomplete`, rather than checking the archived
snapshot. Without a completed rotation, a missing live state still fails
closed.
- `pnpm run typecheck:typescript` in `packages/paperclip-runner` and
`node scripts/typecheck-layers.mjs` in `server`: pass.
- The full `runnerd-codex-transport*.test.ts`,
`cursor-cloud-runner-launch.test.ts`, and adoption test files fail on
the same 110 tests with and without this change. These tests need the
built `paperclip-runnerd` binary, which needs `cargo`. `cargo` is not
installed locally, so CI covers them.

## Risks

- The change is only on the rotation path. A rotation that has no
archive marker behaves as before: it reads and checks live Runner state,
then archives the control plane.
- A rotation removes an active control plane only when an archive for
that exact identity exists and the active copy has the same schema and
identity. A new run's authority has a different identity, so it cannot
match.
- On a resume, the rotation now uses the archived controller snapshot,
not the caller's copy. The archive is the exact pre-rotation state. A
checkpoint restore can omit fields from it.
- An external Runner whose options define `readRunnerState` and
`archiveExternalRunnerState` but not `prepareExternalRunnerState` can
now rotate a live control plane. Before, that branch required the
prepare hook. The plugin and sandbox drivers define all three.
- The sandbox duplicate check needs `cmp`. A host without `cmp` fails
closed as before.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code with tool use (shell, file edits, test runs). It diagnosed the
failure from the run events, the server logs, and the on-disk controller
and Launcher state, then wrote the fix and the tests.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [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:27:55 -08:00
DottaandClaude Opus 5.5 8479929f62 test(restart-chaos): add an RC-06 mask, wave spend and replay masks (#19559)
## 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 during real agent tasks and grades each task on rules
R1–R6
> - One known product bug (RC-06) leaves a completed run "awaiting
accounting". The company hard stop then refuses all new work, so one
stuck row fails every later task in the wave
> - That masks every other restart bug in the wave. Waves also had no
record of what they spent
> - This pull request adds an opt-in RC-06 mask, records wave spend, and
lets a replay keep or add the mask
> - The benefit is that chaos waves can find new restart bugs while
RC-06 is open, and stay inside a known spend

## Linked Issues or Issue Description

Refs #19477 (the Phase 1 harness this extends).

**What existing behavior does this improve?**
Restart chaos waves. One stuck accounting row (RC-06) hard-stops the
isolated Chaos Lab company. All later tasks then rest blocked "over
budget", and a Retry is refused with "Company cannot start work while
completed runs await accounting". Two waves went 2/6 and 0/6 for this
one reason.

**Subsystem affected**
Test harness only: `tests/restart-chaos/` (config, seed, scheduler,
ledger, supervisor, main, README).

**Current behavior**
- No way to see past RC-06 without disabling the budget.
- Wave spend is not recorded.
- `--replay` rejected any mask flag, and it was not documented that the
saved plan carries the mask.

**Proposed behavior**
- `--rc06-masked` keeps the budget amount and turns off only the hard
stop on the isolated company. Masked tasks are labeled in the ledger and
summary.
- Each wave records its spend.
- A replay keeps the source plan's masks and may add the RC-06 mask.

## What Changed

- `--rc06-masked` (wave mode): after the company is created, the seed
upserts the company budget policy with the same amount and
`hardStopEnabled: false`. It writes `seed_mask_rc06` to the timeline.
The plan stores `options.masks = ["rc06"]`.
- The accounting gate in `budgets.ts` (`observedBlocks`) applies only
when `hardStopEnabled` is true, so the mask lifts it. No product code
changes.
- Ledger lines carry `masks`. `SUMMARY.md` labels the cell and the
latest-wave row `rc06-masked`.
- Wave spend: before the sweep, `main.ts` reads
`/api/companies/:id/costs/summary?period=all` (each wave owns its
company), writes `wave_spend` to the timeline and prints it. If the wave
stops early, cleanup records it once, best effort, before the server
stops.
- Load: #19513 already records `loadAvg1m` on each fault receipt. This
branch first added the same value under another name, and the rebase
removed it. The README now documents `loadAvg1m` and the load-suspect
threshold.
- Replay: `--replay <wave>/<task>` keeps the masks saved in the source
`plan.json`. `--replay … --rc06-masked` adds the RC-06 mask. A replay
never drops a mask.
- README documents the flag, spend, load and the replay rule.
- New unit tests: replay keeps saved masks and the replay flag adds the
mask; the masked seed posts the company policy with the same amount and
`hardStopEnabled: false` and records `seed_mask_rc06`, and an unmasked
seed makes no policy call.

## Verification

- `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts`:
71/71 pass (after rebase onto master with #19513).
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`: pass.
- Live: 4 masked local waves ran with this code (seeds 3307, 4409, 5501,
6619; Codex, ACPX Claude and OpenCode).
- `seed_mask_rc06` appeared and no task was blocked by the accounting
gate, even though `pendingRunCount` was 1–8 at wave end.
  - `wave_spend` was recorded each time (38¢–127¢ per wave).
  - The 1-minute load average was recorded on every fault (15–58).
- Before the mask, the same harness gave 2/6 and 0/6, every failure
RC-06. With the mask, the waves found other restart failures, all since
fixed on master: RC-14, RC-16, RC-20 and RC-21.

## Risks

- Low risk. Test harness only, and the mask is opt-in.
- A masked wave has no product hard stop. The harness still records
spend for each wave, and the operator must watch it. The mask applies
only to the isolated, temporary Chaos Lab company. It never touches
another instance.
- Masked results do not count as RC-06 coverage. The ledger labels them
so they are not mixed with unmasked cells.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with
tool use (shell, file edit). 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 15:27:12 -06:00
DottaandPaperclip 6148fbd31f feat(ui): distinguish GitHub resources in inline Markdown links (#19476)
## Thinking Path

> - Paperclip helps people manage AI agents and their work.
> - Task conversations and documents use the shared Markdown renderer.
> - GitHub repository, pull request, and issue links currently use the
same icon.
> - Long pasted URLs make these references hard to scan in a
conversation.
> - This change identifies each resource from its URL and shows a
compact label with a distinct icon.
> - The exact destination and custom link text stay intact. Connected
status data keeps its existing behavior.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

Inline GitHub links in rendered Markdown.

**Current behavior**

Repositories, pull requests, and issues all use the GitHub mark. Pasted
resource links show the full URL.

**Proposed behavior**

Pull requests use the pull request icon. Issues use the issue icon.
Repositories and other GitHub pages keep the brand mark. Pasted resource
URLs show `owner/repository#number` or `owner/repository`. Custom labels
keep their text and formatting.

**Reason and benefit**

Readers can identify GitHub resource types and scan references in a
narrow conversation. Full URLs, query strings, and comment anchors stay
available through the link destination. Link rendering makes no remote
metadata requests.

**Breaking changes**

The visible text of a bare GitHub resource URL becomes compact. The
destination does not change. No API, database, or connection changes are
required.

Searches found no duplicate work for GitHub inline resource
presentation. Related work: [same-origin Markdown
routing](https://github.com/paperclipai/paperclip/pull/17864). This
change preserves that route handling.

## What Changed

- Add a pure GitHub URL classifier with exact host checks and resource
labels.
- Move ordinary URL presentation into `MarkdownUrlLink`. Keep internal
references, workspace links, custom resolvers, and connected metadata in
their existing order.
- Add separate icons and accessible resource names. Preserve authored
Markdown titles and formatted labels.
- Add Playground, Narrow, and Streaming Storybook examples that mount
`MarkdownBody`.
- Add parser and renderer tests, eight Chromium browser cases, and the
browser command in the development guide.
- Consider Open Graph loading. Use the existing connected metadata path
for remote status and titles. Add no arbitrary page fetches.

## Verification

- Focused Vitest coverage: 129 passing tests across nine files for
GitHub parsing, Markdown rendering, connected metadata, document
rendering, and workspace-file rendering.
- `pnpm check:token-gates`.
- `pnpm --filter @paperclipai/ui typecheck`.
- `pnpm --filter @paperclipai/ui build`.
- `pnpm build-storybook`.
- Storybook catalog generation/check and 30 catalog/map tests.
- Site-route regression: Stars, Education, and enterprise pages keep
general GitHub branding and their full URL text. Three parser cases
failed before the fix; parser and renderer cases now pass.
- Streaming regression: three resource cases failed before the fix and
pass after it. The fix preserves the generated word span and custom
formatted labels.
- Chromium: `STORYBOOK_TEST_BASE_URL=http://localhost:6106 pnpm exec
playwright test inline-links.spec.ts
--config=tests/storybook-visual/playwright.config.ts --project=stories`.
- Browser scope: eight passing cases for ordinary/streaming modes,
dark/light at desktop and 390px mobile, completed play assertions, exact
comment destinations, safe external attributes, keyboard focus,
wrapping, and no GitHub requests. Screenshots are saved as task
artifacts; no review images are committed.
- The first browser attempt in the resumed environment could not launch
Chromium because its executable was missing. The browser was installed,
and all eight cases then passed.
- The browser cases use synthetic text and one connected metadata
fixture. They do not prove provider authentication or server
persistence. No visual baseline comparison is claimed; the repository
baseline manifest is unpublished.
- Initial typecheck found two unsupported story assertion options and an
unbuilt SDK dependency in the fresh checkout. The options were removed
and the SDK was built before the successful retry.
- `pnpm verify` returned 2 because the new browser spec requires broad
CI coverage. Focused tests and the browser suite supply local evidence.
Required CI supplies broad coverage.

## Risks

- Compact labels hide URL detail from the visible line. The exact href
and default tooltip preserve that detail.
- GitHub site routes can change. Unknown paths retain the general GitHub
treatment. Lookalike hosts and credential-bearing URLs do not get GitHub
resource treatment.
- Connected external-object status and title rendering keeps priority
over URL-derived presentation.

## Model Used

OpenAI Codex, GPT-6 (`gpt-6` family), with reasoning, tool use, browser
checks, and code execution. The exact deployment ID and context window
are not exposed in this run.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 15:26:10 -06:00
DottaandPaperclip b6e9615aed feat(ci): request advisory merge review after PR verification (#19480)
## Thinking Path

> - Paperclip helps people manage AI agents and their work.
> - Pull requests already run CI and automated code review.
> - A completed check does not start a final advisory merge review.
> - A private routine webhook can connect these two steps.
> - The webhook credential must stay outside PR code and public logs.
> - This pull request adds an optional notification from trusted branch
code.
> - The routine records a recommendation. A person keeps merge control.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

It connects successful PR verification to an advisory Paperclip routine.

**Subsystem affected**

GitHub Actions and the existing routine webhook API.

**Current behavior**

A PR can finish verification without asking a routine for a final merge
recommendation.

**Proposed behavior**

A trusted workflow reads the current PR and its complete check list. It
sends the PR identity to a private webhook when required checks pass and
other reported checks finish successfully, as skipped, or as neutral.
The receiving routine runs the configured merge review skill and records
its decision on its Paperclip task.

**Reason and benefit**

This gives maintainers an advisory decision after verification. It does
not grant merge control to the routine.

**Breaking changes**

None. Missing secrets or delivery errors leave verification results
unchanged.

Searched open and closed PRs and issues for related work. No duplicate
notification workflow was found. Related API work: #18058 and #18581.
This change uses the routine API and does not alter chat credential
setup.

## What Changed

- Add a default-branch workflow for PR workflow completions, external
checks, and commit statuses.
- Use read permissions and a GitHub environment restricted to master for
the private URL and bearer token.
- Require successful `ci / verify`, `ci / e2e`, and additional
branch-rule checks. Read every status page and refresh the PR before
delivery.
- Use a stable idempotency key for each PR, head, and base. Reject stale
events and changed candidates.
- Verify the two core check runs belong to the same real GitHub Actions
suite for the unchanged PR caller and master-owned reusable CI. Reject
forged names, renamed callers, and incomplete file listings.
- Keep HTTP redirects disabled, bound request time, and print fixed
delivery states only.
- Add regression checks and document private setup, pause, rotation, and
acceptance steps.
- Grant commit-status read access. Add a separate PR API probe with the
same read permissions and the real Actions token. The probe has no
private environment or webhook secrets and sends no notification.

## Verification

- Pass: `node --test
.github/scripts/tests/notify-pr-merge-review.test.mjs` (21 tests after
the security and token-permission fixes).
- Pass: actionlint v1.7.12 on `.github/workflows/pr-merge-review.yml`.
- Pass: live GitHub query of a current PR and its complete status list.
This found a GraphQL field mismatch during development. The corrected
query passed with 62 checks.
- Pass: the configured bearer webhook returned `test_received` with
`routineStarted=false` in setup mode. Readback confirmed an active
routine and environment secrets restricted to the master branch.
- Not run: `pnpm verify --explain` could not start because this checkout
has no installed workspace dependencies. Required CI will supply broad
coverage.
- The first quality-gate run rejected a CI-only title because this
change adds a script. The feature title passed the later gate.
- The security review found that PR-controlled check names could forge
eligibility. A focused regression failed before the fix and passed after
suite, run, caller, and reusable-workflow checks were added. The
corrected provenance query also passed against a completed historical CI
run without sending a webhook.
- Earlier CI at ec2c2dc failed in `workspace-recovery.test.ts` with a
generated company-prefix collision, in
`idle-domain-deadlines-postgres.test.ts` with a deadline snapshot
mismatch and timeout, and in `CoordinatorTasks.test.tsx` with a route
query mismatch. The browser shard also recorded a CSS preload test
failure before cancellation. These failures remain recorded in [run
42245040952](https://github.com/paperclipai/paperclip/actions/runs/42245040952).
This PR does not edit those production paths. Current master is merged
and fresh CI is required; the earlier failures are not claimed resolved.
- The repeated browser, general-server, and UI lanes passed at
715cee88ce. The earlier failures remain historical; their causes are not
all established. The Runner package lane was still running before the
next fix.
- A reviewer found missing `statuses: read`, which the broader local
token did not expose. The permission regression failed before the fix
and passed after it. The new read-only API probe will test the query
with the actual Actions token.
- Pass at c3c84394b7: the real Actions-token API probe, typecheck,
build, browser aggregate, security scan, and Greptile 5/5. No unresolved
review threads remain.
- The only remaining CI failure is a 15 second timeout in the existing
retained-native-session-cleanup-work backlog test (server shard 6/12).
This file and its production implementation are unchanged from the
previous candidate whose lane passed. The one scoped rerun passed at the
same head, and the required verify aggregate now passes. The timeout
remains in [run 42250393645, attempt
1](https://github.com/paperclipai/paperclip/actions/runs/42250393645/attempts/1);
its cause is not established. This is recovery evidence, not a claimed
root-cause fix.
- A nonblocking review suggestion to defer branch-rule lookup until a PR
is matched remains a minor efficiency follow-up. It is not a correctness
or security finding.
- Pending: a real notification and resulting routine task after this
workflow reaches master. The setup test did not start a PR review.

## Risks

- This workflow uses secrets. Keep the environment restricted to the
master branch. It runs only trusted branch code and does not read PR
artifacts, caches, or executable files.
- GitHub event timing and check visibility can race. Delivery is best
effort. A missed event can omit a review; it cannot change CI results or
merge a PR.
- Optional skipped and neutral checks permit an assessment. They do not
prove that a check ran. Required checks must succeed. The routine must
assess unavailable scanner coverage and other evidence gaps before its
recommendation.
- Add any new PR verification workflow name to the completion trigger
list.
- PRs that change the primary CI caller, `.github/workflows/pr.yml`,
need a manual advisory review until their caller change reaches master.
This prevents a PR from replacing verification with a forged result.
- This PR changes CI configuration. CODEOWNERS review and normal merge
rules still apply.

## Model Used

OpenAI GPT-6 family through Codex. The exact deployment model ID and
context window are not exposed to this run. Used reasoning, 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 available version and
capability details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change and contains no internal
Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 15:25:57 -06:00
2e35730637 feat(chat): receive sealed Slack events through the current SDK owner (#19019)
## Thinking Path

> - Paperclip manages AI agents and their work.
> - Slack connectors turn authenticated messages into company work.
> - Direct callbacks need an awake tenant.
> - Cloud can retain authenticated asynchronous events until the tenant
wakes.
> - Replay must preserve the original callback proof and current
endpoint authority.
> - Configuration changes must retain registration responsibility
through failures.
> - This PR enrolls verified Slack identities and drains sealed events
through the normal SDK owner.

## Linked Issues or Issue Description

**Subsystem affected**

Server chat connectors and signed Cloud connector delivery.

**Problem or motivation**

Direct Slack Events API delivery depends on an awake tenant. Broker
outages, lost responses and configuration changes must preserve delivery
and registration responsibility. Enrollment failures must also leave
other connector events able to drain.

**Proposed solution**

Stage nonsecret enrollment or retirement intent inside the local
configuration transaction. Let the existing connection-event owner
perform broker requests and retries. Replay sealed events through the
current Slack SDK with their original authentication evidence.

**Alternatives considered**

An early acknowledgment for every callback changes command and modal
semantics. A second delivery ledger duplicates existing ownership.
Reusing a new stack identity to retire old registrations crosses an
authorization boundary.

A companion Cloud broker change is required. Related setup fixes: Refs
#18837, Refs #18878. Kickoff searches found no competing async
enrollment consumer. The ROADMAP's connector setup and access controls
stay in place.

## What Changed

- Persist enrollment and credential-free retirement intent before local
configuration commits. Broker failure does not make a completed pause or
removal look unsuccessful.
- Bind company, endpoint, connection, public ID, generation and
authorized actor. Preserve lost-response responsibility and recover only
the supported same-instance configuration outage.
- Permit an explicit move to a new authorized stack only after confirmed
old retirement. Keep the old identity until the new broker receipt
matches the current intent and generation.
- Show permanent signing-credential failures as attention. Preserve
transient retries and reject uncertain inbound authentication before SDK
admission.
- Drain events and acknowledgments before enrollment reconciliation.
Persist a bounded retry on the exact busy intent so other due endpoints
can enroll or retire. Preserve current custody under concurrent changes.
- Stage retirement in the Apps removal transaction too. Archive the
endpoint and connection, retain the original subject and instance, then
revoke credentials. Retirement does not need the deleted Slack
credential.
- Acknowledge a malformed sealed Slack receipt as invalid once. Keep
actual broker outages on the retry path.
- Run the real configuration, SDK ingress, generation fences and public
Apps removal paths in the default SQL suite. Keep Vitest transport
helpers outside production services.
- Preserve original callback bytes, signature, time and public URL
evidence. Use the existing transactional chat-delivery ledger for
redelivery.
- Provide a checked-in prewire guard and launcher using the current Node
executable. Add broker, worker, SDK, SQL and controlled-process
acceptance.
- Stabilize AI account summaries by ID and grant ID. This fixes a CI
migration-repeat equality failure without changing SQL, authorization or
reconnect behavior.
- Open the four configured database connections before the enrollment
test freezes its clock. Keep all 35 original assertions and all
timeouts.
- Measure status-card dispatch entry before its mock does database work.
Keep the original strict 500 ms bound and check any still-armed earlier
deadline. Bound that fixture's pool to four connections.

## Verification

- Candidate: `54fa701d41b4ced62f4ff338d39b946015670ae3`. Normal merges
preserve master `47027e7` and all owned changes. A read-only merge
assessment against fetched master `cb5d7e44` is conflict-free. The only
overlapping file has separate AI test additions. The branch has not
merged that later master; fresh remote checks must validate the current
PR test tree.
- At this head, normal server production typechecking and declaration
emission passed in 35.24s. The affected actual CI test shard passed in
6.27s. Other unchanged CI layers, 92 affected pure controls and 12 guard
controls retain their source-matched results.
- All **15 default real-PostgreSQL cases passed in 34.66s**, with no
skips or filtering. They cover normal configure and SDK ingress,
rollback, stale generations, malformed receipts, busy-lease fairness,
concurrent intent custody, unknown errors, and public Apps removal.
Removal tests check actual credential revocation, retained retirement
through outage and repeat removal, one ordinary retry and confirmed
retirement, transaction rollback and foreign-company refusal.
- The public removal counterfactual failed before the fix: archival and
credential revocation passed, but the inactive intent was absent. The
same test source passed after the staging call. Separate counterfactuals
also reproduced busy-endpoint starvation and malformed-receipt retries.
Original failures remain retained.
- The paired broker/SDK/process journey passed in 31.98s at ancestor
`0324c011`. It preserves callback proof, lost-ACK redelivery, exactly
one comment, one delivery and one successful controlled process. Its
test, producer, SDK, guard and runtime bytes are unchanged. The new
public Apps archive branch is not called by that journey; its evidence
carries on that checked basis.
- The new SQL cases need no companion Cloud checkout. Normal CI runs
them with the repository fixture. For an existing dedicated PostgreSQL
fixture, set `PAPERCLIP_SLACK_OWNER_TEST_DATABASE_URL` and
`PAPERCLIP_TEST_POSTGRES_ADMIN_URL`, then run `node
server/scripts/test-slack-cloud-owner.mjs` with Node24. The optional
paired launcher still requires the normally built companion Cloud
checkout.
- Every pool was bounded to four or fewer connections. The final 15-case
run sampled three runtime clients plus one observer. The independent
final audit found zero clients and all five recorded processes absent.
Earlier closing-socket observations are kept separate from final
cleanup.
- Earlier unrelated CI failures are documented: [enrollment
timeout](https://github.com/paperclipai/paperclip/actions/runs/41944373937/job/126973483754)
and [status-card observation
bound](https://github.com/paperclipai/paperclip/actions/runs/41944373937/job/126973483843).
The enrollment test retains all 35 assertions and its original timeouts.
A controlled connection probe established the frozen-clock mechanism;
the original remote access history remains unknown. The status-card
fixture measures dispatch entry with the original strict 500ms bound. A
real delayed-dispatch control fails that bound.
- Full local workspace typecheck, build and test inventory was not run
because its embedded-cluster and wider resource paths exceed this local
lease. Exact-head remote CI now passes: 54 successful checks, three
skips and Ponytail's neutral check, with aggregate verification green
and the legacy security context independently confirmed successful. Both
complete bot reviews are 5/5, with all 13 paginated review threads
resolved. Seven execution jobs originally reported runner shutdown; one
diagnostic rerun recovered them and the dependent aggregate check.
Original logs remain retained, and the underlying shutdown cause is
unknown. This is recovery evidence, not a product-fix claim.
- Provider responses are controlled. This proves local SQL, broker, SDK
and process behavior. It does not prove live Slack, agent tools,
synchronous callbacks or deployed suspension.

## Risks

The companion broker is required. Outage or conflicting authority can
leave explicit pending or attention responsibility. A move between
stacks requires confirmation from the old authorized owner first.
Synchronous commands, actions, modal validation, options and challenges
retain their deadlines and endpoint blocker. This slice grants no full
Slack endpoint sleep exemption. No deployed suspension or savings claim
is made.

## Model Used

OpenAI `gpt-6.1-sol` workers through Codex, with reasoning, repository
tools and code execution. The served backend revision and context limit
are not exposed. A GPT-6-family coordinator performed technical review
and integration.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Codex <noreply@openai.com>
2026-11-06 15:22:51 -06:00
DottaandClaude Opus 5.5 38068de553 chore(evals): commit the Reliability TestOps plan and case identities (#19604)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Runner E2E cases run from Allure TestOps plans through
`runner-full-stack-e2e.yml`
> - #19549 added the "Reliability — staging soak" plan. Its selection
fails closed until real TestOps identities are committed
> - Registration run
https://github.com/paperclipai/paperclip/actions/runs/42289820474 on
master (02214b1a3c) completed. It created plan 135 with cases 885–891
and assigned the plan to the standing GitHub job 166
> - This pull request commits those identities into
`reliability-testops.mjs`
> - The benefit is that a TestOps launch of plan 135 now selects the
seven reliability cells

## Linked Issues or Issue Description

Refs #19549
Refs #19601

**What happened?**
The Reliability TestOps plan exists in TestOps, but the repository still
had an empty `reliabilityRegistration`. A launch therefore failed closed
with `product_regression_testops_reliability_unregistered`.

**Expected behavior**
A launch of plan 135 maps each TestOps case ID to its runner E2E cell
and runs it.

**Steps to reproduce**
1. Launch plan 135 from TestOps before this change.
2. The selection fails with `reliability_unregistered`.

## What Changed

- `tests/runner-e2e/reliability-testops.mjs`: set
`reliabilityRegistration` to plan 135 and the seven case IDs from the
registration receipt.

## Verification

- `node --test tests/runner-e2e/reliability-testops.test.mjs
tests/runner-e2e/product-regression-testops.test.mjs`: 28/28 pass.
- `registeredReliabilityCases()` with plan-2 IDs reserved resolves all
seven cells: 885 interrupt-redirect, 886 reassign-midrun, 887
three-turn-revise, 888 long-silent-command, 889 tree-pause-release, 890
queue-steer (Codex), 891 queued-across-restart.
- The registration receipt from run 42289820474 reports `complete: true`
with assignment and parameters created.

## Risks

- Low risk. The change only adds identities. A launch of plan 135 runs
seven paid cells under the existing admission and budget caps. Nothing
runs per PR or on a schedule.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code with tool use
(shell, GitHub CLI), standard context.

## 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 15:22:02 -06:00
DottaandClaude Opus 5.5 876a2e4857 fix(recovery): recover a prior boot's unstarted run at lease expiry (#19578)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - After a server restart, a task must continue automatically and
promptly when that is safe
> - A user clicks Retry, and the server is killed before the new run
starts a process
> - Recovery leaves that run `running` until the orphan reap's
five-minute stale-age threshold passes
> - The user sees nothing happen for about five minutes
> - This pull request recovers that run when its controller lease from
the earlier boot expires
> - The benefit is that the automatic retry starts about one lease
period after the kill, not five minutes later

## Linked Issues or Issue Description

**What happened?**
A Retry admitted a legacy run. The server was killed before the run
recorded a process. The run kept a controller lease from the killed boot
and `executionStage: preparing`. After the restart, live recovery
skipped the run until `updatedAt` was five minutes old
(`server/src/services/heartbeat/recovery.ts`, `staleThresholdMs ?? 5 *
60_000`). Restart-chaos measured 5 m 01 s from run creation to
`process_lost` and the automatic retry, twice (4 m 20 s after the server
was ready).

**Expected behavior**
Recovery should claim the run as soon as the earlier boot's lease
expires. The lease is the only liveness proof for a legacy controller,
and no process was ever recorded.

**Paperclip version or commit**
`master` at `71313207eb`.

**Steps to reproduce**
Click Retry on an interrupted task, then SIGKILL the server within about
one second. Observe the retried run stay `running` for about five
minutes.

## What Changed

- `recoverActiveExecutions` skips the stale-age gate for a legacy
`running` run when all of these are true: its controller lease is
expired (checked by the existing live-lease gate), the lease belongs to
an earlier boot (`controllerBootId !== legacyControllerBootId`), the run
is still `preparing` (no adapter dispatch), and the run has no recorded
`processPid` or `processGroupId`.
- The stage and process fields committed with the revocation are checked
again. If the old controller dispatched before its lease expired, the
run keeps the age guard.
- The existing revocation (`revokeExpiredLegacyControllerWithState`)
still requires the expired boot ID, so a concurrent renewal wins. The
existing `restartInterruptedBeforeDispatch` rule still decides the
single process-loss retry.
- `doc/execution-semantics.md` describes the rule in section 10.
- Two regressions in `server/src/__tests__/live-recovery-work.test.ts`
(embedded Postgres, real residual-recovery workers).

## Verification

- Red before the fix: `recovers a fresh run from a prior boot at lease
expiry ...` failed with `expected 'running' to be 'failed'`.
- Green after the fix: `pnpm exec vitest run
src/__tests__/live-recovery-work.test.ts -t "stale-age|fresh foreign
controller|suppressed live runs"` passes 4/4. The run fails
`process_lost` with `executionRecovery.providerWorkStarted: false`
within seconds of lease expiry, and exactly one retry runs to
`succeeded`.
- Race case: a DB trigger makes the row committed with the revocation
say `dispatching` while the scan saw `preparing`. The run stays
`running`, and no retry is created.
- Guard cases: a run with an expired lease of the current boot, a run
from an earlier boot with a recorded process, and a run from an earlier
boot that was `dispatching` (for example a remote HTTP adapter) all stay
`running` after four minutes.
- Related suites: `heartbeat-process-recovery`,
`heartbeat-observed-workspace-preservation`,
`native-startup-cleanup-recovery`,
`services/heartbeat/recovery.test.ts`, full `live-recovery-work`.
- Live K8 probe (restart-chaos supervisor, local instance, ACPX Claude
agent): the first run fails with a provider error, the probe clicks
Retry, and the server is SIGKILLed 100 ms later, before the Retry run
records a process. The probe then measures how long the Retry run stays
`running`.

| Build | Retry run created to `process_lost` | Server ready to
`process_lost` | Automatic retry run |
  | --- | --- | --- | --- |
  | Base (`71313207eb`) | 300.1 s | 293.5 s | started at 5 m 00 s |
  | This PR | 60.1 s | 54.5 s | started 0.1 s after `process_lost` |

The claim now happens when the killed boot's 60-second controller lease
expires. In this probe the restart took about 6 s, so the claim came
about 54 s after ready. In the restart-chaos waves, kill to ready took
about 41 s, so the claim would come about 20 s after ready.
- A chaos wave with the `retry` trigger (`w-20261106T195715-s2727`,
branch #19513 merged locally) missed the trigger: no Retry was offered
because recovery completed the task automatically (claude t01 PASS). The
probe above makes the Retry-then-kill timing deterministic.

## Risks

- Low risk. The change only removes an age wait. Ownership is still
proved by the expired lease of another boot. A rolling deploy or hot
restart whose earlier instance is still alive keeps renewing its lease,
so the run is not touched.
- Runs with a recorded process, and dispatched runs, keep the
five-minute guard. A process on another host or a remote request is not
inferred dead.
- Native runs are not changed. Their ownership is in
`native_run_finalizations` and startup native recovery.

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

## Model Used

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

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [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 15:17:58 -06:00
DottaandClaude Opus 5.5 da9a4cb96c fix(native): retire a finished local Runner that stopped before suspending (#19597)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The native Runner path runs each per-turn agent turn through a
runnerd process, and the next turn must prove that the previous Runner
is finished before it takes over the saved session root
> - Under load, a local turn can have its result accepted while its
`runner.suspend` command does not finish, so the Runner's own receipt
stays unsuspended
> - The retirement proof that handles the same case for sandbox Runners
refused every local Runner with `target_not_sandbox_runner`
> - So the next turn failed with `runner_state_identity_mismatch:
prior_owner_terminal_state_indeterminate` and its session root was
quarantined
> - This pull request extends the retirement to a local Runner when the
prior Runner process is proven stopped by exact PID and start time
> - The benefit is that a follow-up turn (for example, after a user
answers a question) continues instead of failing

## Linked Issues or Issue Description

Refs #19525 (keeps the accepted result in the same incident; this is the
next run).

**What happened?**

In the runner E2E cell
`local-session-integrity.runner-acpx-claude.local.structured-question-restart-resume`,
the first turn asked a question and its result was accepted. The Runner
then logged `runner did not prove durable suspension before checkpoint`
(`runner.suspend` pending, Runner lifecycle `ready`). After a server
restart and the answer, the next run failed with
`runner_state_identity_mismatch:
prior_owner_terminal_state_indeterminate;
retirement_refused=target_not_sandbox_runner`, and its root was
quarantined.

**Expected behavior**

When the prior local Runner is provably stopped and every other
retirement proof holds, the next run retires that owner as a continuity
break and continues. If the stop cannot be proven, the run keeps the
named rejection.

**Steps to reproduce**

1. Run a local per-turn native turn whose `runner.suspend` does not
finish after its result is accepted (host load average ~40).
2. Start the next turn for the same session.
3. See `retirement_refused=target_not_sandbox_runner`.

## What Changed

- `server/src/services/native-runtime/native-restart-recovery.ts`: add
`proveNativeLocalRunnerStopped`. It uses the existing restart-recovery
checks: the recorded Runner PID is dead or now belongs to a process with
a different start time; its recorded process group is not alive; each
recorded provider process is dead or recycled. A missing PID, start time
or process group, a live recorded process, or a start time that cannot
be compared is not proof. When settlement already cleared the run's
process fields, the saved `native.local_process_stopped` receipt is the
Runner proof. Extract `nativeProviderProcessIdentities` (checkpoint plus
run events) so restart recovery and this proof read provider PIDs the
same way.
- `server/src/services/native-runtime/native-session-executor.ts`
(`retireFinishedPerTurnRunnerdOwner`, `readSettledRunnerdProjection`): a
local target is now eligible. For a local Runner, the projection accepts
the Runner's own unsuspended receipt for the same identity and
unfinished Runner lifecycle commands (`runner.suspend`, `runner.drain`,
`runner.shutdown`) only. All other proofs are unchanged: committed
result, released lease, no recovery state, stopped controller, delivered
terminal, other commands complete, tool inputs answered, quarantine
copies. The process proof runs before retirement and again just before
the archive.
- `doc/execution-semantics.md`: describe the local case.
- Tests: seven integration cases in
`native-recovered-owner-followup.test.ts` and ten unit cases in
`native-restart-recovery.test.ts`.

## Verification

- Red before the fix: the four new integration cases failed with
`retirement_refused=target_not_sandbox_runner`, which is the incident
error.
- Green after the fix: `pnpm exec vitest run
src/services/native-runtime/native-recovered-owner-followup.test.ts`
35/35. The new cases prove that a stopped Runner is retired (root
archived under `continuity-breaks`, no quarantine), and that a live
recorded Runner (`runner_process_alive`), a missing identity
(`runner_process_identity_missing`), and a pending non-lifecycle command
(`journal_commands_unsettled(turn.start.pending)`) keep the rejection.
All existing sandbox cases still pass, including "keeps an unsuspended
Runner receipt decisive" for sandbox targets.
- `native-restart-recovery.test.ts` unit cases: recycled PID, live
process, unknown start time, live group, live provider, missing PID or
start time.
- `native-session-executor.test.ts` + `native-restart-recovery.test.ts`:
1190 pass. `native-runner-restart-recovery.integration.test.ts` +
`native-restart-recovery-isolation.test.ts` (shared provider-PID
extraction): 81 pass. `tsc --noEmit -p server/tsconfig.json`: pass.
- Live: `pnpm test:e2e:runner -- --max-parallel 1
--max-automatic-retries 0 --id
local-session-integrity.runner-acpx-claude.local.structured-question-restart-resume`,
3 runs at eec57dd5ed: 3/3 PASS. The host load average was ~15 (the
incident had ~40), and `runner.suspend` finished in all three runs, so
these runs show no regression but did not exercise the new retirement
path. The integration cases cover that path.

## Risks

- The new path lets a follow-up archive a local root whose Runner
receipt is unsuspended. This is safe only if the Runner is stopped, so
the proof uses exact PID and start-time identity and fails closed on any
uncertainty. `ps` start times have one-second precision; a sub-second
difference is treated as the same process (not proof).
- A zombie-only process group on macOS can report as alive. That keeps
the rejection, as before.
- No quarantine behavior changes. A refused retirement still quarantines
the root as before.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, through Claude Code
with tool use (shell, file edits, 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 15:11:00 -06:00
DottaandClaude Opus 5.5 586caa5f49 fix(native): hand a shutdown-interrupted startup to the next controller (#19596)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The native Runner path runs an agent turn through a runnerd process
that the control plane owns
> - A restart of the control plane must keep the task going without a
click, if that is safe
> - A graceful shutdown closes the HTTP listener first and then drains
the heartbeat runs
> - A native run that was between provider dispatch and runnerd backend
setup in that gap failed with `local_runtime_api_listener_unavailable`,
so the drain did not see it as running and did not suspend it for the
next boot
> - This pull request makes that failure a restart detach: the run stays
running with its claim, the drain suspends it, and the next controller
adopts it
> - The benefit is that a SIGTERM in this window no longer spends a
failure and a same-run retry, and the task continues by itself

## Linked Issues or Issue Description

Refs #19530 (the same restart window, before provider dispatch).

**What happened?**

In a restart chaos replay (ACPX Claude, local runner), a
restart-recovered run got SIGTERM while
`createRunnerdBackendWithinSessionClaim` built the runnerd backend.
`resolveLocalRuntimeApiUrl()` threw
`local_runtime_api_listener_unavailable` because shutdown had already
closed the listener. The run settled `failed`. The drain then logged
`restartSuspendedRunIds: []`. The next boot used the one bootstrap
retry. A second SIGTERM hit the same window, and the task stopped at a
manual Retry.

**Expected behavior**

A run that this process cannot continue because it is shutting down is
handed to the next controller. It is not settled as failed.

**Steps to reproduce**

1. Start a native Runner task and restart the server (K1) so that
startup recovery resumes the run.
2. Send SIGTERM while the resumed run sets up its runnerd backend.
3. See the run fail with `local_runtime_api_listener_unavailable` and
the drain report no suspended runs.

## What Changed

- `server/src/runtime-api.ts`: `resolveLocalRuntimeApiUrl()` throws the
new `LocalRuntimeApiListenerClosedError` when this process had a
listener and closed it (the `listening` flag is false or the `close`
event fired). The error message stays
`local_runtime_api_listener_unavailable`, so the existing classifiers do
not change. A listener that never started still throws the plain error.
- `server/src/services/native-runtime/native-session-executor.ts`: for a
local Runner, the startup failure handler treats
`LocalRuntimeApiListenerClosedError` like a restart detach. It stops
lease renewal, keeps the claim and throws
`NativeControllerDetachedForRestartError`. Heartbeat already keeps such
a run `running`. A remote Runner keeps the same-run bootstrap retry from
#19462, because startup recovery has no process evidence to adopt a
claimed remote startup
(`native-runner-restart-preparation.integration.test.ts` still covers
that path).
- `doc/execution-semantics.md`: describe the new rule.
- Tests: a heartbeat regression that closes the real runtime API
listener before backend setup, then drains and restarts; a unit test for
the closed versus never-started listener; two `vi.mock` factories keep
the real module exports.

## Verification

- Red before the fix: `pnpm exec vitest run
src/__tests__/heartbeat-native-restart-suspension.test.ts -t listener`
failed with `restartSuspendedRunIds: []` and the run `failed`
(`local_runtime_api_listener_unavailable`, `bootstrap_retry`).
- Green after the fix: the same test passes. The drain suspends the run,
the run stays `running` with `awaiting_runner_reattach`, and the next
controller claims it and reaches the backend once.
- `pnpm exec vitest run
src/__tests__/heartbeat-native-restart-suspension.test.ts
src/http/server.test.ts
src/__tests__/server-startup-feedback-export.test.ts
src/services/native-runtime/native-session-executor.test.ts
src/services/native-runtime/native-recovered-owner-followup.test.ts`:
1333 tests pass. One timing test in `native-session-executor.test.ts`
failed once under host load average ~30 and passed alone.
- `pnpm exec tsc --noEmit -p server/tsconfig.json`: pass.
- Live, restart chaos harness, local ACPX Claude (`claude-sonnet-5`),
`--rc06-masked`, this branch at a35ec8b845 (before the remote-scope
commit 5dc63f24fb, which does not change the local path). The original
replay schedule no longer reached the window, because the task now ends
before the second fault. So I moved the faults earlier: K1 at 10 s, then
K2 as soon as the recovered server is ready.
- K1 + K2, 2 runs: both runs PASS with **R1 pass**. Each drain logged
`restartSuspendedRunIds: [<run>]` (before: `[]`). The next boot
classified the run `resume_dead_runner` and the same run completed. No
`local_runtime_api_listener_unavailable` failure.
- K1 + K2 + K2, 2 runs: each drain suspended the run. The fourth boot
blocked the run with `native_restart_recovery_blocked`, because three
restart claims used the three-attempt recovery budget. Retry then
completed the task (R3 pass). That budget count is a separate open item.
This pull request does not change it.

## Risks

- Low. The new path applies only after this process closed its own
listener, which happens only during shutdown. The run then waits for the
drain or the next boot, as for the other restart detaches.
- If shutdown does not reach the drain (for example SIGKILL after the
listener closes), the run keeps its live claim until the claim expires.
Then startup recovery handles it as a hard restart. This is the same as
any run that is running at a hard kill.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, through Claude Code
with tool use (shell, file edits, 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 15:10:44 -06:00
github-actions[bot]andlockfile-bot e28158b353 chore(lockfile): refresh pnpm lockfiles (#19617)
## 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 21:08:43 +00:00
Devin FoleyandClaude Opus 5.5 4ef3fa4271 docs(release): prepare v2026.1106.0 stable notes before publish (#19607)
<!-- Write all pull request text in Simplified Technical English
(ASD-STE100): short sentences, one instruction per sentence, simple
approved vocabulary, and the active voice. -->

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Each stable release publishes curated release notes as its GitHub
Release body.
> - The stable release workflow reads
`releases/beta/v2026.1023.0-beta.0.md` from master when it starts. It
publishes that exact revision.
> - The notes on master show v2026.1103.0 and the date 2026-11-03. That
version did not ship.
> - The promotion of `2026.1023.0-beta.0` now starts on 2026-11-06, so
the version is `2026.1106.0`.
> - This pull request sets the header to the correct version and date
before the stable release starts.
> - The benefit is that the release page shows the version and date that
actually ship.

## Linked Issues or Issue Description

Refs #18842 (the previous pre-publish fix of these notes, for
v2026.1103.0). Refs #17388 (the curated notes for the
`2026.1023.0-beta.0` soak).

**Problem**
The stable notes for the promotion of `2026.1023.0-beta.0` show `#
Paperclip v2026.1103.0` and `> Released: 2026-11-03`. The release for
that version did not publish. `scripts/release.sh stable --print-version
--date 2026-11-06` on master gives `2026.1106.0`.

**Expected result**
The notes show `# Paperclip v2026.1106.0` and `> Released: 2026-11-06`.
The intro paragraph names v2026.1106.0.

**Impact**
The stable workflow publishes these notes as the GitHub Release body. If
this pull request does not merge before the release starts, the release
page shows the incorrect version and date.

## What Changed

- Set the H1 to `# Paperclip v2026.1106.0`.
- Set the release line to `> Released: 2026-11-06`.
- Set the version in the first sentence of the intro paragraph to
v2026.1106.0.
- No other text changes. The diff is 3 lines in one file.

## Verification

- `scripts/release.sh stable --print-version --date 2026-11-06` on
master `d728be8fbffc95a903ffd58bc527c7e8f1a1c704` gives `2026.1106.0`.
- The file has no other `1103` or `2026-11-03` strings after the change.
- The audit of the content against
`v2026.1023.0..d69631b01b9c658f80bf8bed389863dce82643e1` did not change
since #18842. I ran it again:
  - `git rev-list --count` = 515. This matches the notes.
- All 255 cited PR numbers are in the range. Five cited PRs (#14180,
#16356, #16401, #16403, #16976) are squash commits without a `(#N)`
suffix.
- `git shortlog -sn` without `github-actions[bot]` gives 27
contributors. This matches the notes. The 21 credited handles do not
change.
- The migrations and the defaults in the Upgrade Guide were checked in
#18842. The range did not change.
- The file on this branch has blob
`16f05f22d4d71de3a568b493de2ddcb8eadb7859`. This is identical to the
locally checked edit.

## Risks

- Low risk. This is a change to documentation only, in one file.
- If the UTC date changes before the release starts, the release
workflow keeps `stable_date=2026-11-06` to match this header.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use (shell,
git, GitHub API). Used to check the version, run the audit again, and
write the change.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [ ] I have run tests locally and they pass (documentation only; no
tests apply)
- [ ] I have added or updated tests where applicable (not 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:05:37 -08:00
DottaandPaperclip 4d554f0821 test(runner): set default inputs in workflow target fixture (#19602)
## Thinking Path

> - Paperclip manages work performed by AI agents.
> - Its Runner support tests verify workflow authorization and target
pinning.
> - The workflow now passes two Boolean inputs to its shell guard.
> - The target fixture runs that guard with `set -u` but omits those
inputs.
> - Five cases stop on an unset variable before they reach their
assertions.
> - This change supplies the workflow's normal false defaults in the
fixture.
> - The production guard and all target-pinning assertions remain
unchanged.

## Linked Issues or Issue Description

Refs #19537, #19576.

**What happened?**

The required Runner support suite fails five `workflow-target.test.ts`
cases with `IMAGE_ONLY: unbound variable`. The real workflow supplies
`IMAGE_ONLY` and `TESTOPS_REQUESTED`. The test fixture does not supply
either value.

**Expected behavior**

The fixture uses the normal workflow defaults and reaches the target
authorization assertions.

**Steps to reproduce**

Run `pnpm exec vitest run --config tests/runner-e2e/vitest.config.ts
workflow-target.test.ts` on master
`4938a930d66846094810cbed45d2fd33c0d14c2d`.

## What Changed

- Set `IMAGE_ONLY` and `TESTOPS_REQUESTED` to `false` in the target
fixture's child environment.

## Verification

- RED: the unchanged current-master fixture had five failures and four
passes. Bash stopped on `IMAGE_ONLY: unbound variable` before target
authorization ran (220ms).
- GREEN: all nine cases pass after the one-line fixture repair (168ms).
Existing checks still reject a custom bounded target and preserve the
queued commit for scheduled and bounded manual runs.
- Local evidence uses the actual test and workflow files with installed
Vitest 5.0.3 and a temporary focused config. It omits the unrelated
provider-bundle global setup. No provider, external service, or paid
workflow runs.
- The [original required CI
failure](https://github.com/paperclipai/paperclip/actions/runs/42288042001/job/128134318743)
has the same five Bash failures. Its evidence is retained; no unchanged
retry was used.
- Whitespace checks pass. Exact head
`f1d178ad483a49f8dd087a1c6d22bf43c06a39e4` has 56 successful checks,
three expected skips, one neutral advisory status and successful Snyk.
No check is pending or failed. The [full Runner support
suite](https://github.com/paperclipai/paperclip/actions/runs/42289536842/job/128139308510),
typecheck, build and [aggregate
verification](https://github.com/paperclipai/paperclip/actions/runs/42289536842/job/128145908574)
pass. Greptile and Paperclip Review both rate this exact head 5/5, with
no findings or unresolved threads.
- Environment: macOS arm64, Node 26.4.0, Vitest 5.0.3.

## Risks

Low risk. This changes only fixture inputs for normal target selection.
It does not change the production workflow guard, branch policy,
credentials, paid execution, timeouts or assertions. Product
documentation needs no update for test-only default inputs.

## Model Used

OpenAI Codex, GPT-6 family, with repository analysis, code editing and
tool execution. The exact runtime model ID and context window are not
exposed in this session.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub references)
- [x] My branch name describes the change and contains no internal
Paperclip ticket id
- [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 (not
needed: fixture-only default inputs)
- [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 14:52:38 -06:00
DottaandClaude Opus 5.5 d728be8fbf fix(native): re-bootstrap a Daytona runner restarted before session.open (#19583)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs on Daytona start a `paperclip-runnerd` process in a
sandbox. The local control plane keeps the runner's controller state in
an encrypted Postgres checkpoint.
> - When the control plane restarts, restart recovery classifies each
live native run and decides to reattach, resume, bootstrap again, or
hold the task.
> - A restart chaos wave killed the control plane twice while a Daytona
runner was connecting. The run was claimed and the sandbox runner was
launched, but `session.open` never completed. No provider evidence
existed.
> - Recovery waited correctly for the old 60 s controller lease to
expire. Then the classifier had no rule for a claimed remote bootstrap.
It returned `ambiguous_remote_recovery_evidence` and blocked the task as
"Workspace repair required".
> - The controller writes each command to the Postgres checkpoint before
it sends it. After the lease expires, that checkpoint is the complete
record of what reached the sandbox runner.
> - This pull request lets recovery prove from that checkpoint that no
provider work exists, and then start the same-run bootstrap again.
> - The benefit is that a restart during Daytona runner startup
continues automatically instead of stopping the task.

## Linked Issues or Issue Description

**What happened?**

A restart during Daytona remote runner startup (native ACPX Claude, two
SIGKILLs about 20 s apart) left the task blocked. The run history showed
`remote_controller_lease_live` twice, then
`ambiguous_remote_recovery_evidence` with disposition `blocked`. The
task showed "Workspace repair required"
(`native_restart_recovery_blocked`) and did not continue.

**Expected behavior**

When the durable controller record proves that the runner received only
bootstrap commands, recovery continues the run with a fresh bootstrap.
Any evidence of provider work keeps the hold.

**Steps to reproduce**

1. Start a native run on a Daytona environment.
2. Kill the control plane after the sandbox runner connects and before
`session.open` completes.
3. Restart the control plane and wait for the old controller lease to
expire.
4. See that restart recovery blocks the task with
`ambiguous_remote_recovery_evidence`.

Root cause:
`server/src/services/native-runtime/native-restart-recovery.ts`
`classifyNativeRunnerRecoveryEvidence`, remote branch. It admitted only
an unclaimed bootstrap (`remote_execution_never_claimed`, #16349) or a
run with a provider checkpoint. A claimed bootstrap with no provider
evidence fell through to `ambiguous_remote_recovery_evidence`.

The decrypted checkpoint of the failed run held `connectionCount: 1`,
`run.prepare` delivered once and pending, `session.open` pending and
never delivered, no committed events, and `ackedSourceSeq: 0`.

Refs #16349, #19524

## What Changed

- `native-bootstrap-proof.ts`: move the incomplete-bootstrap predicates
from the executor into a small pure module. Restart recovery and the
executor now use the same proof. No behavior change.
- `native-runner-checkpoint.ts`: add
`nativeRunnerCheckpointProvesIncompleteBootstrap`. It reads only a
checkpoint with the bootstrap parts (header, cursor, `command:1`,
`command:2`), checks the run, runner instance, and session identity, and
applies the shared proof. A remote runner journal is not on this host,
so the proof uses the controller record alone.
- `native-restart-recovery.ts`: for a classic sandbox run whose owner
lease has expired, with a durable checkpoint, no session checkpoint, no
provider evidence, and no staged attachment, claim
`bootstrap_incomplete` with the reason
`remote_bootstrap_proven_incomplete` when the checkpoint proof holds. At
three provider attempts, block with
`execution_recovery_budget_exhausted`.
- `native-session-executor.ts`: add
`restoreRemoteBootstrapCheckpointForRecovery`. For a
`bootstrap_incomplete` recovery on a sandbox target, it restores the
Postgres checkpoint over the local external-state marker. The existing
incomplete-bootstrap proof then checks the restored state before it
quarantines the root and bootstraps again.
- `doc/run-log-events.md`: document the new disposition reason.

## Verification

- Red, then green: `cd server && npx vitest run
src/services/native-runtime/native-runner-restart-recovery.integration.test.ts
-t "remote bootstrap"`. Before the fix, the proven case and the budget
case returned `blocked` / `ambiguous_remote_recovery_evidence`.
- New cases: the proven claimed bootstrap gets `bootstrap_incomplete`
(attempt kept, generation +1, run still `running`, no recovery action).
The hold stays for a queued `turn.start`, an acknowledged event, adopted
state, no checkpoint, and a spent budget. The executor helper restores
the checkpoint only for a bootstrap recovery on a classic sandbox and
only over a marker for the same run.
- Regression set: `native-runner-restart-recovery.integration`,
`native-session-executor`, `native-restart-recovery`,
`native-bootstrap-retry.integration`, `heartbeat/recovery`: 1383 passed,
14 skipped.
- Live Daytona replay of the failing chaos task (double SIGKILL during
remote runner startup), 3 runs: REPLAY_RESULTS

## Risks

- A fresh bootstrap acquires a new sandbox. The old sandbox keeps its
lease until run settlement releases all run leases. The old runner gets
no turn command, because only the controller issues one and the new
controller never connects to it.
- If `session.open` reached the old runner, the old provider session can
exist without a turn. No provider work or tool call can happen in it.
- Any checkpoint shape other than the bootstrap-only shape keeps the
previous fail-closed hold.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
canary/v2026.1106.0-canary.20
2026-11-06 14:43:22 -06:00
DottaandClaude Opus 5.5 dc1adb7209 fix(tasks): advertise a native resume only while the scheduler would claim it (#19523)
## 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"
> - #19503 made the task interruption read the coordinator's pending
resume time. Review found that the coordinator also pushes that time
forward while it defers an ineligible candidate, so a reassigned or
blocked task could hide Retry for good
> - This pull request advertises the resume only while the scheduler's
synchronous admission checks pass
> - 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

Follow-up to #19503. Refs #19487, #19497.

**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 pending-resume read now also
requires the task to be in a resumable status, assigned to the run's
agent, not owned by another execution or checkout run, the run's
question not cancelled, and the agent invokable (not paused, terminated
or pending approval). These mirror `readNativeSessionResumptionAgent`.
- `native-session-resumption-statuses.ts`: one shared status set used by
both the scheduler admission and the read model.
- No time cap: provider cooldowns and remote backoff schedules stay
visible as a pending resume.

## Verification

- `task-interruption-query.test.ts` (embedded Postgres): a reassigned
task and a blocked task show one Retry; a paused agent asks to be
resumed; in each case `readNativeSessionResumptionAgent` also refuses
the candidate, so the read model and the scheduler agree. A resume an
hour out (provider cooldown) stays `retry_scheduled` with no Retry. Red
on master for the reassigned case.
- All 57 presentation and query tests pass. Server typecheck passes.

## Risks

- Low risk. Read model only.
- A review-assignment resume (reviewer is not the assignee) shows Retry
instead of the pending resume. That is the safe fallback.
- Deferrals for a pause hold, dependency readiness or a competing run
are not mirrored. Those gates also hold a manual Retry, so no competing
action appears.
- The `POST /issues/:id/retry` endpoint is unchanged.

## 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 14:38:29 -06:00
DottaandClaude Opus 5.5 02214b1a3c fix(evals): treat a job-less TestOps plan row as unassigned during registration (#19601)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Runner E2E cases run from Allure TestOps plans through
`runner-full-stack-e2e.yml`
> - #19549 added a sibling "Reliability — staging soak" plan and a
one-shot registration workflow
> - The first live `register` dispatch created the seven cases and the
plan, then stopped with `plan_job_conflict`
> - Live TestOps lists a new plan's uncovered cases as one row without a
job, and the code read that row as another job
> - This pull request treats a job-less row as "not assigned"
> - The benefit is that a second `register` dispatch can assign the plan
to the standing GitHub job and finish registration

## Linked Issues or Issue Description

Refs #19549

**What happened?**
The `runner-e2e-testops-registration.yml` `register` run on master
failed with `reliability_testops_registration_plan_job_conflict`. It had
already created the seven cases and the plan. `GET
/api/testplan/<id>/job` for that plan returned `[{"testCasesCount":7}]`.
The row has no `job` field.

**Expected behavior**
Registration treats a plan with only a job-less row as unassigned. It
assigns the plan to the standing Product regression GitHub job and reads
the assignment back.

**Steps to reproduce**
1. Dispatch `runner-e2e-testops-registration.yml` on master with
`operation=register` when no Reliability plan exists.
2. The new plan reports one job-less row.
3. Registration fails with `plan_job_conflict` before the assignment
step.

**Paperclip version or commit**
master at 744056ecc8 and later.

## What Changed

- `tests/runner-e2e/reliability-testops-registration.mjs`: add
`jobAssignments()`. It keeps the array check and ignores rows without a
`job`. Inspect, the pre-assignment conflict check and the
post-assignment read-back use it.
- `tests/runner-e2e/reliability-testops.test.mjs`: the fake TestOps now
returns the live job-less row for a new plan. A new test reproduces the
half-registered plan and checks that `register` assigns the standing job
and that the receipt becomes complete.

## Verification

- `node --test tests/runner-e2e/reliability-testops.test.mjs
tests/runner-e2e/product-regression-testops.test.mjs`: 28/28 pass.
- Red before the fix: with the old registration module, 4 tests fail,
including the new one. Green after: 13/13 in the reliability file.
- Live check after merge: dispatch `register` again. The cases and plan
already exist, so it only assigns the plan to the standing job.

## Risks

- Low risk. The change touches only the one-shot registration helper and
its tests. It does not change provider cells, launch selection or
reporting.
- A plan assigned to another job still fails with `plan_job_conflict`.
The existing test still covers that case.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code with tool use
(shell, browser read of the TestOps API response), standard context.

## 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 14:36:26 -06:00
DottaandClaude Opus 5.5 8c7e86e73b fix(workspaces): let non-git workspace directories pass close readiness (#19553)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Execution workspaces hold the files that agents work in, and a
person can close a workspace when the work ends
> - Before close, Paperclip checks "close readiness" so that it does not
destroy unsaved git work
> - The check assumed that every existing workspace path is a git
repository
> - A shared or `local_fs` workspace can be a plain directory. For that
directory, `git rev-parse` fails, and the check reported "Paperclip
could not verify the workspace git status" as a blocking reason
> - So a person could never close these workspaces
> - This pull request treats a path with no `.git` marker as a workspace
with no git state
> - The benefit is that plain-directory workspaces can close, and real
git errors still block close

## Linked Issues or Issue Description

**What happened?**
`GET /api/execution-workspaces/:id/close-readiness` returned `state:
"blocked"` for a `shared_workspace` with provider `local_fs`. The
workspace directory existed but was not a git repository. The only
blocking reason was "Paperclip could not verify the workspace git
status. Retry before destructive cleanup."

**Steps to reproduce**
1. Create a project workspace and an execution workspace
(`shared_workspace`, `local_fs`) whose `cwd` is a plain directory that
exists.
2. Request close readiness for the execution workspace.
3. See `state: "blocked"` and `isDestructiveCloseAllowed: false`.

**Expected behavior**
The workspace is ready to close. It has no git state to lose.

## What Changed

- `inspectGitCloseReadiness` in
`server/src/services/execution-workspaces.ts`: when `git rev-parse
--show-toplevel` fails, the provider is not `git_worktree`, and no
`.git` entry exists on the path or on an ancestor, return `git: null`
with `statusInspectionSucceeded: true`.
- Added `hasGitMarkerInPathOrAncestors`. The check uses the filesystem,
not git error text, so it does not depend on the git locale. It uses
`lstat`, so a dangling `.git` symlink still counts as a marker. Any
error other than `ENOENT` counts as a possible marker, so the check
fails closed.
- For a plain directory, the result is a git readiness object with
`repoRoot: null`, the same shape as the missing-path case.
`createdByRuntime` stays on it, so close readiness still plans
`remove_local_directory` for a runtime-created directory.
- Added two regression tests in
`server/src/__tests__/execution-workspaces-service.test.ts`:
- A plain directory is `ready_with_warnings` with `git: null` and no
blocking reasons. This test failed before the fix (`state: "blocked"`).
- A directory with a broken `.git` pointer is still `blocked` with the
git-status reason.
- A directory with a dangling `.git` symlink is still `blocked`. This
test failed before the second commit.
- A runtime-created plain directory plans `remove_local_directory`. This
test failed before the second commit.
- A plain directory nested under an ancestor with an unreadable `.git`
marker is still `blocked`. This test covers the ancestor walk.

## Verification

- Red/green: the new non-git test failed on `master` (`state:
"blocked"`) and passes with the fix. The broken-git test passes on both.
- `cd server && npx vitest run
src/__tests__/execution-workspaces-service.test.ts
src/__tests__/execution-workspaces-routes.test.ts`: 2 files, 101 tests
pass. The five close-readiness tests also pass after the third commit.
- `pnpm verify` selected 2,223 of 2,991 test files because this service
has many importers. I did not run that near-full suite locally. Required
CI supplies that coverage.
- `tsc -p server/tsconfig.json` shows no errors in the changed files.

## Risks

- Low risk. The change only affects paths where git inspection already
failed.
- With `git: null` removed, the result uses the same `repoRoot: null`
shape as a missing path. Existing code already handles that shape.
- `git_worktree` workspaces still fail closed. A real repository that
git cannot read (corrupt, missing git dir, ownership refusal) still has
a `.git` marker, so it still blocks.
- The terminal-workspace sweep also uses this inspection. With no repo
root there is no HEAD SHA, so delivery cannot be "merged", and the sweep
still skips these workspaces. Only an explicit close by a person
proceeds.
- A `.git` on an ancestor across a filesystem boundary makes git report
"not a repository" while the marker check finds it. That case stays
blocked, which is the safe result.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 14:33:09 -06:00
DottaandClaude Opus 5.5 4938a930d6 fix(budgets): stop pending or restart-lost accounting from hard-stopping companies (#19497)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Budget admission and run accounting decide whether any task in a
company may start
> - A control-plane restart that kills a provider turn mid-flight can
lose that turn's final usage receipt. ACPX Claude reports usage only at
turn end, so the receipt never becomes complete
> - Accounting kept such a run "awaiting receipt" forever. Every later
admission in the company then refused with "completed runs await
accounting", and the budget evaluator opened a hard-stop incident that
paused the company. No product action could clear it: raising the budget
refuses while runs await accounting, accounting retry finds no new data,
and allowing unpriced usage does not apply to pending runs
> - Restart chaos waves showed this as a dead task Retry, a new task
blocked as "over budget" at 22 of 500 cents, and a hard stop that
cancelled queued Retries and an unrelated running task
> - This pull request closes the debt from evidence: once native
recovery reaches terminal failure and the provider stop is verified, the
known part settles as an unpriced charge. Until then the run holds new
work as recovering accounting, without a hard stop
> - The benefit is that a restart no longer bricks a company with a hard
budget. The operator's unpriced-usage policy decides admission, and
unknown spend still never shows as zero cost

## Linked Issues or Issue Description

Refs #19399, #19426, #19431 (earlier restart Retry fixes; this is the
budget layer under the same journey).

**What happened?**
After a control-plane SIGKILL during an ACPX Claude turn, the killed run
ended `native_provider_terminal_failed` with
`usage_json.accountingReceiptReady = false` and `cost_accounting_pending
= true`. It stayed that way for good. The task Retry was accepted but
parked with `budget.blocked` ("Company cannot start work while completed
runs await accounting"), which the UI showed as "Retry is waiting for
execution checks". A new Codex task was escalated to `blocked` with "the
original recovery target is over budget" while the company had spent 22
of 500 cents. After the next restart the budget evaluator opened a
hard-stop incident (`amount_observed` 22.08 < 500), paused the company
and cancelled both queued Retries.

**What did you expect?**
A restart should not stop the company. Unknown usage from a stopped
provider should become an honest, operator-governed state, and Retry
should start a run.

**Steps to reproduce**
1. Give a company a hard-stop budget.
2. Start an ACPX Claude task and SIGKILL the control plane mid-turn, so
the resumed turn fails.
3. Observe the run stay `cost_accounting_pending` with an incomplete
receipt, and every new admission refuse.

## What Changed

- `budgets.ts`: completed runs that await accounting now hold new work
only. They no longer open a hard-stop incident, pause the company, place
an agent budget hold, or deliver Stop to running work. Only spend at the
limit, or unpriced usage under a blocking policy, does that. Chaos wave
A and a pre-restart lifecycle cell showed the wedge: a run that had just
succeeded stayed terminal for about 100 ms before its charge committed,
the evaluator paused the company in that gap, and the Stop it delivered
left a cancelled run whose receipt never closed.
- `run-cost-accounting.ts`: a failed native run is a closed receipt
source only with positive stop evidence: its finalization is
`terminal_failure` with no result (not a finalization-deadline failure),
it no longer records a process identity, and its latest server-authored
process event is `native.local_process_stopped`. One SQL predicate
serves the reconciler scan and settlement. Accounting now settles its
known part as an `unpriced` charge instead of waiting forever. The
background reconciler selects these runs. The reservation settles. Other
incomplete receipts stay pending as before. A cancelled run keeps its
existing accounting drain, so Stop recovery can still deliver its final
receipt.
- `native-local-process-stop.ts` and its transactional callers:
releasing a verified local or remote process stop wakes the accounting
worker, so the closed debt settles promptly.
- `usage-receipts.ts`: a charge settled without its receipt is marked
`accountingReceiptUnavailable`. A later complete receipt that carries
its own price (including a rate-card price) corrects it. A receipt with
no own price and unchanged usage does not reopen the hold.
- `budget-exposure.ts`: the blocking unpriced-usage policy no longer
counts a charge settled from a lost receipt with no observed usage. That
is unknown, not "usage with no reliable price". Complete receipts that
report no tokens (for example Cursor Cloud) still count. A live wave
under the default policy hard-stopped its company at 0 of 500 cents on
one such charge.
- `budget-exposure.ts`: a terminal-failure native run with an
incomplete, not capture-failed receipt counts as recovering accounting.
Admission holds new work ("New work must wait while a native run
recovers its unfinished accounting") without a hard-stop incident, a
company pause or cancellation of running work.
- `recovery/service.ts`: the stranded-work comment names the actual
budget gate instead of always saying "over budget".
- `task-interruption-presentation.ts`: a Retry parked by
`budget.blocked` says it waits for the budget, not for "execution
checks".
- `doc/SPEC-implementation.md`: documents the closed-receipt contract
next to the existing incomplete-receipt rule.

## Verification

- New embedded-Postgres suite
`server/src/__tests__/closed-native-receipt-accounting.test.ts`, built
from the persisted shape of the killed run:
- Red before the fix: the run stayed pending, `reconcileRunCosts`
accounted 0, and admission refused with "completed runs await
accounting".
- Debt stays pending with no verified stop event, with a
finalization-deadline failure, with a recorded process identity, and
while the coordinator can still produce a result.
- Under a blocking policy, a lost receipt with no usage does not
hard-stop; one with tokens still blocks. A run with a still-recorded
process identity paused the company with a hard-stop incident.
- Green after: the stopped run settles as one `unpriced` charge with the
reservation settled; admission is clear under an `allow` policy and
names "no reliable price" under `block`; a run with a recorded process
identity or a non-terminal coordinator stays pending; the closing window
holds admission without an incident or pause; a final receipt that
arrives later still enters late-receipt reconciliation.
- Pending accounting from a run that just finished holds admission with
no incident, no pause, no enforcement version bump, and an unrelated
running run stays running. Red before the `budgets.ts` change: the
company was paused.
- A later priced receipt corrects the unavailable charge to its price;
an unpriced one leaves it settled.
- `task-interruption-presentation.test.ts`: the `budget.blocked` Retry
wait message.
- `budget-enforcement-scoping.test.ts`: the accounting-hold release
cases now assert that pending accounting places no budget hold and that
admission clears after settlement.
- Existing suites pass locally: all budget suites (control publication,
enforcement scoping, enforcement worker, evaluation work, live attempts,
history, service, live usage, budget cancellation),
native-stop-accounting, explicit-native-continuation,
native-case-handoff, cost-data-quality, heartbeat-cost-accounting,
run-cost-accounting, native-usage-delivery. Server typecheck passes.
- Runner e2e cell
`lifecycle-baseline.runner-codex.local.lifecycle-dependency-restart-neutral`,
which failed on master with a false agent budget hold before any
restart, passes on this branch.
- Live restart-chaos wave (local runner, K1 SIGKILL, codex + 2× ACPX
Claude + OpenCode, 4 restarts, company policy `unpricedUsagePolicy:
allow`) with the accounting fix: every killed run settled (no
`cost_accounting_pending` left), the company stayed active, the OpenCode
Retry completed the task on the first click and the Codex Retry finished
its task. That wave also exposed a 1-second false hard stop in the
closing window (observed 0 of 500 cents) that cancelled the running
OpenCode turn; the `budget-exposure.ts` change fixes it and has its own
red/green test.
- Confirming live wave with both commits, same plan (2× ACPX Claude,
OpenCode, Codex; 4 K1 restarts): both Claude tasks passed R3 (one Retry
click started a run that completed the task), OpenCode passed R1
(continued by itself), and the Codex Retry completed its task. Zero
budget incidents, zero cancelled runs, zero runs left pending
accounting, company active; three killed runs recorded as `unpriced`
charges. The only open signature is an unrelated workspace-durability
check (`R2:pre_fault_files_lost` on the Codex long-bench task).

## Risks

- Behavior change: a stopped native run's lost receipt now becomes an
`unpriced` ledger charge with its known usage. If that usage has tokens,
the default `block` policy still stops new work with "recorded usage has
no reliable price". If it has none, the charge is visible in Costs but
does not stop the company. Other unpriced charges keep their existing
effect. Unknown spend is never recorded as a priced receipt. Budget
enforcement stays best effort, as the spec says; killed OpenCode and
Codex requests already lose their in-flight usage.
- Remote runs and runs without a verified local stop keep their debt
pending (an admission hold only, no hard stop).
- Contract change: pending accounting no longer pauses a scope or stops
running work. New work still waits with "completed runs await
accounting". A scope's real hard stop (spend at the limit, or blocked
unpriced usage) is unchanged.
- If a provider stop can never be verified, the run stays "recovering
accounting": new work waits without a hard-stop incident. Before, the
same state paused the company. The hold is visible in admission errors
and Costs.
- A receipt that arrives after settlement posts a late correction. The
original unpriced charge stays in the ledger.
- No schema or migration change.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 14:27:51 -06:00
DottaandClaude Opus 5.5 1634f8b66b test(restart-chaos): run chaos waves against Daytona sandboxes without Docker (#19544)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness (`tests/restart-chaos/`) restarts a local
control plane on purpose while agent tasks run, and grades every task on
rules R1 to R6
> - The plan targets agent runs in Daytona sandboxes, but the harness
only supported `--env local`
> - A local controller needs two inputs that match the sandbox image
exactly: the image's Linux `paperclip-runnerd` and its provider pack.
The documented way to get them uses Docker, and Docker is not available
on the operator machine
> - The Daytona org that chaos uses also runs the live Cloud fleet, so
the harness must never change a sandbox it did not create through its
own Paperclip environment
> - This pull request adds `--env daytona`. It extracts both inputs from
the image without Docker, and refuses a controller whose runner source
differs from the image. It seeds a Daytona environment from the
runner-e2e fixtures and adds a read-only Daytona R6 sweep
> - The benefit is that chaos waves can now cross real Daytona sandboxes
with a local, killable control plane

## Linked Issues or Issue Description

**Feature description**
Run restart chaos waves with a local control plane and agent runs in
Daytona sandboxes (plan
`doc/plans/2026-11-06-control-plane-restart-chaos.md`, Phase 1b).

**Problem it solves**
`--env daytona` threw "not wired yet". The controller-side runnerd and
provider pack had to come from `docker cp`, which needs a Docker daemon.

**Proposed solution**
Use `skopeo copy` (no daemon) to fetch the linux/amd64 image into a
directory, walk the OCI layers from top to bottom, and extract only the
two paths. Before the wave starts, compare the image's content-ID label
with this checkout.

Refs #19477

## What Changed

- `tests/restart-chaos/daytona-image.ts`: a streaming tar reader and an
OCI layer walk (top to bottom, `.wh.<name>` and `.wh..wh..opq`
whiteouts, hard links, symlinks written last, no `..` paths). It
extracts `/usr/local/bin/paperclip-runnerd` and
`/opt/paperclip-runner/provider-pack/`. It checks the layer blob
digests, the ELF header (x86-64), the provider-pack manifest schema, and
its `runnerSourceRevision` against the image revision label. It records
digests in `receipt.json`, caches the result by image digest under
`results/daytona-images/`, and deletes the layer blobs. It also runs
standalone.
- Controller/image check: `assertControllerMatchesImage` compares the
image's `io.paperclip.runner.content-id` label with
`computeDaytonaImageContentId` for this checkout. On a mismatch it fails
and prints both IDs and the image's source revision.
- `seed.ts`: for `--env daytona`, it installs the bundled Daytona plugin
(`installFixtureDaytonaProvider`) and waits for provider readiness. It
creates the `daytona` environment fixture from `catalog.ts` with a
`DAYTONA_API_KEY` company secret and runs the native Codex
platform-qualification probe. Then it assigns the agents and the project
to that environment. `--daytona-cpu` sets the sandbox vCPU (default 4, 4
GiB, 10 GiB).
- `daytona-sweep.ts`: a read-only R6 for Daytona. The Daytona client is
a frozen wrapper that exposes only a label-filtered `list`, and no stop,
archive or delete exists. The sweep keeps only rows whose
`paperclip-company-id` and `paperclip-environment-id` both equal the
wave's own UUIDs, and it re-checks every row. It reconciles sandboxes
with the environment's leases:
  - A sandbox archived after its lease was released is expected.
- An orphaned sandbox or an `active`/`pending_cleanup` lease fails R6
for the owning task, or the wave.
- `assertHarnessMayMutate` refuses `paperclip-create-*` names and
`fleet-sbx-*` snapshots.
- Cleanup only deletes the wave's environment through Paperclip and
reports what is still running.
- `config.ts`: `--daytona-image` (or `PAPERCLIP_E2E_DAYTONA_IMAGE`)
needs an immutable digest. `--concurrency × --daytona-cpu` must stay
within `RESTART_CHAOS_DAYTONA_VCPU_BUDGET` (default 40). B5 is
local-only, because its oracle reads the live workspace.
- `main.ts`: Daytona preflight (credentials, image extraction,
content-ID check, Daytona plugin build as CI does). It passes
`PAPERCLIP_RUNNER_REMOTE_BINARY_PATH` and
`PAPERCLIP_RUNNER_REMOTE_PROVIDER_PACK_PATH` to every server start and
writes `sandbox-sweep.json`. Cleanup errors never skip stopping the
controller.
- README: a Daytona section (image matching, image-only CI dispatch,
vCPU budget, fleet safety, R6).
- Unit tests: synthetic tar layers for the tar reader, whiteouts, opaque
directories, overrides, hard links, symlinks and parent replacement.
Further tests cover the content-ID check, sweep reconciliation, exact
wave selection (unlabelled rows and other labels are never selected),
the read-only client, the fleet guard, and the CLI options.

## Verification

- `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts`:
62/62 pass, credential-free.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`: clean.
- Real extraction without Docker, image `…@sha256:b0c71f4d…`: 42 layers,
about 8 minutes, a 3 GB pack.
- The content-ID check refused the current checkout. The image is
`b8b16e15…` from `7e6d882558`; the checkout is `aec45589…`.
- A checkout of `7e6d882558` computes `b8b16e15…`, so the check compares
like with like.
- Current image:
`ghcr.io/paperclipai/paperclip-daytona-runner@sha256:a84579522192de0393ae24d09e67c4e752df4c701bd315ff54ad9bbca42e2dc0`
(source `5d25117ffa`, content `4e9a221f…`). It matches this branch, and
the receipt shows the content ID match.
- Paid waves, `--env daytona --adapters codex,claude --tasks 3
--concurrency 2 --faults K1 --seed 11`:
- Wave 1 (10 vCPU org, 4 vCPU sandboxes): 0/3. Every create returned
HTTP 400 "Total CPU limit exceeded. Maximum allowed: 10", reproduced
directly with the SDK. This led to the vCPU budget, and the
archived-sandbox fix in R6.
- Wave 2 (same org, 2 vCPU sandboxes): **3/3 PASS**, all
restart-crossing. Codex completed through one working Retry; both Claude
tasks continued automatically. R6: 4 sandboxes archived after release, 0
orphans, 0 held leases.
- Wave 3 (company org, 4 vCPU sandboxes): 0/3. Two product failures were
found and reported to the campaign ledger: an ACPX Claude recovery
blocked on `ambiguous_remote_recovery_evidence` with no Retry, and a
false budget hard stop at 10.75¢ of 3000¢ that paused the company. The
harness changed no sandbox. R6 reported 2 held leases, and the
environment delete returned 409 `pending_sandbox_cleanup` because of
them.
- Fleet-org key check: one sandbox with a unique non-`paperclip-create`
name and label was created, then deleted by the exact ID that create
returned.

## Risks

- This change touches the test harness only, with no product code.
- The extracted provider pack is about 3 GB per image in the gitignored
results directory. Delete old `results/daytona-images/<digest>`
directories by hand.
- The harness never changes Daytona sandboxes. If Paperclip's
environment cleanup is blocked, sandboxes stay until their 60-minute ttl
or a person acts. The harness reports them in `sandbox-sweep.json` and
fails the wave.

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

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, in Claude Code with
tool use (shell, file edits, GitHub CLI). Default context window,
extended thinking.

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 14:26:45 -06:00
DottaandClaude Opus 5.5 b7e28eb2f7 fix(evals): let an allowlisted manual dispatch authorize its private evidence (#19599)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Chat-feature evals run in the trusted `runner-full-stack-e2e`
workflow and keep their evidence in private history
> - A manual chat run needs authority to write that private evidence
> - Today that authority is a stored approval for the exact run ID in
`RUNNER_E2E_MANUAL_PRIVATE_APPROVALS_JSON`
> - The run ID exists only after dispatch, so every manual run stops
until a person writes the record
> - The dispatcher is already checked against
`RUNNER_E2E_ALLOWED_ACTOR_IDS`, so the second step adds friction without
a new decision
> - This pull request lets an allowlisted manual dispatch authorize its
own private evidence, as the TestOps admission already does
> - The benefit is that an allowlisted operator can start a chat eval
and it runs, while scheduled runs still never touch chat evidence

## Linked Issues or Issue Description

Refs #18134, Refs #18170

**What existing behavior does this improve?**
A manual `runner-full-stack-e2e` run with chat-feature IDs fails at
private evidence transfer unless a person first adds an approval row for
that run ID to a repository variable.

**Subsystem affected**
Runner E2E manual workflow private evidence authorization
(`manual-private-transfer.ts`, `runner-full-stack-e2e.yml`).

**Proposed change**
Derive the evidence authority from the dispatch itself, with the same
checks the TestOps chat admission uses.

## What Changed

- New `tests/runner-e2e/manual-dispatch-authority.ts`:
`authorizeManualDispatch` admits private evidence only when all of these
are true:
- The event is `workflow_dispatch` of `runner-full-stack-e2e.yml` on the
default branch, with the `manual` cadence.
  - The run attempt is `1`.
- The actor is in `RUNNER_E2E_ALLOWED_ACTOR_IDS`, the event sender is
that actor, and the triggering actor is the same person.
  - The trusted cell selection equals the dispatch's own `id` input.
- The authority binds the workflow SHA, the target SHA from the
authorize job, the run ID and the cells. Every job derives the same
identity hash.
- `manual-private-transfer.ts`: uses the TestOps authority first, then
the dispatch authority, then the stored approval. With the dispatch
authority, `prepare` records the target launcher digest instead of
comparing it with an approved digest.
- `runner-full-stack-e2e.yml`: the 14 steps that read
`RUNNER_E2E_MANUAL_PRIVATE_APPROVALS_JSON` now set
`PAPERCLIP_MANUAL_DISPATCH_AUTHORITY: v1` and pass
`RUNNER_E2E_ALLOWED_ACTOR_IDS`.
- `doc/manual-e2e-private-evidence.md`: describes the dispatch
authority. The stored-approval shape stays documented for the TestOps
catalog case workflow.
- The new file is separate on purpose. `manual-private-retention.ts` and
the other protocol files must match each target byte for byte, so
existing candidate branches keep working.

## Verification

- `vitest run tests/runner-e2e/manual-dispatch-authority.test.ts
tests/runner-e2e/manual-private-transfer.test.ts
tests/runner-e2e/manual-private-report.test.ts
tests/runner-e2e/manual-private-report-integration.test.ts
tests/runner-e2e/manual-private-policy.test.ts`: 57 tests passed.
- New tests cover one valid dispatch and rejection of: a scheduled run,
a bounded cadence, a non-allowlisted actor, an empty allowlist, a rerun
by another person, a second attempt, a non-default or different workflow
ref, a sender who is not the actor, a missing target SHA, a missing
authority switch, and cells that differ from the dispatch input. The
eight-cell limit still applies.
- A `prepare` test runs the real transfer script: a non-allowlisted
actor produces no outputs, and an allowlisted dispatch produces the
session policy and an identity that records the target launcher digest.
- Negative control: with the previous `manual-private-transfer.ts`, the
new `prepare` test fails.
- Workflow tests that read this workflow: 111 of 116 passed. The 5
failures in `workflow-target.test.ts` (`IMAGE_ONLY: unbound variable`)
also fail on unchanged `master`. `paid-runner-routing.test.mjs` passed
(7 tests).
- I did not run a live dispatch. The first live proof is a manual
dispatch of chat IDs after merge.

## Risks

- The dispatcher now vouches for the target branch. A target branch with
a changed launcher can write private evidence if an allowlisted operator
dispatches it. The protocol files must still match the trusted copies
byte for byte.
- The eight-cell limit is unchanged. The report job has eight fixed read
slots, and each read policy must fit the 2,048-character session policy
limit. Lifting the limit needs a separate change to the report sessions.
- The `runner-e2e-paid` environment still has required reviewers. That
is a repository setting and this change does not touch it.
- The authority uses `RUNNER_E2E_ALLOWED_ACTOR_IDS`, not `CODEOWNERS`.
Keep that variable aligned with the people who may start paid runs.

> 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, 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 14:26:31 -06:00
DottaandPaperclip b55d8a9201 fix(runner): retain admitted OpenCode pack on ordinary continuation (#19558)
## Thinking Path

> - Paperclip manages AI agents and preserves their work across runs.
> - Native OpenCode turns can restore a saved provider session after the
prior daemon stops.
> - The Rust provider binds that saved state to exact qualified launch
paths and artifact bytes.
> - Ordinary continuation selected the current provider pack, although
live reattachment retained the admitted pack.
> - A new proxy or a diagnostics setting could therefore replace the
launch profile behind saved state.
> - This pull request retains the recorded pack for ordinary OpenCode
continuation with a verified durable binding.
> - The benefit is compatible continuation without weakening artifact
integrity or granting replay authority.

## Linked Issues or Issue Description

Refs: #17512, #18237. These added saved authority and compatible pack
reuse. This change covers ordinary OpenCode continuation.

**What happened?**

An ordinary remote OpenCode continuation could select a newer pack while
restoring its existing provider session. The Rust recovery guard then
rejected the changed launch profile. The same error can occur after a
controller release changes the bundled proxy.

**Expected behavior**

A continuation with a valid saved binding and pack receipt uses the same
admitted artifacts. If those artifacts are unavailable, it holds the
saved state instead of selecting another pack or creating a provider
session.

**Steps to reproduce**

1. Admit a remote OpenCode session with a qualified protected provider
pack.
2. Persist its pack receipt and suspended durable Runner state.
3. Change the controller bundle to a valid pack with a different proxy
digest.
4. Start an ordinary continuation with diagnostics enabled.

The new executor regression fails before the repair because selection
rejects the available original pack in favor of the new bundle. It
passes after the repair.

## What Changed

- Retain the saved provider pack for ordinary OpenCode continuation with
a verified durable session binding.
- Keep provider-pack retention independent of surviving-daemon identity:
a new daemon records its current binary, and an authorized continuity
break restores normal compatible pack selection.
- Test retained pack selection after a proxy upgrade and fail closed
when only a newer pack is available.
- Test the real Rust executor with changed, valid proxy file bytes. It
rejects drift without changing saved state and accepts the original
artifacts again.
- Document the selection rule and its limits.

## Verification

- Initial prevention focused Vitest: 220 passed across native session
execution, pack authority, pack verification and pack preparation. The
executor regression was observed failing before the production change.
- Rust native provider tests: all five OpenCode tests passed. These
include the existing drift guard, actual changed proxy bytes, original
artifact restoration and restart-lost turn behavior.
- Advisory review follow-up: reproduced stale daemon identity in the
actual executor and incompatible pack selection after a governed
continuity archive. Both fail before their corresponding repairs and
pass after them. Added an executor regression for a missing authority
receipt; it remains at the current release integrity gate without
creating a new receipt.
- Updated head `639c080856` integrates master `17901d74ba` by normal
merge. Affected Vitest: 202 passed across four files, including all new
review regressions; all seven server typecheck layers passed. Exact-head
CI and fresh Greptile/Paperclip review are pending. The prior head had
57 successful checks plus successful Snyk and Greptile 5/5; those
results do not qualify this revision.
- Environment: macOS arm64, Node 26.4.0, locked pnpm 9.15.4
dependencies, controlled remote command boundary and real Rust provider
executor. No paid provider calls or hosted retries.
- This is a bounded prevention change. It does not deliver historical
provider artifacts to a replacement sandbox or replacement controller.
Missing saved receipts or unavailable old artifacts remain a separate
cold-recovery gap. No full hosted recovery is claimed.

## Risks

- A continuation now requires its exact recorded pack when it has a
durable binding. A newer installed pack cannot substitute for missing
historical bytes.
- Fresh launches, other providers and explicit continuity replacement
keep their existing paths. Current ownership, company, model, permission
and receipt binding checks remain in effect.
- The repair does not relax the Rust launch-profile guard, replay prior
tool effects, or authorize a new provider conversation. Existing
published review receipts remain history.

## Model Used

OpenAI Codex, GPT-6 family. The host does not expose the exact model ID
or context window. Capabilities used: reasoning, code editing, shell
execution, tests and authenticated read-only browser diagnostics.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 14:21:17 -06:00
DottaandClaude Opus 5.5 744056ecc8 feat(evals): run the staging-soak reliability cases from a TestOps plan (#19549)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The runner full-stack E2E lane runs real Paperclip work against real
agents, and TestOps launches its standing plans
> - PR #19455 added seven staging-soak reliability cases, but no TestOps
plan or workflow lane selects them, so they run only by hand with
personal keys
> - These cases catch restart, interrupt, queue and lost-runner
regressions, so they must run from TestOps before a release with the
standard reports
> - This pull request adds a sibling "Reliability — staging soak"
TestOps plan on the existing plan-2 lane, with native OpenCode and
DeepSeek 1017 through OpenRouter as the plan profile for six of seven
cases
> - The benefit is a cheap, repeatable pre-release reliability launch
with the same Allure/TestOps results, retained report and dashboard as
plan 2

## Linked Issues or Issue Description

Refs #19455

**What existing behavior does this improve?**
The staging-soak reliability cases from #19455 run only by a manual
local command.

**Subsystem affected**
Runner full-stack E2E (`tests/runner-e2e/`) and its TestOps lane in
`.github/workflows/runner-full-stack-e2e.yml`.

**Current behavior**
No TestOps plan includes the cases. The workflow cannot select them from
TestOps. Every cell uses native Codex (`gpt-5.6-sol`) or an ACPX facade
on Daytona, which is expensive.

**Proposed behavior**
A TestOps launch of the Reliability plan runs seven cells (six DeepSeek
1017, one native Codex for steering) through the standing plan-2 path
and reports them like plan 2.

**Reason and benefit**
The cases become a first-class, low-cost, pre-release check with
retained evidence. Plan 2 keeps its 20-case qualification semantics.

**Breaking changes**
None. Plan 2 selection, reporting and history identities stay the same.

## What Changed

- **Plan choice: a sibling plan, not an extension of plan 2.** Plan 2 is
bounded at 20 standing IDs. Its `full-run` / `test-plan-2` tag needs a
complete 20-case attempt, and subset runs do not qualify it. Adding
seven cases would change that denominator and the qualification history.
The sibling plan shares the same TestOps GitHub job, selection,
admission, projection, upload and verification code. A launch selects
cases from exactly one plan (`plan_mixed` fails), and each plan has its
own full-plan size (20 or 7).
- `tests/runner-e2e/reliability-testops.mjs` (new): the mapping module.
It has seven cases, full names `paperclip.Product
E2E.everyday-workflows.<case>`, execution IDs, soak signatures and an
empty `reliabilityRegistration`. A partial, duplicate, plan-2 or
malformed registration is rejected. An empty registration makes
selection fail closed before any spend
(`product_regression_testops_reliability_unregistered` for a bootstrap,
`case_mismatch` for a TestOps launch). No TestOps IDs are invented.
- `product-regression-testops.mjs` / `-cli.mjs`: select a plan by its
case identities or by the new bootstrap input. The projection binds each
cell's own environment (`local` or `daytona`) instead of a fixed
`local`. It adds a `soak-<signature>` tag and reports `plan` and a
per-plan `fullPlan`. Plan-2 history IDs, parameters and results do not
change.
- Catalog: `runner-opencode-deepseek-1017` is now one shared fixture
with `supportedEnvironments: ["local", "daytona"]`. The OpenCode server
driver already runs on Daytona for `runner-opencode` (task-workspaces,
core-compatibility, rolling deployment); only the model differs. The
chat-feature suite stays local, and its definition hash does not change.
`everyday-workflows` gains exactly six DeepSeek plan cells (definition
version 12, 52 + 13 + 3 cells).
- Cell matrix (plan), DeepSeek 1017 on native OpenCode:
`interrupt-redirect`, `reassign-midrun` and `tree-pause-release` run on
`local`. `three-turn-revise` runs on Daytona with the per-turn lease
(S49). `long-silent-command` (S52/S52b) and `queued-across-restart`
(S55/U9) run on the Daytona warm lease.
- `queue-steer` is the one exception. It must steer an active turn, and
the OpenCode server driver declares `steering: false`, so the route
answers `steering_unsupported` and the case cannot pass. The plan uses
the existing steering-capable
`everyday-workflows.runner-codex.daytona.queue-steer` cell for it
(Greptile finding). No DeepSeek `queue-steer` cell exists. The other
native Codex Daytona cells and the ACPX facades stay selectable through
`--group reliability`, but they are outside the plan.
- `runner-full-stack-e2e.yml`: adds the `testops_plan` bootstrap input
(`product-regression` by default), sets the plan-specific bootstrap
launch name and tags, and renames two jobs to plan-neutral names. The
OpenCode steps now match `startsWith(matrix.profileId,
'runner-opencode')`, so the DeepSeek profile gets the pinned OpenCode
binary, runner TypeScript, the local Node check and the remote provider
pack. Before this change, the DeepSeek profile could not run in this
workflow. `OPENROUTER_API_KEY`, `OPENAI_API_KEY` (only for
`queue-steer`) and `DAYTONA_API_KEY` come from the same
`matrix.credentialName` and `matrix.environmentId` gates that other
cells use.
- `runner-e2e-testops-registration.yml` +
`reliability-testops-registration.mjs` (new): a trusted, master-only,
allowlisted-actor, idempotent `workflow_dispatch`. `inspect` only reads.
`register` reuses cases with the same full name, creates missing cases
and the plan, and assigns the plan to plan 2's GitHub job with plan 2's
job parameters. It writes a receipt with a ready-to-commit
`reliabilityRegistration` snippet. Only this job gets `ALLURE_TOKEN`,
and it never runs a provider cell.
- **No schedule and not per PR.** The plan runs only from a TestOps
launch or a manual dispatch. It is not in the daily or weekly cron
(`scheduledSelection` excludes it). A weekly run would add a paid
50-minute Daytona cell every week, but these signatures move with
release-risky changes, not with time.
- Docs: `EVERYDAY-WORKFLOWS.md` (matrix, environment reasons, local
coverage limits, registration steps), `README.md`, `doc/evals.md`.

### Registration still needed (requires TestOps credentials)

1. Dispatch **Runner E2E TestOps registration** from master with
`operation=inspect`.
2. Dispatch it with `operation=register`.
3. Commit the `reliabilityRegistration` snippet from the run summary
into `tests/runner-e2e/reliability-testops.mjs` in one reviewed PR.
4. Launch the plan from TestOps. Tag only a complete seven-case attempt
`full-run`.

### What local plan cells cannot reproduce

The local cells do not reproduce the Daytona-only variants: the remote
`workspace_busy` save after Stop (S47), remote stop settlement during
reassignment (S48), and a lost or replaced remote runner during a tree
hold (S50). S51 (lost runner) has no case yet. The explicit-only native
Codex Daytona extras still cover S47, S48 and S50 on a sandbox.

## Verification

- `node --test tests/runner-e2e/product-regression-testops.test.mjs
tests/runner-e2e/reliability-testops.test.mjs`: 26 pass. These tests
cover fail-closed selection, partial or duplicate registration, plan
mixing, environment binding, per-plan denominators, preserved failure
classes, the CLI select → project → verify flow for a seven-case launch,
registration idempotency and job checks, and the workflow inputs and
credential gates.
- `vitest` for `reliability-testops-catalog`,
`product-regression-testops-catalog`, `everyday-reliability` and
`catalog.test`: 98 pass. These tests bind the mapping to real catalog
cells (profile, model, environment, credentials, single attempt). They
also check that the cells stay out of `--all`, the suite, profile and
environment selectors and the daily/weekly cron. Real grader output must
survive projection.
- `pnpm test:e2e:runner:typecheck`: pass.
- `pnpm test:e2e:runner -- --list --suite everyday-workflows --group
reliability` lists 16 cells: 6 DeepSeek plan cells, 7 native Codex cells
(one is the `queue-steer` plan cell) and 3 ACPX extras. `--matrix-json`
for the DeepSeek plan IDs emits `needsDaytona=true` and three Daytona
OpenCode cells with `credentialName=OPENROUTER_API_KEY`.
- Full `pnpm test:e2e:runner:unit`: the remaining failures happen the
same way on an unchanged master checkout. They are in
`workflow-security` (3), `controller-outage`, `operating-receipts`,
`pi-*`, `private-framework-report` and `remote-native-bootstrap`. #19529
tracks that drift. This PR adds no failures.
- Not run: paid cells, a TestOps registration and a TestOps launch. They
need provider and TestOps credentials, which exist only in protected CI
environments. Catalog and projection tests are not live qualification.

## Risks

- The registration script calls TestOps APIs (`POST /api/testcase`,
`/api/testplan`, `/assign`, `/jobparameter`) through the same endpoints
as the chat catalog reconcile. Tests use fakes, not the live API.
`inspect` comes first, and `register` is idempotent.
- DeepSeek 1017 on Daytona has not run before. The first launch is the
acceptance step for those three cells.
- If a later case needs another capability that OpenCode lacks, it must
use a capable profile in the same way as `queue-steer`. The catalog test
pins the single exception.
- The OpenCode workflow conditions now match any `runner-opencode*`
profile. Today this covers `runner-opencode` and
`runner-opencode-deepseek-1017` only.
- If someone commits a bad `reliabilityRegistration`, module load fails
for both plans. A unit test rejects any committed registration that is
not empty or complete.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`) through Claude Code, with tool use
(shell, file edits, local 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 14:19:58 -06:00
DottaandPaperclip 0e111c5075 fix(recovery): verify host preparation logs before continuation (#19580)
## Thinking Path

> - Paperclip manages AI agents and the work assigned to them.
> - A task can retain saved input after a failure during execution
preparation.
> - Retry must prove that the old attempt stopped and its files are
saved before it admits a new turn.
> - The host can write a skill announcement before it dispatches a
provider.
> - The historical preparation verifier requires an empty log and
rejects this safe host output.
> - This pull request verifies that exact small log against the saved
skill selection and immutable log facts.
> - Eligible saved input can continue once. Uncertain execution and
current policy gates stay held.

## Linked Issues or Issue Description

Refs: #19004, #19464, #19552. These repairs establish the existing
preparation proof. They do not accept the host skill announcement
described here. A search for the exact announcement found no duplicate
repair. This is a bug fix within the existing recovery contract and does
not duplicate a roadmap feature.

**What happened?**

A legacy preparation failed before provider dispatch with a host
bootstrap certificate. Its remote lease stopped and its files were
recovered. A newer preparation failed while that save was in progress.
An accepted Retry then remained in execution recovery because its
ancestor had one host skill log record.

**Expected behavior**

Admit one fresh turn for the existing saved user request after all
execution, workspace and policy proofs pass. Preserve old outcomes and
source records.

**Steps to reproduce**

1. Create the real PostgreSQL fixture in
`saved-bootstrap-continuation.test.ts`. It includes a recovered remote
workspace, exact stop receipt, bootstrap ancestor, later failed
preparation and saved user status queue.
2. Call `requestTaskRetry`, then call the real resume path concurrently.
3. On the baseline guard, the empty-log control dispatches. Adding only
the host skill announcement leaves zero queued successors and the same
execution-recovery wait.

**Paperclip version or commit**

Reproduced on master `65aee998a3`. The final branch rebases cleanly onto
`c650f36007`.

**Deployment mode**

Source checkout with real embedded PostgreSQL and controlled log
storage. The reported runtime is a server with remote sandbox
workspaces.

## What Changed

- Add a narrow verifier for one host skill-announcement record. It binds
the complete bytes to the run log reference, digest, size, sequence,
timestamp, output excerpt and recorded skill selection. Each announced
skill must have a saved version; unrelated configured skills may also be
selected.
- Read at most 2,049 bytes with a two-second cancellation deadline.
Missing, slow, changed, additional or provider output fails closed.
- Use this proof only for historical bootstrap ancestors that already
pass the no-provider, settlement, stop and saved-file checks.
- Add real admission regressions for one successor, concurrent resume,
duplicate Retry and delivery of the original saved comment. Add
current-gate and storage-boundary negative cases.
- Document the continuation contract.

## Verification

- **RED:** On the unchanged baseline guard, the exact final fixture
passes the empty-log control and fails the host-announcement case: zero
successors instead of one. The saved comment and Retry remain deferred.
- **GREEN:** 486/486 tests pass across `task-retry.test.ts`,
`explicit-native-continuation.test.ts`,
`saved-bootstrap-continuation.test.ts` and
`verified-preparation-log.test.ts`.
- The new cases cover provider-started evidence, provider events, a
native coordinator, missing stop proof, missing skill selection, changed
hash, unknown output, pending approval and operator Stop.
- Read-boundary cases cover foreign references, changed bytes,
truncation, extra records, malformed selection and cancellation. An
unrelated recovery completes while another read is stalled.
- After the rebase, the original 27 new cases passed again at head
`be640292fe136232e3db0dc6938233f53b6ccb43`. The review fix adds three
cases, and the full 486-test pass includes all 30 new cases.
- The exact previously observed historical input also passes the
production log verifier: one read, 240 bytes matching the stored size
and SHA, the same timestamp and output sequence, and a recorded version
for the announced skill. This uses captured bytes for transport and does
not claim live hosted admission.
- Full local server layered typecheck passes after building this
checkout's Runner declarations and normal excluded-plugin SDK links. The
typing correction also passes all 30 new cases and the actual captured
historical log proof. The prior CI build/typecheck failure was TS7006 in
the JSON parser; it is retained as failed evidence and corrected in this
revision.
- Module boundary and whitespace checks pass. Local verification
selection requires broad CI because the execution contract changed;
focused local suites are recorded above. [Required final-head
CI](https://github.com/paperclipai/paperclip/actions/runs/42281590449)
typecheck, build and tests pass at
`c557cdc4dc57000cb478f9be0717675dcac7364e` (56 successful checks, no
required check pending or failed). Greptile rates this final revision
5/5; the addressed thread is resolved. The advisory hosted Paperclip
Review is paused with its request saved and has not supplied a
final-head assessment. Its pending status is disclosed separately from
required CI and Greptile.
- Controlled fixtures exercise the admission and dispatch receipts.
Hosted continuation still requires rollout and reassessment of the
existing request.

## Risks

This adds one bounded storage read to an active historical admission
check. Failed or uncertain reads keep the existing hold. The host
bootstrap certificate remains the primary no-provider proof. Log text
alone cannot authorize continuation. No schema, retry budget, provider
setting, account selection or 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 14:19:31 -06:00
DottaandPaperclip 96da284ed8 fix(workspaces): attach latest recovered files to Retry (#19542)
## Thinking Path

> - Paperclip manages work performed by AI agents.
> - A recovered run must retain its saved files when the user presses
Retry.
> - A prior fresh attempt can leave the task bound to a different
workspace.
> - Remote preparation restored older recovery attachments but omitted
the latest canonical snapshot.
> - The provider also received recovery paths only for fresh workspace
attempts.
> - This change attaches the latest verified snapshot and includes its
paths in the normal continuation context.
> - The selected workspace, its files, and execution admission rules
remain intact.

## Linked Issues or Issue Description

Refs #19479, #19340.

**What happened?**

Retry could start with an older recovery attachment while the newest
saved files stayed in another workspace. A real database regression
restored an older one-line log and omitted a newer verified twelve-line
log. Workspace selection alone did not reveal this failure.

**Expected behavior**

The provider can find and read the newest verified saved files. Recovery
preserves the selected workspace and existing files. Missing or invalid
recovery proof stops preparation.

**Steps to reproduce**

1. Recover a failed run into a canonical remote snapshot.
2. Retain a later selected workspace with an older recovery attachment.
3. Press Retry and inspect the actual files and provider continuation
paths after preparation.

**Paperclip version or commit**

Reproduced on master `5d25117ffad69a93fb92026950b9acd9cec08f69` with
synthetic data and local PostgreSQL 17.

## What Changed

- Follow the admitted run's committed recovery ancestry as well as
retained source IDs.
- Import canonical snapshots as separate `.paperclip-recovered/`
attachments. Reuse snapshot decryption, hash checks, and manifest
validation.
- Check the current writer, company, task, immutable committed snapshot
receipt, and source access before import. Read-only historical proof
remains valid when a later writer advances the source; execution
admission proof is unchanged.
- Preserve older attachments and later selected workspace files.
Repeated imports retain the same path and preserve user edits.
- Include only physically present recovery paths in ordinary provider
continuation context, using the caller’s storage backend.
- Document the contract and add actual-file regressions and negative
evidence cases.

## Verification

- Before the fix, normal Retry admission and remote preparation restored
only the older one-line log. The newest twelve-line archive was
independently readable but absent from the prepared workspace.
- With the original context projection, full heartbeat workspace
preparation restored the files but omitted their paths from provider
context. The regression failed on the missing newest path.
- All 28 focused real-database cases pass together at the final local
revision (46.32s). An earlier run had one asynchronous comment-admission
poll failure before workspace preparation; its bounded rerun passed, and
the fresh final 28-case run also passed without timeout or assertion
changes. These cover actual Retry admission, remote preparation,
provider prompt rendering, missing and corrupt evidence, company and
task scope, duplicate imports, and later workspace files.
- A controlled local child reads the path from provider context and
reports twelve lines with the expected first and last timestamps. It
does not run a loop or write the log.
- `node node_modules/vitest/vitest.mjs run
server/src/services/workspace-recovery.test.ts -t 'restores the newest
recovered tick bytes|canonical recovery attachments|deduplicates
immutable canonical sidecars|caller.s storage backend|restores ordinary
same-workspace Retry|admits one HTTP Retry|keeps verified
copyback|prepares the .* task workspace|continues recovered preparation'
--fsModuleCache` runs against a dedicated local PostgreSQL 17 test
database.
- Cold save and restore on a new allocation preserves user edits inside
an existing sidecar. Both review findings failed before their fixes.
Incomplete prepared or missing-snapshot references now stop preparation.
- An upgrade regression reproduced valid legacy and canonical sidecar
paths disappearing on same-allocation reuse. The fix rebuilds expected
proofs from existing scoped receipts and checks the live destination,
preserving user edits without copying files on that live allocation. The
same actual-file case also saves the parent workspace, reserves it for a
child, preserves both legacy and canonical paths, and refuses a revoked
receipt. For related-task sidecars, retention verifies the selected
workspace’s succeeded migration history, exact source run/task entry,
and matching workspace and source-run markers. When legacy migration
markers are absent, a verified committed snapshot of the selected
workspace can establish the same exact provenance. It is read once per
preparation with the existing decryption, hash, and manifest validators;
the live workspace is never modified. Task relationships alone grant no
access; fresh import stays scoped to the current task. Missing or
mismatched migration history fails closed. The retained-path checks live
in the attachment owner; remote-workspaces.ts remains below 1,000 lines.
- Further regressions failed before the fix for deleted-sidecar context,
caller-supplied storage, and a legitimately advanced source workspace.
The fixed cases preserve edited sidecars and ordinary same-workspace
Retry restore.
- Real non-root regressions reproduced EACCES when cleaning read-only
directories in import staging and snapshot verification.
Permission-aware cleanup fixes both while preserving live 0555 directory
modes and contents.
- The complete remote file-transport suite passes: 123 tests, including
six real-command provenance cases for absent, matching, foreign,
symlinked, and hard-linked proof.
- Server typechecks include the CI layered configuration, after building
the current Runner TypeScript package and SDK dependency closure. `cd
server && node scripts/typecheck-layers.mjs` passes all seven layers at
the latest head (exit 0).
- macOS arm64, Node 26.4.0, repository-pinned pnpm 9.15.4. The initial
embedded PostgreSQL startup timed out before tests; the passing runs use
a disposable PostgreSQL 17 container with temporary storage. No timeout
was increased.
- Required CI passes at `42495c7a73f3716240987fa403af7511fc9df9b9`: 55
successful checks, three expected skips, no failures; [aggregate
verification](https://github.com/paperclipai/paperclip/actions/runs/42282134772/job/128121560893)
passes. Greptile is 5/5 with zero unresolved threads. The separate
hosted Paperclip Review remains queued and is disclosed as pending. This
change has not been deployed; its affected hosted workflow remains held.

## Risks

Canonical snapshot import adds bounded storage and archive work during
active preparation. Upgrading an older related-task sidecar without
migration proof may verify the selected workspace archive once; its
temporary copy is removed and later runs reuse the recorded provenance.
Missing objects, invalid receipts, or archive corruption stop
preparation. Imports use separate directories and do not replace
selected workspace files. Existing admission, Stop, budget, pause, and
provider replay rules still apply.

## Model Used

OpenAI Codex, GPT-6 family, with repository analysis, code editing, and
tool execution. The runtime-specific model ID and context window are not
exposed in this session.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [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 14:19:24 -06:00
DottaandPaperclip 0b48bc7ac5 test(connections): isolate scheduled GitHub history scans (#19541)
## Thinking Path

> - Paperclip lets people manage AI agents and their service
connections.
> - Connection identity reads must use the authenticated caller's
current account grants.
> - This integration suite runs the real HTTP routes and bearer
authentication against provider fixtures.
> - An earlier GitHub App fixture stays active after its test ends.
> - Its history worker can run 60 seconds later against the next test's
shared provider handler.
> - This pull request keeps that unrelated worker inactive in this
suite.
> - The identity test keeps its original provider-count and
authorization assertions.

## Linked Issues or Issue Description

**What happened?**

The Anthropic identity integration test reported three provider calls
instead of two. The denied request returned `connection_unavailable`
correctly. A controlled reproduction traced the third call to GitHub's
`/app/hook/config` from an earlier test's webhook recovery worker.

**Expected behavior**

The suite must measure requests from the operation under test.
Foreign-company, unknown-grant, and revoked-grant requests must not
contact the identity provider.

**Steps to reproduce**

Run the GitHub App import fixture before the identity test. Initialize
the saved GitHub recovery epoch. Advance only the fixture Date by 60,001
ms, then run the actual current recovery check after the two authorized
identity reads. The original final count assertion fails with three
calls. The extra call's stack runs through `readGitHubAppWebhookConfig`,
`recoverGitHubWebhookEndpoint`, and the recovery schedule's `check`.

Related implementation: #17666. No duplicate fixture repair was found.

## What Changed

- Keep automatic GitHub webhook history work inactive in this
connection-management test suite.
- Keep real connection setup, HTTP routes, database state, agent API
keys, JWT authentication, and all existing identity assertions.
- Leave production registration and the dedicated chat-channel recovery
tests unchanged.

## Verification

- Exact baseline `5d25117ffad69a93fb92026950b9acd9cec08f69`: the
unchanged focused identity test passed.
- Controlled red on that baseline: the original final
`expect(calls).toBe(2)` failed with three. The third URL was
`https://api.github.com/app/hook/config`. The revoked identity response
stayed unavailable.
- Local provider traffic uses fixture responses. No real provider
credentials or requests were used.
- Full connection-management suite: **90/90 passed** on the exact
release baseline plus this fixture fix (commit `e33c091438`). Command
from `server`: `../node_modules/.bin/vitest run --config
vitest.config.ts --fsModuleCache
src/__tests__/connection-management.integration.test.ts`.
- Rebased onto current master. A fresh GitHub-import plus identity pair
passed **2/2** on final head `95ed6147920e48128892744e1a4f220373f7da5b`
after removal of the added comments required by review. Greptile
reviewed that exact head at **5/5**. The addressed comment thread is
resolved. Required CI remains pending.

Final reviewed head: `95ed6147920e48128892744e1a4f220373f7da5b`.
[Required
CI](https://github.com/paperclipai/paperclip/actions/runs/42272042111)
passed (54 successful checks, three expected skips and the completed
neutral Paperclip Review check; none pending or failed). Greptile and
Paperclip Review both rated the change 5/5 with no remaining findings.
The addressed review thread is resolved. The first attempt's canary
runner communication loss and unrelated cold-import timeout are retained
as failed evidence; the unchanged cold-import suite passed 4/4 locally,
and only failed CI jobs were rerun successfully.

## Risks

Low risk. This changes test isolation only. The connection-management
suite no longer runs unrelated GitHub history scans. Dedicated
chat-channel integration tests still cover those scans. No authorization
check, assertion, deadline, or production behavior 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 14:19:17 -06:00
DottaandClaude Opus 5.5 840838476a perf(runner-e2e): cut the Daytona image build critical path (#19545)
## 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 14:15:16 -06:00
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