Commit Graph
6757 Commits
Author SHA1 Message Date
DottaandClaude Opus 5.5 fb1b458409 fix(runner-e2e): drop the Rust dependency layer that filled the image builder disk (#19669)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Runner Full-Stack E2E runs Daytona cells from a signed,
content-addressed runner image
> - #19545 made the image build faster. One part of it added a Rust
dependency layer that keeps Cargo's target and registry trees in the
builder
> - The first default-branch build after the merge ran out of builder
disk while it exported the registry cache
> - That run pushed the image tag before the failure, so the tag has no
signature, and campaigns that reuse it fail `cosign verify`
> - This pull request removes only the dependency layer and keeps the
parts of #19545 that add no disk
> - The benefit is a working default-branch image build again, a new
content ID that replaces the unsigned tag, and the measured 16.5 to 11.6
minute improvement

## Linked Issues or Issue Description

Refs #19545. Related #18494 (the same builder disk limit).

**What happened?**

The default-branch `image_only` dispatch, run 42286903290 (job
128202722275), built and pushed
`ghcr.io/paperclipai/paperclip-daytona-runner:e2e-content-7154c0dfc9abb297d8d638a982326c1435dc4440c25112afacc559712d4430b3`
(`sha256:2767e91c5279...`). The registry cache export then failed with
`ResourceExhausted: ... no space left on device` (exit 102). `preparing
build cache for export` took 182 s, against 87 s before #19545. The job
stopped before `cosign sign`. The dependency layer retained `target` 852
MB and Cargo registry 333 MB (measured in run 42271236140).

**Expected behavior**

Default-branch image builds publish the image, write the cache, and sign
the digest within the builder disk.

**Steps to reproduce**

Dispatch `runner-full-stack-e2e.yml` from master with `-f
image_only=true` and a Daytona selector on `840838476a`. Read the `Reuse
or publish immutable image` step log.

## What Changed

- `docker/daytona-runner/Dockerfile`: remove the placeholder-target
dependency layer. The runnerd stage is again the single build layer from
before #19545, so its cache key matches the earlier default-branch
cache. Restore and extend the disk comment.
- Keep the content-ID check above the `COPY --from` steps and keep `COPY
--link`. They add no retained data.
- `tests/runner-e2e/README.md`: describe the two remaining layout rules
and why there is no dependency layer.
- The Dockerfile is a content-ID input, so the next build gets a new
tag. No campaign reuses the unsigned tag.

## Verification

- `vitest run tests/runner-e2e/daytona-image.test.ts
tests/runner-e2e/workflow-image-only.test.ts`: 20/20 pass.
- Branch evidence for the kept parts: job 128077687214 took 11.6 min,
against 16.5 min before. The cached-layer download overlapped the
compile.
- After merge, an `image_only` dispatch from master must publish, write
the cache and sign. I did not use a dispatch before merge, because only
a default-branch build writes the cache, and the cache export is what
failed.

## Risks

- Runner source changes again recompile the full Rust dependency graph
(about 5 min). To get that saving back, the builder needs more free disk
first (for example, remove unused preinstalled toolchains in the image
job, or use a larger RunsOn volume). That is a separate change.
- The unsigned tag stays in GHCR. Nothing selects it after this change,
because the content ID changes.

## 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 17:49:39 -06:00
DottaandClaude Opus 5.5 07a548073a fix(ui): keep inline document links in the text line; plain-text request previews (#19639)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents talk to people in task chat. Their messages render as
markdown.
> - In an onboarding walk, two chat papercuts made agent text hard to
read.
> - First: a document link inside a sentence ("Your [plan is saved],
approved, ...") rendered as a padded card. The card is taller than the
text line, so it overlaps the lines around it.
> - Second: the one-line row above a pending review card showed the
agent's raw markdown summary (backticks, "- ", "**").
> - This pull request renders document links in running text as an
inline chip, and flattens the collapsed preview to plain text.
> - The benefit is that agent messages and pending-request rows read
cleanly.

## Linked Issues or Issue Description

**What happened?**
1. In task chat, an agent wrote: "Yes—we completed the onboarding
interview here in chat. Your [plan is saved](#document-plan), approved,
and handed off to execution task PIX-3." The "plan is saved" link
rendered as a 42px-tall bordered card in a 20px text line. It overlapped
the text above and below and left the text baseline.
2. The collapsed row above a "Review the creative brief (v1)" card read:
"The `creative-brief` document is saved on this task. It covers: -
**Brand voice and audience:** ..." with literal markdown syntax.

**Steps to reproduce**
1. Open a coordinator task chat where an agent message has a
`#document-...` link inside a sentence. Or open the Storybook story
`coordinator-conversation--inline-document-link`.
2. Open a task with a pending request whose summary contains markdown.
Or open
`composer-interaction-above-composer--pending-request-markdown-summary`.

**Expected behavior**
1. The document link sits in the text line like other inline links.
2. The collapsed preview shows the words as plain text.

## What Changed

- `SidebarDocumentLink` gets a `variant` prop. `card` (default) is
unchanged and is still used by the standalone plan preview card.
`inline` is a text-flow chip: `display: inline`, horizontal padding
only, a small muted background, and a 14px icon aligned to the text
bottom. Token classes only.
- `MarkdownBody` uses the `inline` variant for both document-link paths
(coordinator issue document links and `#document-...` hash links). The
label no longer uses `truncate` (nowrap), so long labels wrap with the
sentence.
- New `ui/src/lib/markdown-plain-text.ts` (`markdownToPlainText`): drops
code ticks, emphasis/strikethrough markers, list/heading/quote/task-box
prefixes, fences, rules, link/image targets and inline HTML tags, keeps
snake_case intact, and collapses whitespace. Unit tests included.
- `TaskChatInteractionCard` flattens the collapsed pending-request
summary and the unanswered-question prompt with `markdownToPlainText`.
- Stories: `Coordinator conversation / Inline document link` (+ dark)
with a play check that the link is no taller than the line and starts
inside the paragraph; `Interaction above composer / Pending request
markdown summary` with a play check that the row has no markdown syntax.

## Verification

Red on master (`e482f0f632`), then green with the fix:

```
# Red (master + new tests)
 × renders a document link inside a sentence as inline text, not a padded card
   AssertionError: expected 'inline-flex max-w-full items-center gap-2 rounded-lg border border-border bg-muted/25 px-3 py-2.5 ...' not to match /(^|\s)(inline-flex|flex|block|py-\S+|p-\S+)(\s|$)/
 × shows the collapsed pending-request preview as plain text, not raw markdown
   AssertionError: expected 'Review the creative brief (v1)The `cr…' to contain 'The creative-brief document is saved …'
 Tests  2 failed

# Green (fix)
 Tests  7 passed  (the 2 above + 5 markdownToPlainText unit tests)
```

Browser geometry check (Playwright against Storybook, story
`coordinator-conversation--inline-document-link`, light and dark). It
measures the link box against the paragraph line-height and the text
node before the link:

```
# Before
light: FAIL lineHeight=20 chipHeight=42.0 chipTop=61.7 chipBottom=103.7 textTop=76.7 textBottom=93.7
dark:  FAIL lineHeight=20 chipHeight=42.0 chipTop=61.7 chipBottom=103.7 textTop=76.7 textBottom=93.7
# After
light: PASS lineHeight=20 chipHeight=17.0 chipTop=62.7 chipBottom=79.7 textTop=62.7 textBottom=79.7
dark:  PASS lineHeight=20 chipHeight=17.0 chipTop=62.7 chipBottom=79.7 textTop=62.7 textBottom=79.7
```

The story's own play function also failed on master in the browser:
`AssertionError: expected 42 to be less than or equal to 20`.

Inline document link, before / after (dark, then light):

![chip before
dark](https://raw.githubusercontent.com/paperclipai/paperclip/bbbeaadb3fb5ff2521edfe9434a31389bf8dfd50/inline-chip-preview/chip-before-dark.png)
![chip after
dark](https://raw.githubusercontent.com/paperclipai/paperclip/bbbeaadb3fb5ff2521edfe9434a31389bf8dfd50/inline-chip-preview/chip-after-dark.png)
![chip before
light](https://raw.githubusercontent.com/paperclipai/paperclip/bbbeaadb3fb5ff2521edfe9434a31389bf8dfd50/inline-chip-preview/chip-before-light.png)
![chip after
light](https://raw.githubusercontent.com/paperclipai/paperclip/bbbeaadb3fb5ff2521edfe9434a31389bf8dfd50/inline-chip-preview/chip-after-light.png)

Collapsed pending-request preview, before / after:

![preview before
dark](https://raw.githubusercontent.com/paperclipai/paperclip/bbbeaadb3fb5ff2521edfe9434a31389bf8dfd50/inline-chip-preview/preview-before-dark.png)
![preview after
dark](https://raw.githubusercontent.com/paperclipai/paperclip/bbbeaadb3fb5ff2521edfe9434a31389bf8dfd50/inline-chip-preview/preview-after-dark.png)
![preview before
light](https://raw.githubusercontent.com/paperclipai/paperclip/bbbeaadb3fb5ff2521edfe9434a31389bf8dfd50/inline-chip-preview/preview-before-light.png)
![preview after
light](https://raw.githubusercontent.com/paperclipai/paperclip/bbbeaadb3fb5ff2521edfe9434a31389bf8dfd50/inline-chip-preview/preview-after-light.png)

Neighbouring suites: `npx vitest run src/components/MarkdownBody
src/components/task-chat src/lib/markdown-plain-text.test.ts
src/components/MarkdownDocumentCardStyles.test.ts
src/components/MarkdownExternalLink.test.tsx` → 67 files, 1001 tests
passed.

Also run: `pnpm check:token-gates` (clean), UI `tsc -b` (no errors in
changed files; the only errors were an unbuilt local
`@paperclipai/plugin-sdk`), `node scripts/storybook-catalog.mjs`. `pnpm
verify` exited 2 (story file is a broad input), so broad coverage comes
from required CI.

## Risks

- Low risk. UI-only styling and text change.
- Document links in markdown now look like inline links with a small
icon instead of a card. A link that sits alone in its own paragraph also
renders inline. The standalone plan preview card keeps the card style.
- `markdownToPlainText` is a regex flattener for one-line previews, not
a full markdown parser. An unusual construct can keep a stray marker. It
does not change stored data.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 17:41:07 -06:00
c82211251b test(chat): cover Slack forwarding deadlines through registered owners (#19348)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - People answer agent questions through Slack commands, buttons, and
forms.
> - Slack requires a response within three seconds. A modal trigger also
expires after three seconds and can be used once.
> - Durable event delivery does not prove these synchronous response
paths.
> - This pull request exercises the registered app owners and pinned SDK
with original signed requests and controlled forwarding delays.
> - The tests record deadline failures without clearing invalid forms,
extending triggers, or changing production behavior.

## Linked Issues or Issue Description

Refs #19019, which has merged. This PR now targets `master`.

Related work: #8300 handles raw-body capture and plugin event fan-out.
#19328 handles Slack connection onboarding. This PR changes neither
path. I searched related open and closed work and read those PR bodies
and changed paths.

**What happened?**
A forwarding delay can leave an authenticated Slack callback too late to
acknowledge a command, exchange a modal trigger, or return field errors.
The existing direct modal test did not cover the normal registered
ingress or these provider deadlines.

**Expected behavior**
Tests must distinguish useful app persistence from a response that Slack
can still accept. They must preserve current authorization, field
validation, original signatures, and exactly-once effects.

**Steps to reproduce**
Use an owned PostgreSQL test database and the repository launcher below.
Run warm first. The default run adds one real 3500ms delay independently
before command, form-open action, and invalid submission forwarding.

**Paperclip version or commit**
Production remains byte-identical to
`54fa701d41b4ced62f4ff338d39b946015670ae3`. This test-only candidate is
`a3cce4e9868fd3e6e25d9df7d0090899bdf6a204`. It adds a recovery-fixture
setup guard after `dac0010649c435f673172c496b3383e99780e8bf`.

**Deployment mode**
Source-checkout integration test with real PostgreSQL, registered app
owners, and the pinned Slack SDK. Provider API responses and the
heartbeat receipt helper are controlled fixtures. No Slack account,
model process, deployment, or physical tenant suspension was tested.

## What Changed

- Route original signed command, action, and modal requests through the
normal chat service and real SDK runtime.
- Check forged-signature refusal, callback evidence, current-role
refusal, invalid-field errors, one corrected answer, and duplicate
fencing.
- Intercept the pinned SDK HTTP transport. Model trigger expiry and
one-use exchange without changing request timestamps or signatures.
- Observe the real platform exception through the actual bound
`views.open` callable. Delegate once and rethrow the same error.
- Restore the network guard after every setup failure. Attempt all owned
cleanup and preserve the original error before any cleanup errors.
- Guard auxiliary database cleanup when recovery test setup fails before
it creates a client. Keep temporary-cluster cleanup reachable and
preserve the original setup error.
- Add a repository launcher with the current prewire guard, one worker,
explicit test-database validation, and closed warm-only and
action-delayed-only selectors. The default still selects all four cases.

## Verification

- Current candidate `c6ae7957988e220ea096ab678f070d3bd1c5f9c0` normally
merges fetched master `8621bcf53ee2e4bbefb7d193add42dd544ca06e8`. All
three owned protocol/recovery acceptance files are byte-identical to the
preceding head. All other paths exactly match master. The shared Runner
default test conflict uses master's independently landed constant-based
repair. That supersedes this branch's temporary two-literal repair; no
fourth file remains in the PR diff.
- Original CI server4/12 had two stale `gpt-5.6-sol` expectations after
the normal default changed to `gpt-6.1-sol`. The complete failure log is
retained. Before the master repair landed, the focused two-literal
correction passed the whole six-case file with explicit-model,
non-Codex, lifecycle, permission and skill checks intact. That local
result is source-specific; it is not a fresh full merged-tree pass.
- The original guard-leak counterfactual reproduced unrestored global
network functions after setup rejection. The repaired callback passed
four controlled cleanup diagnostics. Cleanup attempts each initialized
owner and restoration even after a failure. It preserves the primary
error first and retains separate cleanup errors.
- A fresh registered SQL/SDK warm case passed on the qualified protocol
source with one persisted answer, 17.15 seconds total and normal
original assertions. Final independent SQL and recorded PID/group audits
were empty. A diagnostic sampler initially hit EPERM, and the worker
exit guard line was absent; those limits remain explicit. The cleanup
diagnostics are local qualification tools, not four newly checked-in CI
tests.
- Delayed modes remain explicit diagnostic commands. Ordinary CI runs
warm. Earlier mode results do not establish a final-source four-case
single-command pass. The provider transport is controlled; no live Slack
display, deployed sleep or wake result is claimed.
- The original server6/12 cleanup-backlog case hit its 15-second
whole-case timeout. Its phase and cause remain unknown. The original E2E
shard received runner shutdown without an assertion verdict. Complete
original logs are retained; neither failure has been labeled a flake or
repaired by extending a deadline.
- Own offline dependency setup passed with an unchanged lockfile. An
omitted private guard dependency initially prevented test entry; that
setup failure is retained separately from product results. The immutable
helper closure corrected it. No local full-repository
typecheck/build/test pass is claimed. Fresh exact-head CI and complete
edited bot reviews are required before merge.

## Risks

This is a test-only change. It changes no production code, credential
flow, schema, API, or sleep eligibility. The existing large integration
fixture remains large; this PR does not cross a file from below 1000
lines to above it and adds no code comments.

The provider transport is synthetic. The tests demonstrate deadline
consequences and app persistence, not that Slack displayed a late
response. They do not make commands or open forms safe while the tenant
sleeps. Other Slack lanes and persistent transports remain open work. CI
may expose additional harness assumptions.

The roadmap's Apps and Connections work is unchanged. This focused
regression complements the existing integration; it adds no separate
feature or provider setup path.

## Model Used

OpenAI, configured model `gpt-6.1-sol` in Codex. The exact served
backend revision and context window are not exposed. Assistance used
reasoning, source inspection, Git, native TypeScript checks, and
controlled integration tests. A coordinating GPT-6-family agent reviewed
boundaries and evidence.

## Checklist

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

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native task turns can keep a provider conversation from an earlier
run.
> - A copied conversation checkpoint names the new run before its
controller has attached.
> - A failed attachment can make recovery treat that copied identity as
current execution authority.
> - The current run needs its own durable controller checkpoint, or a
valid staged attachment, before exact resume.
> - This pull request checks that ownership and uses the normal Retry
path to continue a saved user turn after verified cleanup.
> - The task can continue once with its saved work and history.

## Linked Issues or Issue Description

Refs #19399 and #18860.

**What happened?**

A new user comment resumed a completed native conversation. The retained
runner process was gone before the new controller attached. The new run
still had a copied provider session ID. Recovery selected exact resume
for that new run and then failed its unchanged owner and lease checks.
Ordinary first-attempt failure settlement also omitted the saved-user
Retry handoff that restart recovery already uses.

**Expected behavior**

Exact resume requires current controller evidence. A saved user turn can
request one normal Retry after this infrastructure failure. The normal
path must verify provider stop, saved files, current user authority and
all execution gates before it starts a fresh session.

**Steps to reproduce**

1. Finish a native warm run and retain its provider checkpoint.
2. Admit a user comment as a new run and copy the provider checkpoint
through the real resume producer.
3. Stop the retained runner before attachment. The real remote process
check reports that the process is not running.
4. Observe that the copied provider ID selects exact current-run resume.
Observe that ordinary failure settlement saves no Retry request.

## What Changed

- Check a bounded encrypted controller header for fenced remote Runner
recovery. Keep local Runner recovery on its existing disk-backed path.
Match the run, conversation and runner identity. Preserve the existing
staged-attachment path.
- Share the existing saved-user Retry admission between restart recovery
and ordinary terminal native settlement. Keep its exact infrastructure
errors, user wake, comment, provider-turn and retry-chain checks.
- Add real PostgreSQL and stopped-process regressions. Verify malformed
and foreign checkpoint holds, current-owner resume, user authority and
Stop. Verify saved-file recovery creates one fresh-session successor.
- Document the ownership and Retry contract.

## Verification

- RED: the real checkpoint rebind producer and stopped-process check
selected exact current-run resume without a current controller
checkpoint.
- RED: ordinary settlement created zero Retry receipts for the real
native failure code; expected one.
- Review RED: a real current local controller file existed with no
database checkpoint rows. The initial gate returned ambiguous state. The
fix takes a required trusted execution-target kind and applies the
database requirement only to the remote persistence path.
- Initial local revision: `3662055880ccf8e09c3c50a340897e16c78627e7`,
macOS ARM64, Node 26.4.0, pnpm 9.15.4, embedded PostgreSQL.
- Initial server Vitest: 42 selected cases passed across controller
recovery integration, remote restart adoption, ordinary run completion
and retained workspace recovery. The cases cover the stopped-process
producer, current/foreign/malformed ownership, automatic user
continuation, Stop, unsettled turns, system wakes and a manual Retry on
first use.
- `pnpm --filter @paperclipai/server typecheck`: passed with this
worktree's Runner, shared and SDK builds and all seven server layers.
- `node scripts/check-module-boundaries.mjs`, whitespace checks and the
added-code-comment audit: passed.
- Review correction: `8c795e2e0e44b5209da3e228593c5cd35e948cae`. The
real local/remote controller case passed against a fresh PostgreSQL
cluster. It verifies local disk recovery without database rows, remote
current-owner resume, and remote missing/foreign/malformed holds. Fresh
`node scripts/typecheck-layers.mjs` passed all seven server layers. The
existing offline chat reader now requests metadata only and retains its
full independent native-state safety check.
- A broader local recheck hit PostgreSQL setup/cleanup hook timeouts.
These are retained as failed setup attempts; the fresh-cluster
regression above passed. Required CI supplies broad final-head coverage.
- Settlement-order correction:
`dafb721c3bbb7ae86fbc3c9c6c953ace29ca088b`. The real PostgreSQL
regression first proved both failures: Stop after an already-failed row
created one Retry instead of zero; a DB-native promotion with an old
legacy caller snapshot created zero instead of one. All six settlement
scenarios now pass. The hook requires the local failed outcome and the
canonical settled native row. Fresh seven-layer server typecheck passed
again.
- Required shared fixture base integrated at
`d22d8f9d5ee3dc36550a94d55c1d8376f3b9aab2`. The six runner-skills
fixture cases and all seven server typecheck layers passed. Required CI
passed for this head:
[workflow](https://github.com/paperclipai/paperclip/actions/runs/42309419592),
including all server layers, tests, build, Runner verification and E2E.
The final snapshot has 57 successful checks, three intended skips and
one neutral result. Current-head Greptile is 5/5; both concrete review
threads are resolved. The independent re-review ended without a final
response; its prior Stop finding has a real RED/GREEN repair.

The focused command was run from `server/`:

```sh
../node_modules/.bin/vitest run --config vitest.config.ts src/services/heartbeat/run-completion.test.ts src/services/native-runtime/native-controller-recovery.integration.test.ts src/services/native-runtime/native-session-executor.test.ts src/services/workspace-recovery.test.ts -t 'replays saved user intent|does not assign current-run resume authority|remote controller restart adoption|continues a user turn once|does not continue automatically'
```

## Risks

- A terminal failure of a fenced Runner adds one exact-run header read.
The read selects one encrypted header and limits inspection to 1 MiB.
Invalid or missing ownership evidence stays held.
- Saved user input can request one automatic Retry. It uses normal
preservation, current authorization, budget, approval, Stop and cleanup
gates. Unsettled provider turns and system wakes do not start an
automatic successor.
- This PR changes no provider process, credential, model or lease
authority checks.

## Model Used

OpenAI GPT-6 family through Codex, with reasoning, tool use and code
execution. The model is inherited; no override was used. The exact
serving model ID and context-window size are not exposed by this
runtime.

## Checklist

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

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Agent files and installed skills need durable storage before a
legacy volume can be removed.
> - Their maintenance commands currently require every writer to stop.
> - Normal file edits already use agent row locks and skill publication
locks.
> - Shared agent heads also remain durable while their agents run.
> - This pull request uses those owners to copy settled files online and
keeps unresolved recovery states intact.
> - The benefit is useful migration progress while unrelated work
continues.

## Linked Issues or Issue Description

Related: #19642.

**What happened?**

Agent-file adoption and skill installation conversion reject apply
requests without a global stopped-writer acknowledgement. Agent-file
maintenance also reports an active agent as blocked when its shared head
is already durable. Live enrollment omits the existing
unresolved-local-instruction-copy guard.

**Expected behavior**

Copy settled agent files and skill installations under their existing
writer locks. Keep old skill directories, versions, and inline bytes.
Keep unresolved local copies and unknown historical pin obligations
visible to their owners.

**Steps to reproduce**

Run agent-file maintenance with `--apply` on the original host with
durable storage enabled. A managed agent without active execution is
rejected without `--writers-stopped`. The same gate rejects skill
installation conversion. An agent that already has a shared head is also
reported as blocked while it runs.

## What Changed

- Adopt settled agent files under the existing agent row lock. Recognize
an existing shared head before checking active execution, without
rewriting its projection.
- Reuse the local working-copy preservation predicate for first adoption
and live enrollment. Keep its original public export.
- Prepare skill uploads outside the shared run-pinning lock. Verify the
exact local tree and source identity under the cache, slug, and row
publication locks. Publish catalog origin history in that same
transaction.
- Retain old skill directories, versions, and inline bytes. Keep pin
retention and volume-removal guards unchanged.
- Add PostgreSQL and filesystem coverage for active durable heads,
unresolved copies, concurrent source edits, and linked files. Update the
maintenance docs.

## Verification

- Agent-file durability and live enrollment suites: 16 tests passed with
real PostgreSQL and filesystem fixtures.
- Managed skill suite: 22 tests passed. Seven final controls also
passed: upload-time file edits, source relocation, unknown historical
pins, hard-linked files, pinning during a blocked upload, and both
rejected/successful catalog-origin publication.
- Server source typecheck and focused changed-test typecheck passed.
- Module boundaries, diff whitespace, and added-line
credential/internal-reference scans passed.
- Baseline production rejected both online paths at their stopped-writer
guards. Review regressions also reproduced a busy pinning lock during
upload and extra catalog history after rejection; their fixes passed
real PostgreSQL controls. All expected red results are preserved.
- One final-control attempt failed the existing 30-second fixture
startup hook before executing tests. A sequential retry passed all four
controls without changing guards or timeouts; the failed attempt remains
recorded.
- Exact head `001a5beed9d0e55c7c178906fbad1f9a47f87788`: [required CI
passed](https://github.com/paperclipai/paperclip/actions/runs/42309446426),
including typecheck, build, tests, Runner and end-to-end gates. Greptile
completed successfully at 5/5 with zero unresolved review threads;
GitHub reports mergeable/clean.

## Risks

Changed or unsafe sources remain blocked. Storage failures keep the
original files and recovery state. This conversion does not settle
retained provider copies or historical execution. Unknown pin
obligations still block retention and volume removal; this PR alone does
not qualify removal of the original volume.

## Model Used

OpenAI GPT-6 (Codex). The runtime did not expose a more specific backend
model ID or context-window size. Used reasoning, code editing, and tool
execution.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (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 17:31:04 -06:00
DottaandPaperclip bee0f0e2fa fix(server): recognize recoverable result finalization during upgrades (#19651)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native execution saves an accepted result before it finishes
workspace export and status projection.
> - The upgrade report currently recognizes only a live execution in the
observed phase.
> - A durable accepted result can therefore block an upgrade after its
execution lease is released.
> - Existing finalization owners already resume that saved work with
task binding, physical worker stop proof, and an atomic owner claim.
> - This pull request lets the report inspect those existing contracts
without taking authority.
> - Qualified remote work can continue through a normal server
replacement.

## Linked Issues or Issue Description

Refs #18037 and #19596.

**What happened?**

An accepted remote native result remains in result acceptance, workspace
finalization, retry, or committed projection. The upgrade report marks
it blocking because it is no longer a result-less observed execution
with a live execution lease.

**Expected behavior**

Report a saved result as recoverable only when the existing replacement
owners can resume it. Preserve all task, result, contract, remote
workspace, provider lease, physical owner, decision, and workspace
barrier checks. Keep unsupported or unknown work blocking.

**Steps to reproduce**

Run the new PostgreSQL post-result readiness fixture. It persists a real
encrypted Runner header and an accepted terminal result with a bound
remote workspace. The unchanged report returns one blocker.

**Paperclip version or commit**

Regression base: ee6c101854. Related shutdown recovery is in #19596.

## What Changed

- Delegate saved-result eligibility to the native finalization owner.
- Require the accepted terminal result and contract, current task
binding, remote authority, retained provider lease or successful
workspace receipt, and a supported physical worker identity.
- Require an applied decision and assessment for committed projection.
Keep finalizer leases beyond the supported outage budget blocking.
- Reuse the existing result recovery binding with selected metadata. Do
not load complete run or result payloads for the new observation.
- Keep real signed stop verification, owner compare-and-swap, retry
deadlines, and actual finalization with their existing owners.
- Share a two-second scheduling budget across saved-result inspections,
leaving unchecked candidates blocking after the budget expires.
- Add PostgreSQL, encrypted-header, signed stop-proof, competing-owner,
projection, and independent-work cases. Clarify the operator contract.

## Verification

- The valid accepted-result fixture reproduced the original blocking
report, then passed with this change.
- The initial eight post-result cases passed against PostgreSQL. They
include a real Ed25519 stop receipt, the existing verifier, a competing
PostgreSQL ownership claim, one copyback, and receipt reuse.
- Seventeen focused existing PostgreSQL shutdown and workspace ownership
cases passed.
- A broader five-file run passed 91 cases. Its one failure was an
invalid negative fixture: PostgreSQL correctly refused a contract change
protected by the accepted-result foreign key. The test now asserts that
refusal.
- A subsequent standalone run timed out in PostgreSQL bootstrap before
tests ran. The final run used the existing shared PostgreSQL command and
isolated per-suite template databases: all 92 tests passed across five
files (159.95 seconds). It covers both readiness suites, platform stop
proof, workspace finalization, and graceful shutdown. That scope passed
on `ce4e3995b6`. A follow-up restricts provider leases to the active
status and adds released/unknown-status controls. The new fixture now
uses the existing PostgreSQL bootstrap timeout constant. The affected
eight-case rerun passed on `850859b44d`. The explicit configured-stack
guard also passed its six platform-proof cases. Broad local source
typecheck still has stale borrowed dependency declarations; fresh CI
supplies that coverage.
- Review's locked-query regression reproduced RED: three saved-result
candidates took 6.081 seconds. With the shared scheduling budget, both
readiness suites passed all 16 cases (49.36 seconds) on final code
committed as `70b0b7b8f7`. The lock case leaves all three candidates
blocking, releases its connection, and observes all three as recoverable
after the lock is released.
- Greptile reviewed final `70b0b7b8f7` at 5/5 with no open threads.
Current-head CI typecheck, build, tests, and native review are pending.
No hosted replacement or enrollment acceptance is claimed.

## Risks

A false positive could select an unsafe upgrade. Missing evidence and
read errors remain blocking. Readiness does not stop admission, detach
an agent, acquire a lease, request a stop receipt, or authorize
takeover. The replacement still verifies physical stop and rechecks
current task ownership before copyback. Saved results retaining a
previous controller boot remain conservatively blocking, including
finalizers resumed by a later boot. This PR does not change enrollment
policy or migrate data.

## Model Used

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

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change 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 17:30:28 -06:00
DottaandPaperclip 374a918d67 fix(runner): retain qualified OpenCode artifacts for cold recovery (#19622)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native recovery must keep admitted provider state and useful
workspace files.
> - OpenCode binds its saved session to exact executable paths and
hashes.
> - A replacement image can remove those files even when session data is
saved.
> - Retaining a manifest alone cannot restore the missing launch files.
> - This pull request saves the qualified files before provider work and
restores the same owned paths.
> - The benefit is cold continuity without relaxing the Runner guard or
replaying a prior turn.

## Linked Issues or Issue Description

Refs #19558. That fix retains the selected pack for ordinary
continuation. This change adds durable file retention for a cold
allocation. Related #19487 adjusts the admission budget; it does not
retain launch files. A search of open and closed OpenCode cold/durable
artifact PRs found no duplicate.

**What happened?**

A saved OpenCode session could outlive the image that supplied its Node
executable, proxy, and native executable. A new image supplied different
bytes. The Runner then correctly refused the changed launch profile. The
saved harness did not contain the old files.

**Expected behavior**

Core must save qualified launch files before the first turn. It must
restore those exact files under the same owned account and absolute
paths. Unknown ownership, missing files, and changed hashes must still
stop recovery.

**Steps to reproduce**

Start an admitted remote OpenCode session. Save its workspace, encrypted
checkpoint, and harness data. Destroy the original allocation and
controller files. Restore into a new allocation with an image whose
qualified proxy has different bytes. Resume the same session. The new
opt-in local qualification command automates this path with actual
OpenCode and a local synthetic model.

## What Changed

- Store company-bound, encrypted launch objects and canonical run/backup
references. Generate migration 0438 through the normal database
workflow.
- Stage qualified Node, proxy, and native OpenCode files under a private
content-addressed OS-account home. Record the account UID and exact
launch paths. Preserve the Rust profile guard.
- Gate first provider work on durable capture under the current native
claim. Bound Stop/deadline waits while the actual late transfer keeps
its worker and scratch ownership until settlement. Restore cold caches
with verified staging and atomic rename. Keep live reattachment and
historical image-rooted sessions on their existing paths.
- Reuse bounded canonical expiration owners. Atomically fence old
checkpoint authority with a strict company/run-scoped retirement marker
before external deletion. Keep its indexed cleanup anchor across failed
deletes and restart. Preserve earlier resume handles and reject later
header or source-consumer writes. Add no idle scan or network call
inside a transaction.
- Add storage, race, executor, filesystem, and actual Rust resume
regressions. Add the opt-in local Linux qualification command and
operating limits in `doc/opencode-cold-recovery.md`.

## Verification

- RED before repair: real PostgreSQL/AES cold restore lacked the launch
files; normal executor preparation chose the new image pack. Separate
regressions exposed lost upload receipts, missing cache repair,
last-owner retirement, an account-UID mismatch, and a resume-handle race
during external deletion. Each expected failure was captured before its
repair.
- Local qualification before the final master integration: 1,169
executor tests, 233 storage/archive/admission/expiration tests, all
seven server typecheck layers, migration safety, and the actual Rust
cold-resume test passed.
- The maintained Linux command passed with two cached immutable
Paperclip images whose proxy hashes differ. It removed the original
account/controller trees, restored the production published cwd under a
new lease, resumed the same actual OpenCode session, kept a useful file
and prior model context, and made one new synthetic model request. No
external model call or network was available in either container.
- The object-store fixture uses real encrypted StorageService streams
with an independent owned local directory. It is not hosted S3
acceptance or a complete task HTTP/actor workflow. Historical tasks
still require normal effects reconciliation and recovery after
deployment.
- Review regressions reproduced partial-cache publication,
resume-versus-deletion, fresh-writer and source-consumer resurrection,
and unbounded Stop/deadline waits before each repair. They now pass
through real filesystem, encrypted storage, PostgreSQL, and SDK
boundaries. File validation is split from database ownership (384/831
source lines).
- Clean runtime head `aadab7acc69ff959678030db46ffe2dcfbefe28f`: the
maintained actual OpenCode cold qualification passed with `dirty:false`.
It preserved the provider session and recorded launch paths, restored
useful workspace content and prior model context, and made exactly one
resumed synthetic model request. The 314,560,000-byte launch capture
took 23.118 seconds; the later harness was 833,536 bytes. Subsequent
repairs change backup-reference retirement and governed continuity-cache
classification; capture, restore, launch, and published-workspace inputs
are unchanged, so that runtime receipt remains scoped to those paths.
The continuity-break consumer is verified separately.
- Retention repair `c14d386cecf7123257918ba6a8d47307df2da397`: 68/68
affected real PostgreSQL/AES storage tests and 15/15 focused executor
authority/admission cases pass. All seven server typecheck layers pass
(source layers and unchanged test shards, with the final 590-file test
layer repeated after fixture edits). All review threads have
evidence-backed resolutions. Its subsequent CI caught the separate
lifecycle-classification gap described below. The refreshed Paperclip
review reported paused work, not a completed new review.
- Final head `4d1cd3e3309e328b11abc85adc6d78c6d0c7f55d`: all 19 focused
root-entry/executor cases and all three source declaration layers pass.
[Greptile reviewed this exact head at
5/5](https://github.com/paperclipai/paperclip/pull/19622#issuecomment-6695984874).
Final-head [CI
verification](https://github.com/paperclipai/paperclip/actions/runs/42308908167/job/128214100339)
passes: 60 successful checks, five conditional skips, and the explicitly
neutral paused Paperclip review. No check is pending or failing.
- CI also caught the launch cache missing from the session-root
lifecycle contract. Both the writer scan and a real governed-replacement
consumer failed locally before repair: old launch bytes remained in the
active root. The cache now archives with the prior session state; the
consumer verifies exact bytes are preserved in the archive and absent
from the active root.
- The latest review found that backup retirement could delete an
independent run pin while a selected session retained the checkpoint. A
real PostgreSQL/AES regression failed before the repair. Backup-only
ownership now uses a NULL-safe exact backup predicate; the run pin
belongs to fenced checkpoint expiration. The regression verifies
checkpoint decryption and launch-file restoration after pending-backup
cleanup, followed by deletion only after removing the final resume
handle.
- An executor first-session case checks new-session durable capture
through the real capture helper and receipt propagation into the actual
harness manifest at the backup boundary. Real storage cases verify the
run-profile receipt and canonical backup/run pins.
- Sequential closure reuse and simultaneous first admission are
different scopes. A deterministic two-uploader test measures both
independent uploads, one verified ready winner, loser-only deletion, and
usable restoration for both runs. Each capture is bounded to 512 MiB and
five minutes; up to three pending receipts per run are retained. There
is no claim of global single-flight capture or a company-wide egress
bound.
- Earlier optional warm-ownership verification encountered missing local
binaries. After exact-source setup, all nine ownership assertions
passed; two other suites encountered local PostgreSQL setup/teardown
contention, so those interrupted attempts are not passes. Required
current-head CI supplies the broad final coverage. No deployment or
hosted historical recovery is claimed.

## Risks

- This adds two tables and a forward migration. The file object is
bounded to 512 MiB. Capture adds one initial transfer; later harness
archives exclude those large files. Company-scoped ready objects are
shared and retained until the last canonical owner retires. Concurrent
initial admissions can duplicate bounded transfers; each keeps
independent ownership and cancellation rather than waiting on another
controller. A continuity break retains the controller cache with its
prior state until normal session-root cleanup, adding up to one bounded
launch closure per retired state.
- Cold continuity requires the same qualified Linux platform, supported
runtime dependencies, owned account UID/home, and published absolute
cwd. A mismatch fails closed. No alias or Rust guard change is included.
- Existing image-rooted profiles have no retained file object. This
change cannot backfill missing historical binaries. A fresh governed
session must reconcile prior effects first.
- Live reattachment does not read artifact objects. Storage or
provenance failures before a new turn hold admission. They do not grant
new execution, model, tool, or replay authority.

## Model Used

OpenAI Codex, GPT-6 family, with reasoning, repository tools, code
execution, and local browser tools. The exact served model ID and
context window are not exposed in this session. No claim is made for
unavailable model 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 17:30:00 -06:00
DottaandClaude Opus 5.5 fd58378ee6 fix(runner): never dispatch an OpenCode tool call its MCP client abandoned (#19546)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The Paperclip runner runs native OpenCode agents. OpenCode calls
Paperclip tools through an MCP bridge inside the OpenCode proxy. The
proxy sends each call to runnerd as an `item/tool/call` frame on stdout.
> - runnerd reads the proxy stdout only after the controller
acknowledges earlier events. A backlog of event frames can build in the
proxy. A tool call waits behind that backlog.
> - The OpenCode MCP client can time out while the call waits. The agent
then gets `MCP error -32001: Request timed out`. But the queued frame
still reaches runnerd later, and runnerd still runs the call.
> - This is a safety bug. A mutating call (for example, create a gateway
token) can succeed after the agent was told that it failed. The agent
can then repeat it.
> - This pull request makes sure that a call whose MCP client abandoned
it is never sent to runnerd while it is still queued in the proxy.
> - The benefit: "timed out" now means "did not run" for every call that
had not left the proxy.

## Linked Issues or Issue Description

**What happened?**
In chat-feature evals with native OpenCode, the agent got `MCP error
-32001: Request timed out` for a POST tool call. The server still ran
the POST and returned 201 about one second later. Other Paperclip tools
that took 0.03–0.5 s on the server reached OpenCode 10–22 s later. The
delay grew over the run, and then the calls failed with -32001.
Sometimes they failed at once.

**Expected behavior**
If the MCP client abandons a tool call before Paperclip dispatches it,
Paperclip must not run it.

**Steps to reproduce**
1. Run a native OpenCode agent (`paperclip_runner` adapter, runnerd
path) for a long chat turn with many tool calls.
2. Make the controller acknowledge runnerd events slowly, so a backlog
builds in the proxy.
3. Let the OpenCode MCP client time out on a mutating call that waits in
the backlog.
4. See that the agent gets -32001, but the call still runs on the
server.

**Where the per-call lag comes from (finding)**
- runnerd (`provider_backend.rs`, `poll_current_provider_with_control`)
does not read proxy stdout while any event is not acknowledged. It reads
at most 128 events per poll.
- Each poll that admits events, and each acknowledgement, calls
`persist_state`. That function writes the full provider state JSON again
(up to 16 MiB, and it grows during the run) and does two fsyncs. So each
round trip gets slower during the run.
- Pipe writes from Node are asynchronous on POSIX. OpenCode event frames
and `item/tool/call` requests therefore wait in one FIFO buffer in the
proxy. A tool call waits behind all earlier event frames, and the
results come back through the same gated path.
- OpenCode starts its MCP timeout when it calls the tool, not when
runnerd reads the frame. The -32001 event frame can itself wait in the
backlog, so it can look "instant" when it arrives.
- Evidence: the delay from runner emit to server receipt stays below 0.5
s. But runnerd's `item.completed` for a call comes 10–22 s before
OpenCode's `tool.execution.completed`, and the gap grows during the run.
- The fix for the lag is not small. It needs a change to runnerd
persistence: do not gate stdout reads on a full acknowledgement, or use
an append-only event log instead of a full state rewrite with two fsyncs
for each batch. This pull request does not change runnerd.

## What Changed

- `mcp-bridge.ts`: abort the call signal when the MCP client drops its
HTTP connection before a response. The `notifications/cancelled` path
already aborted it.
- `opencode-server-driver.ts`: send the bridge abort signal to
`dynamicToolHandler` (new optional `signal` field).
- `opencode-proxy-output.ts`: add `createBackpressuredFrameWriter`. It
gives the stream only what its buffer accepts and keeps the backlog in
its own queue. Frames keep their order. If a queued frame's signal
aborts, the writer removes the frame and calls `onAbandoned`.
- `opencode-app-server-proxy.ts`: write stdout through the new writer.
Tool-call requests carry the call signal. An abandoned request fails
with "abandoned by the MCP client before it was dispatched; it did not
run". Shutdown drains all live frames.
- Tests: a new regression test uses the real bridge and the writer with
a stalled stdout. It covers a cancel after timeout, a dropped
connection, and a live call. There are also unit tests for the writer.

## Verification

- `npx vitest run src/drivers/opencode src/cli/opencode` in
`packages/paperclip-runner`: 23 files, 226 tests pass.
- Red check: I removed the disconnect abort and the queue gating. Then 2
of the 3 tests in `opencode-proxy-abandoned-tool-call.test.ts` failed:
the abandoned `item/tool/call` reached stdout. After I restored the
code, they pass.
- `pnpm verify` exits 2 because the changed proxy files are outside its
resolved test graph. The required CI jobs supply broad coverage.
- `tsc --noEmit` for the runner package shows no errors in the changed
files. One error remains in `src/eval/workflow-harness.ts`. It comes
from a workspace package that is not built and is not related to this
change.

## Risks

- A call that already left the proxy (it is in the kernel pipe buffer or
in runnerd's reader) still runs after a client timeout. The window is
about 64 KiB of pipe buffer plus Node's 16 KiB high-water mark. To close
it fully, runnerd must get a deadline or a cancellation for each call.
That is a follow-up.
- Frames now wait in a proxy queue instead of the Node stream buffer.
Delivery order does not change. The new code drains this queue on
shutdown before the last stdout flush.
- When a client disconnects, a durable call is now cancelled. Before
this change it stayed pending. The client cannot get the result in
either case.
- No schema, contract, or ACPX profile changes.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), used 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 23:29:26 +00:00
DottaandClaude Opus 5.5 411eecdccd fix(ui): store = as typed, without the highlight exporter's escapes (#19540)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - People write task requests and chat messages in the Markdown editor,
and the stored text becomes the agent's prompt
> - MDXEditor supports `==highlight==` marks, and its exporter escapes
every `=` in prose to protect that syntax
> - The editor therefore stores `a=b` as `a\=b` and `x=>y` as `x\=>y`
> - A command or code snippet in a chat message then reaches the agent
as invalid code
> - This pull request removes the `=` escape when the escape cannot
change how the text parses, as #19350 did for intraword underscores
> - The benefit is that commands, assignments and arrows reach agents
exactly as the person typed them

## Linked Issues or Issue Description

Refs #19350

**What happened?**
A chat evaluation sent a `node -e` command through the chat composer.
The stored comment contained `const fs\=require(...)` and `()\=>`. Every
`=` had a backslash. The agent received invalid JavaScript, and an
exact-text check on the stored message failed.

**Expected behavior**
Text the person types is stored as typed. A `=` that cannot start or end
a highlight needs no escape.

**Steps to reproduce**
1. Type `a=b` or `x => y` in any Markdown editor field, such as the chat
composer.
2. Read the stored value through the API.
3. Before this change it is `a\=b` or `x \=> y`.

## What Changed

- New `ui/src/lib/equals-markdown.ts`: `unescapeEqualsSigns` removes
`\=` escapes from prose only when they cannot change the parse:
- any run of escaped `=` whose length is not exactly two (a highlight
sequence is exactly two `=`);
- a run of exactly two with whitespace or the text edge on both sides
(such a run can neither open nor close a highlight).
- Other two-runs, such as `a\=\=b` or `\=\=text\=\=`, stay escaped.
Escaped backslashes (`\\=`) stay as they are.
- `ui/src/lib/underscore-markdown.ts`: the code-range walk is now the
shared `rewriteMarkdownProse` helper. Code spans, code blocks and raw
HTML stay byte-identical for both rewrites. Underscore behavior does not
change.
- `MarkdownEditor.tsx`: `toStoredMarkdown` applies the new step after
the underscore step, on the exporter's exact text.
- Tests: unit cases for commands, `===`, spaced `==`, quotes and lists,
highlight-capable runs, code spans and blocks, raw HTML and escaped
backslashes. One editor test for a pasted command with a code block.

## Verification

- `npx vitest run src/components/MarkdownEditor.test.tsx src/lib
src/components/task-chat` in `ui/`: 280 files, 3388 tests passed.
- Negative control: with the new step removed from `toStoredMarkdown`,
the new editor test fails.
- Parser check: I parsed the unescaped forms with
`micromark-extension-highlight-mark` 1.2.0, the version MDXEditor uses.
`a=b and c=d`, `x === y`, `a == b == c`, `()=>x` and `x!==y` produce
zero highlight nodes. The controls `==a==`, `a ==b== c` and `say
==this== now` produce one highlight node each.
- `tsc -p ui/tsconfig.json`: no errors in the changed files. The
remaining errors are in files this change does not touch.

## Risks

- Low risk. The step only removes an escape when the highlight tokenizer
cannot use that `=`. Text the parser cannot place is returned unchanged.
- Some `==` in code-like prose (for example `x!==y`) stays escaped. This
is intentional: such a run can open a highlight.

> 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 17:27:59 -06:00
DottaandClaude Opus 5.5 a3f80ea043 fix(onboarding): close the onboarding task when its interview moves to chat (#19643)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - New users start in an onboarding task. The first agent can run the
onboarding interview there or in its chat.
> - #19512 moves the interview card to the chat when the agent asks it
there. The task's card expires.
> - The onboarding task then stays `in_review` with no card, run, wake
or reviewer. Review attention calls it "stalled".
> - Needs you then shows "Choose how this task continues" with Send back
to work / Request changes / Mark task done. A new user cannot know what
to pick. The card stays after the interview, plan approval and handoff
finish in chat.
> - This pull request closes the onboarding task when its interview
moves to chat, and links the chat in a system comment.
> - The benefit is that the user never decides what happens to an
internal task.

## Linked Issues or Issue Description

Refs #19512

**What happened?**
In a browser walk (Codex API path), the user chose "Interview me" in the
onboarding task. Then the user asked in Chat: "Can we do my onboarding
interview here in chat instead of in the task?". The chat card worked.
The onboarding task stayed `in_review`, and Needs you showed "Choose how
this task continues" for it until the end of onboarding.

**What did you expect?**
The onboarding task follows the conversation. It closes with a link to
the chat. No decision about an internal task goes to the user.

**Steps to reproduce**
1. Start onboarding. Choose "Interview me" on the onboarding task.
2. Open Chat with the agent. Ask to do the interview in chat.
3. Open Inbox. See "Choose how this task continues" for the onboarding
task.

## What Changed

- `server/src/services/issue-thread-interactions.ts`: after the chat
question supersedes the onboarding task's interview cards, the
onboarding task is closed (`in_review` → `done`) when no other card is
pending on it. A system comment links to the chat. The change writes an
`issue.updated` activity entry (`source: "onboarding_moved_to_chat"`).
Activity publications and issue post-commit actions run after commit.
- The task stays open if it is not `in_review` or if another card is
still pending (for example a card for a teammate).
- `first-task` skill: tells the agent that the task closes when
onboarding moves to chat. The chat then replaces the task for the rest
of the workflow (plan, proposal cards, child tasks, results). The agent
skips the steps that keep the task `in_review` or make it wait for a
child.
- `onboarding-assets/first-task/README.md`: documents the behavior.
- Tests in `server/src/__tests__/coordinator-handoff.test.ts`: the new
red/green case, plus a case that keeps the task open when another card
is pending.

## Verification

Red on master (fix reverted, same test):

```
 FAIL  src/__tests__/coordinator-handoff.test.ts > Coordinator canonical handoffs > resolves the onboarding task when its interview moves to the agent chat
AssertionError: expected [ 'stalled' ] to deeply equal []
- []
+ [
+   "stalled",
+ ]
 ❯ src/__tests__/coordinator-handoff.test.ts:257:40
    256|     // No "Choose how this task continues" stalled-review card for the…
    257|     expect(await stalledReviewCards()).toEqual([]);
      Tests  1 failed | 3 passed | 24 skipped (28)
```

The test reads the real Needs you feed (`attentionService.list`). On
master it holds a `review` item with `reviewAttentionState: "stalled"`
for the onboarding task. That is the "Choose how this task continues"
card.

Green with the fix:

```
cd server && npx vitest run src/__tests__/coordinator-handoff.test.ts
      Tests  28 passed (28)
```

Neighbour suites:

```
npx vitest run src/__tests__/onboarding-first-task-admission.test.ts src/__tests__/issue-onboarding-first-task-routes.test.ts \
  src/__tests__/issue-thread-interaction-routes.test.ts src/__tests__/issue-thread-interactions-service.test.ts \
  src/__tests__/issue-thread-interactions-telemetry.test.ts src/__tests__/coordinator-handoff.test.ts
 Test Files  6 passed (6)
      Tests  373 passed (373)
```

- `server` typecheck (`tsc --noEmit`): pass.
- `pnpm verify` exits 2 (the README is a broad input). Required CI
supplies broad coverage.
- Not run: a live browser walk. The Vitest case drives the same service
path the chat uses.

## Risks

- Low risk. The change runs only when an agent asks a question in its
own chat and that question supersedes a pending onboarding-task question
(the #19512 path).
- The onboarding task becomes `done` without a recorded first-task
decision. Later onboarding happens in chat, so nothing waits on this
task. A done onboarding parent still accepts child-completion report
wakes.
- The onboarding task rows are locked before their question rows, the
same order question creation on the task uses (Greptile review
follow-up). The chat row is locked first; no path locks an onboarding
task and then a chat row.

## Model Used

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

## Checklist

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

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Task status is the main signal on every task surface, including
agent messages and the task chat.
> - #19329 put the status-to-glyph mapping in one resolver. Two
conversation surfaces did not use all of its inputs.
> - Inline task links in a message passed only the bare status. A
blocked task that waits on active sub-tasks showed a red glyph there and
a blue glyph in the list, breadcrumb and sidebar.
> - The "Waiting on live work" strip above the composer drew its own
filled and hollow dots, not task glyphs.
> - This pull request makes both surfaces resolve through the shared
mapping.
> - The benefit is that one task state looks the same in every view.

## Linked Issues or Issue Description

Refs #19329

**What happened?**

In a hands-on onboarding walk, the onboarding task was blocked on four
active sub-tasks. The breadcrumb, the sidebar Recents row, the Tasks
list and the "Blocked" chip used the blue in-queue glyph. Inline task
links in the agent message used the same glyph in red. The "Still
waiting on live work" strip showed the same tasks as plain dots.

**Expected behavior**

The same task state uses the same glyph and colour token in every view,
including inline links and the live-work strip.

**Steps to reproduce**

1. Create a task with sub-tasks, and block the parent on them while a
sub-task runs.
2. Mention the parent in a message (for example "(PAP-1)").
3. Compare the inline link glyph with the task list glyph. The link is
red, the list is blue.
4. Open the parent task. Compare the "Still waiting on live work" strip
with the task list. The strip shows dots.

**Paperclip version or commit**

`master` at e482f0f632.

## What Changed

- `MarkdownBody` issue mentions spread `taskStatusIconProps(issue)` into
`StatusIcon`. They now pass `blockerAttention`, `displayStatus` and
`externalConversationState`.
- `TaskChatLiveWorkLinks` draws `StatusIcon` for each task, in place of
the private dot glyphs. Relation summaries do not carry blocker
attention or the waiting-conversation state, so the strip reads them
from the same `/issues/summaries` batch that the sidebar and
created-task cards use. Until that loads, it uses the relation status.
- `TaskChatThread` passes `companyId` to the strip.
- The strip fetches summaries in batches of `ISSUE_SUMMARIES_MAX_IDS`
(5), and production-mode "Now running" rows draw the glyph too (Greptile
review).
- New tests: `TaskChatBlockerLinks.test.tsx` (strip with seven tasks
through the real fetch path, both presentation modes, "Now running" row)
and a BUG-07 case in `MarkdownBody.test.tsx`. One assertion in
`TaskChatThread.test.tsx` now expects the glyph label text.
- Storybook: "Same state in conversations" section in Foundations /
Status Language shows the list row, inline mentions and the strip side
by side.

## Verification

Red on master (fix reverted, new tests kept):

```
 ❯ src/components/task-chat/TaskChatBlockerLinks.test.tsx (2 tests | 2 failed)
     × draws each waiting task with the task list's glyph and colour token (streamlined)
     × draws each waiting task with the task list's glyph and colour token (production)
 ❯ src/components/MarkdownBody.test.tsx (66 tests | 1 failed | 65 skipped)
     × draws a blocked task mention with the same glyph and colour token as the task list (BUG-07)
AssertionError: expected '<div class="paperclip-markdown …' to contain 'var(--status-task-icon-in_queue)'
  … style="color:var(--status-task-icon-blocked)" role="img" aria-label="Blocked" …
AssertionError: BRA-2: expected null to be '--status-task-icon-in_progress'
 Test Files  2 failed (2)
      Tests  3 failed | 65 skipped (68)
```

Green with the fix:

```
cd ui && npx vitest run src/components/task-chat/TaskChatBlockerLinks.test.tsx src/components/MarkdownBody.test.tsx -t "BUG-07"
 Test Files  2 passed (2)
      Tests  3 passed | 65 skipped (68)
```

Neighbouring suites (`src/components/task-chat`,
`MarkdownBody.test.tsx`, `TaskChatThread.test.tsx`, `StatusIcon*`): 64
files, 1364 passed, 1 skipped. `ui` tsc reports no errors in touched
files. `pnpm check:token-gates`: all gates clean. `node
scripts/storybook-catalog.mjs`: OK.

Storybook (Foundations / Status Language / Full matrix, "Same state in
conversations"):

| Before (master) | After |
| --- | --- |
|
![before](https://raw.githubusercontent.com/paperclipai/paperclip/93b50f09d37d4023903e1da67c340f162dbcf7d0/shots/before.png)
|
![after](https://raw.githubusercontent.com/paperclipai/paperclip/93b50f09d37d4023903e1da67c340f162dbcf7d0/shots/after.png)
|

Before: inline mentions are red, the strip shows dots. After: inline
mentions and the strip use the same blue in-queue, in-progress and todo
glyphs as the task list row.

## Risks

- Low risk. The change is presentation only.
- The strip issues one `/issues/summaries` request for its listed task
IDs (at most a few). The request is shared between the top and bottom
strip by query key. If it fails, the strip uses the relation status.
- Step links now include the glyph's accessible title in their text
content.

## Model Used

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

## Checklist

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

> - Paperclip manages work performed by AI agents.
> - Server replacement must preserve retained Runner sessions.
> - Older sessions can retain controller checkpoints on the server
volume.
> - The existing importer requires all execution writers to stop,
including unrelated runs.
> - This change adds a separate online path for successful, settled
sources.
> - It locks the source task and finalizer while it reads and publishes
that source.
> - Unrelated work continues. The offline importer keeps its existing
authority.

## Linked Issues or Issue Description

**What happened?**

A retained controller checkpoint cannot be converted by the existing
maintenance importer while another task runs. Requiring a busy server to
become globally idle prevents online migration.

**Expected behavior**

An operator can convert a successful retained source after its finalizer
releases ownership, while unrelated tasks continue. An expired owner
remains an owner. Related active work and uncertain evidence remain
blocked.

**Steps to reproduce**

1. Retain a valid legacy controller for a successful run with an
accepted result and a released committed finalizer.
2. Start a different task.
3. Run controller preparation. The old importer refuses because
execution writers are active.

Related preservation contracts: #19038 and #19024. This change does not
replace workspace migration or authorize volume removal.

## What Changed

- Add `prestageNativeControllers` with task, finalizer, and source-run
row locks. Recheck related runs and exact checkpoint identity under
those locks.
- Allow legacy checkpoint material through normal settled-owner import
checks. Keep offline authority separate.
- Compare captured local controller and supervisor bytes before commit.
A changed source rolls back publication.
- Add `--controllers-only` to the existing finite native preparation
entry. This avoids re-reading provider archives during controller
conversion.
- Add PostgreSQL cases for unrelated work, expired owners, related
active work, source-lock contention, and source changes.
- Document the finite command and its limits.

## Verification

- Real PostgreSQL affected suites: 56 passed across controller migration
and recovery. A first per-server run was interrupted because it was
slow; the completed run used the repository's shared PostgreSQL test
wrapper.
- Initial online cases: 3 passed, covering unrelated work, an expired
owner, and related active work.
- Source lock and changed-file cases: 2 passed. Publication rolled back
on a source change.
- Final source-evidence regression: 2 passed. A first unproven source
deferred and a later valid source imported; changed-file rollback
remained intact.
- Server TypeScript check passed with the SDK and Runner declarations
built.
- Server package build passed, including the Runner vendor checks and
compiled command.
- Runtime bundling suite: 10 passed.
- Full repository CI and exact-head automated review remain required
before merge. This draft is not staging acceptance.


- PASS: 7 CLI cases after adding the controller-only dependency fixture
and explicit-mode coverage. The first CI run exposed missing mock/export
coverage; the fixture repair passed after rebase.
- The unrelated model-default failures were fixed on master; this branch
now includes that fix.

## Risks

- A source task can wait briefly on its own migration lock. A one-second
lock acquisition budget defers a contended source. No global admission
hold is acquired.
- Only successful sources with a released committed finalizer are
eligible. Failed or unsettled sources stay deferred and require their
preservation owners.
- The operator must run the command on the host that owns the original
files. The command does not certify enrollment readiness or detach any
volume.
- The existing offline path remains unchanged and still requires stopped
writers.

## Model Used

OpenAI Codex, GPT-6 family, with reasoning, tool use, and code
execution. The exact serving model ID and context window are not exposed
in this session.

## Checklist

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

> - Paperclip is the open source app people use to manage AI agents for
work
> - A user can send a message while an agent works. The message waits in
a queue strip above the composer. The strip has an **Interrupt** button
that stops the current turn so the message starts now.
> - In a browser walk of onboarding, Interrupt showed a red "Couldn't
finish this attempt. Your message is saved." banner with Retry. The
thread also showed two rows for the same stopped run ("Stopped for 22s"
and "Stopped after 21s"). After about 20 s it recovered by itself.
> - A stop that the user asked for is not a failure. The red banner and
Retry make a normal action look broken.
> - #19369 stopped resource waits from showing as failed attempts. This
is the same banner, reached through the Interrupt path.
> - This pull request shows the Interrupt hand-off as working, and
renders the stopped turn once.
> - The benefit is that steering a running agent looks calm and correct:
one stopped row, no error, and the queued message starts next.

## Linked Issues or Issue Description

Refs #19369 (same banner class, resource-wait path)

**What happened?**
While an agent ran, the user queued a message and clicked Interrupt on
the queued strip. A red banner "Couldn't finish this attempt. Your
message is saved." with Retry appeared at the top of the task. Two rows
showed the same stopped run: "Stopped for 22s" and "Stopped after 21s".
About 20 s later the banner went away and the message was delivered.

**Expected behavior**
A user Interrupt is not a failure. No red banner or Retry. One "Stopped"
row, and the queued message starts next.

**Steps to reproduce**
1. Start a task with a Paperclip Runner agent.
2. While it works, send a message so it waits in the queue strip.
3. Wait more than 10 s, then click Interrupt on the queued strip.
4. See the red banner, Retry, and two "Stopped" rows.

**Paperclip version or commit**
master e482f0f632

**Agent adapter(s) involved**
Not adapter-specific (core bug); seen with Claude API through Paperclip
Runner.

## What Changed

Cause 1 — banner
(`server/src/services/task-interruption-presentation.ts`):
- The Interrupt route cancels the run with
`queuedCommentInterruptQueueId` set to the queued wake, and stamps
`queuedCommentInterrupt.requestedAt` on that wake. The projection skips
its "intentional Stop" rule whenever a pending wake exists, and treats a
pending wake as "working" only for 10 s after the message was *queued*.
A message queued earlier than 10 s before Interrupt, or a cleanup that
takes longer than 10 s, fell through to `state: "failed", canRetry:
true`.
- The projection now reads `queuedCommentInterruptQueueId` from the run
and `queuedCommentInterrupt.requestedAt` from the wake (both bounded
strings in the existing batched queries). When the cancelled run was
stopped for exactly the pending wake, it returns `working` for up to 5
min after the click. Pause, approval, reconnect and other user actions
stay first. A pending Retry preparation and exhausted system recovery
keep their own presentation. After 5 min, a hand-off that never started
shows one Retry, as before.

Cause 2 — two rows (`ui/src/components/TaskChatThread.tsx`):
- When a native runner turn ends, the live tail stays until the settled
turn *and* its reply comment render, or a 15 s backstop expires. A
stopped turn posts no reply, so the tail stayed for 15 s next to the
settled turn. That gave "Stopped after 21s" (live tail) and "Stopped for
22s" (settled turn).
- A cancelled settling run now hands off as soon as its settled turn
renders.

Tests:
- `server/src/__tests__/task-interruption-query.test.ts`: new
embedded-Postgres test drives the real batched query with the exact
`resultJson` the Interrupt route writes. It checks `working`/no Retry/no
Sentry failure capture, a run stopped for a different queue still fails,
and the 5 min bound.
- `ui/src/components/TaskChatThread.test.tsx`: new test renders a live
native runner turn, then the cancelled run with no reply. It checks
there is no live tail and exactly one "Stopped" row.

## Verification

Red on master (tests added before the fix):

```
$ npx vitest run src/__tests__/task-interruption-query.test.ts -t "queued-message Interrupt"
 FAIL  task interruption latest run query > shows a queued-message Interrupt as handing off, not as a failed attempt
AssertionError: expected { …(8) } to match object { state: 'working', …(2) }
-   "canRetry": false,
+   "canRetry": true,
-   "state": "working",
+   "state": "failed",
 Tests  1 failed | 18 skipped (19)
```

```
$ npx vitest run src/components/TaskChatThread.test.tsx -t "interrupted native turn"
 ❯ src/components/TaskChatThread.test.tsx:5528:82
    expect(container.querySelector('[data-testid="task-chat-live-transcript"]')).toBeNull();
 Tests  1 failed | 368 skipped (369)
```

Green after the fix:

```
$ npx vitest run src/__tests__/task-interruption-query.test.ts src/services/task-interruption-presentation.test.ts
 Test Files  2 passed (2)
      Tests  60 passed (60)

$ npx vitest run src/components/TaskChatThread.test.tsx -t "interrupted native turn"
 Tests  1 passed | 368 skipped (369)
```

Local `pnpm verify` (affected Vitest selection) result is added in a
follow-up comment; required CI supplies typecheck, build and the full
suite.

## Risks

- Low risk. Both changes are presentation only. Admission, cleanup and
Stop gates do not change.
- A hand-off that is blocked for a long time now shows no banner for up
to 5 min. The queued strip still shows its own wait text ("Waiting for
the previous run to finish recovery…") during that time, so the wait is
visible. After 5 min, Retry returns.
- The thread change applies only to a settling run with status
`cancelled`. Failed and succeeded runs keep the existing reply-comment
hand-off.
- Not in scope: the task status icon changed to `todo` during the
hand-off. That is the issue status, not this projection.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 17:18:04 -06:00
DottaandClaude Opus 5.5 15ee6a25a6 fix(native): re-bootstrap a claimed Daytona run killed before runner launch (#19616)
## 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. Restart recovery decides what to do with each live run after
the control plane restarts.
> - #19583 lets recovery start the same-run bootstrap again when the
Postgres controller checkpoint proves that the runner received only
bootstrap commands.
> - A live replay found an earlier window. The control plane died after
the executor claimed the run and staged the sandbox workspace, but
before it asked for a runner process. No runner checkpoint existed, so
#19583 did not apply.
> - Recovery classified that run as `ambiguous_remote_recovery_evidence`
and blocked the task. The task also showed "Workspace repair required",
which did not describe the real cause.
> - This pull request proves the "nothing reached a runner" case from
durable evidence and starts the bootstrap again. It also names the
restart in the settled hold note.
> - The benefit is that a restart at any point of Daytona startup before
runner launch continues automatically.

## Linked Issues or Issue Description

**What happened?**

A chaos replay killed the control plane about 2 s after a claimed ACPX
Claude run started on Daytona. The kill came after `stage.workspace` and
environment acquisition, before `native.process_start_requested`. The
restarted server took over because the previous controller process was
dead. It then returned `ambiguous_remote_recovery_evidence` with
disposition `blocked`. The task rested blocked as "Workspace repair
required".

Evidence in the saved database at that point: coordinator `attempt: 1`,
`controllerGeneration: 1`, previous controller identity recorded and
dead. Native workspace sync was `prepared`. There were no runner
checkpoint rows, no session checkpoint, no process PID or start time, no
provider session, and no execution-start or runner-sourced event. The
local session root was an empty directory.

**Expected behavior**

When durable evidence proves that no runner process was requested and no
controller checkpoint exists, recovery continues the run with a fresh
bootstrap. Any evidence that a runner may have started keeps the hold.

**Steps to reproduce**

1. Start a native run on a Daytona environment.
2. Kill the control plane after workspace staging and before the
executor requests the runner process.
3. Restart the control plane.
4. See that restart recovery blocks the task with
`ambiguous_remote_recovery_evidence`.

Root cause:
`server/src/services/native-runtime/native-restart-recovery.ts`,
function `classifyNativeRunnerRecoveryEvidence`, remote branch. The only
no-checkpoint remote rule required an unclaimed coordinator (`attempt:
0`, no controller identity). A claimed run with no launch evidence fell
through to "ambiguous".

Refs #19583, #16349

## What Changed

- `native-restart-recovery.ts`: add the `remote_runner_never_launched`
proof, which leads to `bootstrap_incomplete`. It applies only to a
classic sandbox run with a prepared workspace and the preparation
fencing version. The run must have a real executor claim (controller
identity and lease owner) whose controller is gone (takeover allowed).
It needs no durable checkpoint, no session checkpoint, no process
fields, no provider session, and no provider evidence. The run must also
have no `native.process_start_requested`, process identity,
`adapter.invoke`, `tool.execution.started`, or runner-sourced event. The
same three-attempt budget applies. The shared event query moved into
`hasNativeExecutionStartEvidence`.
- `native-session-executor.ts`:
`restoreRemoteBootstrapCheckpointForRecovery` removes an empty session
root that preparation created. Without that step, the fresh bootstrap
read it as `durable_identity_unreadable`.
- `execution-recovery-resolution.ts`: the new
`executionRecoverySettlementNote` names a settled
`native_restart_recovery_blocked` hold as a stop after a server restart.
It no longer shows "Workspace repair required" because a later workspace
export was retried. Other causes keep their notes.
- `doc/run-log-events.md`: document the new reason.

## Verification

- Red, then green: `cd server && npx vitest run
src/services/native-runtime/native-runner-restart-recovery.integration.test.ts
-t "unlaunched|never requested|Postgres checkpoint"`. Before the fix,
the proven case (lease live with a dead owner, and lease expired) and
the budget case returned `blocked` /
`ambiguous_remote_recovery_evidence`. The empty-root executor case
failed.
- Fail-closed cases: these keep the hold:
`native.process_start_requested`, a process identity event,
`adapter.invoke`, a runner-sourced event, a recorded PID, a recorded
provider session, and a spent budget. A live claiming controller returns
`awaiting_evidence` / `controller_still_alive`. The existing
"contradictory evidence" cases (`claimed` with no controller identity,
`launch`, `process`, and others) stay blocked.
- Wording: `server/src/services/execution-recovery-resolution.test.ts`.
- Regression set (10 files, including `workspace-recovery`,
`native-safe-replacement`, `heartbeat/recovery`,
`native-session-executor`, and `native-restart-recovery`): 2035 passed,
14 skipped.
- Live Daytona replay of the failing chaos task, 3 runs, sequential,
ACPX Claude, double SIGKILL, current master plus this change. Image:
content-matched `paperclip-daytona-runner@sha256:5a29a797…` built from
c6eecac1e0, with runner inputs pinned to it, because the image-only
build sat in the Actions queue. Result: **3/3 PASS** (R1 pass; R2, R4,
R5, R6 and T pass). Each run's faults landed after the runner launched.
Recovery waited on the live remote controller lease, then continued
without a Retry. Run 3 continued through `reattach_remote_runner`. The
saved logs for runs 1 and 2 do not name their claim kind. The earlier
pre-launch window (the subject of this PR) is not hit reliably by replay
timing. The integration tests cover that window with the same evidence
shape as the saved failing database.

## Risks

- The proof depends on the executor writing
`native.process_start_requested` before a backend may spawn a runner
(`onSessionAdmission` in `executeNativeSession`). A future change that
spawns a runner earlier would weaken this proof. The fail-closed tests
pin the event as the gate.
- A fresh bootstrap acquires a new sandbox. The staged sandbox from the
dead attempt keeps its lease until run settlement releases all run
leases. It never ran a runner.
- The note change affects only settled holds with the cause
`native_restart_recovery_blocked`.

## 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>
2026-11-06 17:17:14 -06:00
DottaandPaperclip c9391720a1 fix(heartbeat): send saved follow-ups after task completion (#19600)
## Thinking Path

> - Paperclip manages agents and their work.
> - Task messages remain in a durable queue while an earlier run saves
its result.
> - That result can mark the task done before the user sends the saved
messages.
> - Send records current user intent, but its wake guard allowed only
active task states.
> - The old queue therefore remained pending with an obsolete
finalization message.
> - This change admits that exact Send receipt and reopens the task
through the issue service in the successor transaction.
> - The agent can receive the saved input once while the completed
result remains unchanged.

## Linked Issues or Issue Description

**What happened?**

Send on a saved message can leave the message queued after its native
predecessor succeeds and commits. The old wait message remains visible.
Allowing admission alone also fails: continuation construction rejects a
task that remains done.

**Expected behavior**

Explicit Send must deliver the saved input once through normal
admission. It must retain the previous run and its result.

**Steps to reproduce**

1. Save a follow-up while a native run finishes its result.
2. Let that run succeed, commit its accepted result, and mark the task
done.
3. Send the saved follow-up. Before this change, the receipt stays
deferred.

Related public work: Refs #19542. That change selects recovered
workspace files. This change handles the later saved-message admission.
Refs #19602 for the independent workflow fixture repair required by CI.

## What Changed

- Allow the exact explicit Send wake guard to accept a done task. Keep
automatic recovery and cancelled task guards unchanged.
- Reopen the task through the company-scoped issue service after
admission gates pass. Commit reopening, queue consumption and the
successor together. Publish the audit after commit.
- Add a native succeeded/committed predecessor regression with unchanged
saved work and one successor after two resume calls.
- Verify real heartbeat startup under the clicking operator. Keep
automatic input, cancelled tasks and paused agents held.
- Check workspace preflight for the prospective reopened task. Use the
normal company-scoped transition if preflight blocks a done task, so its
completion metadata is cleared.
- Check a successful native predecessor for its committed accepted
result, current ownership and pending cleanup. Keep approvals and
questions effective even without a recovery action.
- Document the saved Send contract and update the queue test dependency.

## Verification

- Final merge qualification for
`675914f0fa9b5eb1d2c9c8fcd448e0669d0c4836`: all 56 applicable status
checks pass, with no pending or failed checks. [Final
CI](https://github.com/paperclipai/paperclip/actions/runs/42307623559)
includes typecheck, build, all applicable test and E2E shards, and
aggregate verification. Current-head Greptile is 5/5; all five review
threads are resolved. Any paused or queued application review is not
claimed as a new completed review.

- Final base qualification at
`675914f0fa9b5eb1d2c9c8fcd448e0669d0c4836`: official preflight passes
(1.5s); all six integrated model-normalization cases pass (19.07s); both
affected test typecheck layers pass. The integration changes no
production server/shared/Runner/adapter inputs, so the preceding 12-case
Send proof, two ordinary status guards, and unchanged production
typecheck layers remain applicable. Fresh final-head CI supplies broad
coverage.

- New review regression: both failed and cancelled native predecessors
on a task manually marked Done fail before the repair with
`continuation_task_ownership_changed` through real
`resumeQueuedCommentInterrupt` admission. At
`bfb0eff7941090ac3ac43ce4366ba2e3d46612e9`, all 12 affected
Send/source/cleanup/workspace/decision cases pass (246.80s), ordinary
Done/cancelled rejection cases pass (2; 100.31s), and all seven server
typecheck layers pass. History validation uses the prospective reopened
status only for an already-verified explicit native Send. No status is
changed during a dry run.
- Prior `e64e4bb1` CI passed the affected serialized server suites,
including the complete saved-message route suite, typecheck and build.
General server shard 4 still failed two unrelated stale default-model
expectations (`gpt-5.6-sol` versus the new `gpt-6.1-sol` default). The
equivalent repair has already merged in #19610 as
`8621bcf53ee2e4bbefb7d193add42dd544ca06e8`; the separately prepared
#19645 is closed as superseded. This source fix does not weaken or edit
those assertions.

- Original RED: the committed-source and completed-task startup cases
left the saved receipt deferred.
- Review RED: Done plus an unrunnable isolated worktree created one
successor instead of zero. A successful committed predecessor with
pending lease cleanup also created one instead of zero. The new
Done-to-blocked path retained stale completion time. Pending approvals
and questions each allowed a successor. All failures were captured
before their respective repairs.
- Integrated production revision
`99018264dd1ef002ff3500378184bcd167d9206b`: 417 affected cases pass
across the complete native continuation, responsible-user startup, and
workspace preflight suites (386 + 18 + 13). These include the original
successful startup case and six cleanup/finalization faults that hold
the exact saved receipt until settlement, then deliver it once.
- `pnpm --filter @paperclipai/server typecheck` passes all seven layers
on that integrated head, including the local Runner/SDK dependency
build. Module-boundary, whitespace, and added-code-comment checks pass.
- Normal master integrations preserve branch history. No force push or
assertion weakening was used. Fresh CI and automated review are running
on `675914f0fa9b5eb1d2c9c8fcd448e0669d0c4836`, which normally integrates
the already-merged #19610 fixture repair after the reproduced inherited
CI failure.
- The earlier CI attempt failed in five unchanged workflow fixture cases
because IMAGE_ONLY was unset. The merged #19602 repairs that fixture. A
later inherited duplicate default in that independent fixture has a
separate repair; this PR does not edit it.
- Follow-up route-fixture RED: three existing native queue delivery
cases failed because their fixture marked a run successful without an
accepted result or committed finalization. The same three cases pass
after the fixture records the complete native success contract (42.08s);
delivery assertions and production gates are unchanged. The affected
test-tree TypeScript layer also passes. The full route run passed all
119 assertions but failed its 30-second database teardown hook, so it is
not recorded as a suite pass. That result is retained. One unchanged
bounded rerun was stopped after more than 15 minutes without a final
verdict; it is inconclusive, not a pass.
- Final integrated head `e64e4bb1`: all 13 focused route and
completed-task admission cases pass across two real-PostgreSQL suites
(188.95s), including the three repaired route fixtures and every new
cleanup/finalization/decision hold. Fresh preflight passes; all seven
server typecheck layers pass. Required final CI supplies the broader
route and server coverage.
- These tests use real PostgreSQL and normal admission, cleanup, queue
consumption, and startup services. The startup suite controls the
provider boundary. Hosted provider acceptance remains pending normal
deployment.

## Risks

- Explicit Send can reopen a done task. This is limited to its validated
saved receipt and current assignee. A cancelled task remains held.
- Reopening uses the existing company-scoped issue transition and shares
the successor transaction. A failed admission does not reopen the task.
Provider Stop, workspace, approval, pause and budget checks retain their
existing owners.
- Cleanup and pending decisions retain the done task and saved receipt.
An unrunnable workspace uses the existing blocked-task diagnostic. No
schema or runtime package changes.

## Model Used

- OpenAI Codex, GPT-6 family; exact serving snapshot/context window not
exposed; repository analysis, code editing, tool execution.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (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 17:15:07 -06:00
DottaandPaperclip 795c3c4c1d fix(recovery): continue saved input after historical retry pause (#19576)
## Thinking Path

> - Paperclip manages work performed by AI agents.
> - Its scheduler parks work when another run holds a shared workspace.
> - A pause can refuse the scheduled retry before provider dispatch.
> - Older schedulers recorded that refusal in the run log without a JSON
marker.
> - After the pause was released, a saved user message could remain held
for execution recovery.
> - This change verifies the original refusal and parent workspace-wait
proof before admitting one continuation.
> - Existing execution, Stop, budget, approval, and workspace guards
remain in force.

## Linked Issues or Issue Description

Refs #19466, #19552, #19580, #19602.

**What happened?**

A saved user message remained deferred after a pause was released. Its
source was a never-claimed workspace retry. The historical source had an
`issue_paused` error and no JSON cancellation marker. The continuation
proof accepted only a different system-cancellation shape.

**Expected behavior**

The normal saved-message worker admits the existing input once when
trusted scheduler evidence proves no provider dispatch occurred and
current admission gates permit work.

**Steps to reproduce**

1. Record a host-certified pre-provider workspace wait and its scheduled
retry.
2. Let the real scheduler refuse promotion under an active task pause,
including after the user presses Retry Now.
3. Keep the historical run-log-only cancellation shape, then release the
pause.
4. Run concurrent normal saved-message recovery scans.

**Paperclip version or commit**

The regression fails on master `f0006c7783`. It leaves zero queued
successors. The branch now normally integrates the fixture repairs in
#19602 and #19628.

## What Changed

- Accept the historical cancellation shape only when a trusted system
lifecycle warning records the exact scheduler pause refusal. A prior
Retry Now event does not invalidate that proof.
- Bind the event to a released same-company pause and its task
membership.
- Reuse the existing parent workspace-wait certificate,
no-provider-output checks, and retained-execution rejection.
- Preserve historical cancellation records and consume the existing
saved input through normal admission.
- Document the contract and add real PostgreSQL coverage using actual
scheduler suppression and pause release.

## Verification

- RED: the real scheduler suppressed the retry under a pause. After
release, two normal recovery scans left zero successors.
- GREEN: the same path creates one successor and coalesces the original
saved request.
- All 389 tests in the complete native continuation suite pass before
the final base update. The only changed continuation source from master
is the separately merged one-line repair in #19552.
- Fourteen focused cases passed at
`2aaeb50f78ba0a22809ab96a00076ce50fcea559`, including a parent pause
covering a child task. Thirteen cases also passed before the base
update. They cover the normal path and missing event, wrong task,
missing membership, active or mistimed pause, provider activity,
retained lease, operator Stop, and invalid ancestry.
- The earlier seven-layer server typecheck passed after building this
worktree's Runner and SDK dependencies. The current review fix passes
the exact CI test-layer typecheck. Fresh required CI will supply
current-head source coverage.
- The first sandboxed database run could not bind loopback. A later
fresh dependency install omitted native PostgreSQL library symlinks and
failed before executing tests. The package's own postinstall hydration
repaired that setup. Previous-head verification passed all 14 focused
cases at `2aaeb50f78ba0a22809ab96a00076ce50fcea559`.
- Module-boundary and whitespace checks pass.
- Commands: `cd server && pnpm exec vitest run
src/services/explicit-native-continuation.test.ts`; `pnpm --filter
@paperclipai/server typecheck`.
- Environment: macOS arm64, Node 26.4.0, pnpm 9.15.4, real local
PostgreSQL.
- Previous head `2aaeb50f78ba0a22809ab96a00076ce50fcea559` had Greptile
5/5 and 56 passing checks, with three skipped. A later advisory review
found the Retry Now event-order case. Those checks do not qualify the
new head. Hosted recovery remains unverified.
- Review RED: the real `retryScheduledRetryNow` producer wrote
lifecycle/info at sequence 1 and the exact pause warning at sequence 2.
After real hold release and two concurrent normal queued-message scans,
both task-rooted and parent-rooted cases produced zero successors
instead of one. Both failed before the production change (17.24s).
- Review GREEN: all 17 focused cases pass at
`f38e9934b8ce3f7ddfb23eaa0f979c3f44e7a990` (4.74s). The previous 14
cases remain covered. Two new cases use the real Retry Now producer, and
one new case rejects contradictory provider execution evidence. No
pause, membership, timing, ancestry, output, retained-execution, or Stop
check was removed.
- Current focused command: `pnpm exec vitest run
server/src/services/explicit-native-continuation.test.ts -t 'historical
pause|Retry Now pause history' --fsModuleCache`. Current test-layer
typecheck: `cd server && node ../node_modules/typescript/bin/tsc
--noEmit --project
node_modules/.cache/paperclip-typecheck/tsconfig.tests.json --pretty
false`. Both pass.
- Final-head verification: all 17 focused cases passed again at
`c88687d69f2c87fe1b81b5fffd670e3cf26a884d` (8.00s). The prior exact CI
test-layer typecheck qualifies the unchanged typed logic; fresh CI
supplies broader final-head coverage.
- The follow-up commit removes only the new code comments to follow
CONTRIBUTING.md. Recovery and test behavior are unchanged. That revision
had Greptile 5/5 with its actionable review thread resolved. It is
retained as historical verification; current-head qualification is
below.

- The first final-head CI attempt failed in the unchanged CLI company
import/export fixture before assertions: its child server did not reach
the runtime-info health check within 30 seconds. Other suites passed.
The exact fixture passes locally with its original timeout. The single
failed-shard retry passed at the unchanged head, together with its
dependent CI checks. [Original failed
job](https://github.com/paperclipai/paperclip/actions/runs/42279773036/job/128106195975).
No assertion or timeout was changed.

- The independent required Runner support failure reproduced five
`IMAGE_ONLY: unbound variable` errors. #19602 repaired its fixture
inputs and merged as `4d554f08218b04df49ec0f2a0d5c5dfdfaf65660`. This
branch normally merges that master with no conflicts or force push. The
current host-preparation log verifier from #19580 remains intact.
- Final integrated head `ffc9e8d9516343a9823b4b80b5fcfccc548844ba`:
17/17 focused recovery cases pass (12.71s), all nine workflow-target
cases pass through the official Runner Vitest config (1.10s), and all
seven current server typecheck layers pass. Worktree preflight rebuilt
current Runner dependencies and reused the Cargo seed; it completed in
61.3s with Rust debug binaries skipped.
- Prior integrated head `ffc9e8d9516343a9823b4b80b5fcfccc548844ba`
finished with 54 successful checks. Its only code-check failure was a
global unhandled readiness rejection after 2,285 assertions passed in
the server8 shard, plus dependent verify. The real-child race is fixed
separately in #19624; it was not hidden by a rerun here.
- Final normal integration head
`550d1732b6960d6b349d03903f448fa1d71c17f6`: exact 17 paused-retry
lifecycle regressions pass (71.06s), all nine official workflow target
cases pass (2.43s), all seven server typecheck layers pass, and
module/whitespace checks pass. Required preflight completed in 45.641s.
Current preparation-log guards from #19580 remain intact. The diff
against master contains only the original three owner/test/doc files.
- Prior head 550d had two inherited model-default assertion failures
plus a retained-cleanup backlog 15s timeout. The exact unchanged cleanup
case passes locally (41.91s total including DB/hooks); no timer or
assertion was weakened and no old CI job was rerun. The model fixture
repair was independently merged in #19610 at
8621bcf53e.
- Current normal integration head
`14103750d5f26d7f476f937e981e3e45c94054ab`: 17/17 exact lifecycle cases
pass (75.33s), all 6 model normalization cases pass (6.58s), all seven
server typecheck layers and source checks pass. Required preflight
completed in 13.89s. No source guards or timeouts changed during this
integration.
- Current-head CI and Greptile are green. Earlier checks do not qualify
this final integration head.

- Current-head Greptile: 5/5 at
`14103750d5f26d7f476f937e981e3e45c94054ab`, with all review threads
resolved. Required CI passed: 56 successful checks, three intentional
skips, one neutral review status. The separate optional Paperclip review
job remains queued; no open findings.

## Risks

This adds a bounded indexed evidence query for a narrow historical
cancellation shape. Missing or contradictory evidence keeps work held.
It does not replay a provider turn or change current pause, budget,
approval, Stop, or workspace authority.

## Model Used

OpenAI Codex, GPT-6 family, with repository analysis, code editing, and
tool 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 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
- [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 17:13:52 -06:00
Nicky LeachandClaude Opus 5.5 a2aa69bc62 perf(ci): split the Runner package Vitest lane into three duration-balanced lanes (#19646)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Contributors wait for PR CI before merge, so CI wall-clock time sets
the pace of work
> - The Runner package Vitest lane is the PR CI critical path. It
finishes a median 15.3 minutes after the push
> - That lane runs all 311 package test files in one job with 2 workers:
1,381 seconds of serial work and 727 seconds of wall time
> - Ten files account for 61% of the time. Vitest's `--shard` splits by
file count, not by duration, so it cannot balance this work
> - This pull request splits the package files into three lanes balanced
by duration, and runs the native server group in its own lane
> - The benefit is about 232 seconds of tests per package lane instead
of 727 seconds

## Linked Issues or Issue Description

**What existing behavior does this improve?**

The time that PR CI needs to run the Runner package Vitest tests.

**Subsystem affected**

CI: the `Verify Paperclip Runner` Vitest lanes in `pr-trusted.yml`, and
`packages/paperclip-runner/scripts/run-pr-vitest-lane.mjs`.

**Current behavior**

The `vitest package` lane takes a median of 13.6 minutes. It is the last
job to finish in most PR runs. The lane keeps a 2-worker limit so that
Runner and sidecar processes can start within their deadlines. Average
CPU use is 29%.

**Proposed behavior**

Three package lanes each run about one third of the files, balanced by
measured duration. Each lane keeps the 2-worker limit. The native server
group runs in its own lane, as before.

**Reason and benefit**

The Runner lane stops being the critical path. More workers per lane
would add CPU contention and timeouts, so this change adds lanes
instead. The cost is about 3 runner-minutes per run for the extra setup.

**Breaking changes**

None. The `master` ruleset requires only `ci / verify` and `ci / e2e`,
so the new job names do not change required checks.

## What Changed

- Add `packages/paperclip-runner/scripts/vitest-package-lanes.mjs`.
- It gets the file inventory from `vitest list --filesOnly --json`, so
it follows `vitest.config.ts`.
- It partitions the files with the `partitionGeneralServerSuites`
function from `scripts/general-server-shard.mjs`.
- Add `packages/paperclip-runner/scripts/vitest-lane-durations.json`.
- It uses the same schema as
`scripts/general-server-shard-durations.json`.
- It is seeded from CI run 42290920882. It measures 310 of the 311
files.
- `run-pr-vitest-lane.mjs` accepts two new arguments:
  - `--package-lane=K/N` runs one partition.
  - `--native-server` runs only the native server group.
- The `--shard=1/2` and `--shard=2/2` arguments keep their exact
behavior for the matrix that master still sends.
- `.github/ci-task-matrix.json` and the fallback matrix in
`pr-trusted.yml` have three `vitest package (K/3)` lanes and the `vitest
native server` lane. The labels keep the `vitest` prefix because
`scripts/ci-profile/trace.mjs` matches `Verify Paperclip Runner
(vitest`.
- Tests:
- the lanes cover the inventory exactly once, checked with real `vitest
list`;
  - the old `--shard` commands plan the same commands as before;
  - the manifest schema is correct;
  - bad lane arguments are refused.
- Update the Runner Vitest lane paragraph in `doc/DEVELOPING.md`.

## Verification

- `node --test scripts/__tests__/run-vitest-stable-shard.test.mjs`:
44/44 pass.
- `node --test scripts/__tests__/runner-release-crate-cache.test.mjs
.github/scripts/tests/ci-plan.test.mjs`, together with the file above:
71/71 pass.
- Related workflow, cache, routing and profile tests: 446/446, 153/153
and 14/14 pass.
- Local end-to-end run of `GITHUB_WORKFLOW=PR node
scripts/run-pr-vitest-lane.mjs --package-lane=3/3` on macOS:
- the prep steps ran, and the lane ran 104 of 311 files in 123 seconds;
- 2 tests failed: a macOS `kill EPERM` and a 5-second timeout under full
local load without the CI worker limit;
  - both files pass when run alone (35/35 tests).
- This PR's own CI runs the matrix from master, so it uses the old
`--shard` commands. The new lanes run in CI only after merge. Reviewers
can compare the job durations of the first PR runs after the merge.

| Lane | Files | Serial estimate | Wall estimate with 2 workers |
| --- | --- | --- | --- |
| 1/3 | 103 | 464.9 s | ~232 s |
| 2/3 | 104 | 465.0 s | ~232 s |
| 3/3 | 104 | 465.0 s | ~232 s |

## Risks

- The weights come from one CI log and do not include collection or
setup time. The balance can be uneven until a refresh measures real lane
runs.
- Each lane passes its file paths to Vitest as filters, and Vitest
matches a filter as a substring of the path. A path that is a substring
of another path would run a file in two lanes. No current path does
this. If one does, the script prints a warning, and no file is dropped.
- Each package lane runs `ensure:eval-build-deps` and `build:rust`
again, which adds about 3 runner-minutes per run.
- A comment in `scripts/run-vitest-stable.mjs` (about line 110) still
says that the last Runner shard runs the native group. A follow-up will
correct it.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with tool use and
code execution. A Claude sub-agent wrote the change, and the parent
session reviewed it and ran the local lane check.

## 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:12:05 -08:00
DottaandPaperclip 2e340d6235 fix(ui): show unread message counts in agent chat pickers (#19627)
## Thinking Path

> - Paperclip helps people manage AI agents and their work.
> - People switch between agent chats to read replies.
> - The agent picker does not show how many messages are unread.
> - People need that count before they select a chat.
> - The chat list already has the viewer's read state.
> - This pull request adds numeric badges from that shared list.
> - People can find unread replies without extra requests for each
agent.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

The chat header agent picker and the secondary sidebar's Add chat
picker.

**Subsystem affected**

Agent Chat UI, shared task types, and the chat read-state service.

**Current behavior**

The pickers do not show the unread message count for each agent.

**Proposed behavior**

Show the unread count beside each agent. For example, show 3 for three
unread agent messages. Hide the badge when the count is zero. Keep
switching available while chat data loads.

**Reason and benefit**

People can see which chats need attention before they switch. The count
uses the existing shared request and cache.

**Breaking changes**

The chat response gains an optional `unreadMessageCount` field. Existing
fields and read receipts keep their behavior. There is no database
migration.

**Additional context**

Related: #19425 changes chat structure. This change adds counts to the
current agent pickers. The duplicate search found no PR for these count
badges.

## What Changed

- Count unread agent comments in the existing grouped read-state query.
Keep company and viewer scope.
- Exclude human messages, deleted comments, system notices, and
fast-response receipts.
- Pass memoized count maps to both agent pickers. Use a direct lookup
for each row.
- Show numeric badges and accessible singular or plural labels.
- Test count changes, account isolation, cached opens, search, and large
agent lists.
- Update the chat stories, fixture data, and development guide.
- Make the Slack owner retry-deadline test deterministic after
reproducing its CI failure.

## Verification

- On current head `05f5fa6201`, the [main CI
workflow](https://github.com/paperclipai/paperclip/actions/runs/42305845930)
passed, including build, typecheck, all applicable test shards, browser
shards, and final verification. The prior server shard 2, 6, and 7
failures do not recur.
- [Greptile's current-head
review](https://github.com/paperclipai/paperclip/pull/19627#issuecomment-6696161631)
is 5/5, its check succeeded, and both previous review threads are
resolved. Current head check inventory is complete: 55 successful
checks, three intentional skips, and no pending or failed checks. The
Paperclip application review summary reports paused; no new completed
Paperclip review is claimed. Its existing findings are resolved, and the
independent current-head Greptile review is 5/5.

- Current head `05f5fa6201` merges master `8621bcf53e` to resolve an
import conflict. Master now includes the same model-default test repair.
This branch keeps its upstream import, so that file no longer differs
from master.
- After this merge: all 21 server tests in the Slack owner and
model-default suites pass, all 22 tests in the three picker/sidebar
suites pass, and token gates pass. The standard preflight still cannot
build Rust because `cargo` is absent. Fresh CI must supply the broad
checks.

- Head `7d125fb8c7` replaces the Slack owner test's real timer wait with
the worker's existing clock input. It checks that the intent stays
pending one millisecond before the retry deadline and clears at the
deadline. All 15 Slack owner integration tests pass locally. Runtime
retry behavior is unchanged.
- On `4cfb1e9d55`, both review checks gave 5/5. CI [server shard
2](https://github.com/paperclipai/paperclip/actions/runs/42302458830/job/128183671349)
failed the Slack credential-lease retry test. The same assertion failed
in an unchanged local run. A diagnostic run with added logging passed;
the exact timer deviation was not captured. The new test removes the
real-time wait assumption and checks the clock boundary directly. The
earlier model-default and cleanup failures did not recur in that CI run.

- On `a3378dfc40`, both Greptile and Paperclip Review gave 5/5 with no
open findings. Typecheck, build, all eight browser shards, and most test
shards passed.
- That CI attempt failed two server shards. The model-default test still
expected `gpt-5.6-sol`, while the current default is `gpt-6.1-sol`. I
reproduced both failures locally. Commit `4cfb1e9d55` uses the exported
default-model constant; all six tests in that suite now pass. Production
model selection does not change.
- The retained-cleanup backlog test exceeded its 15-second limit in
[server shard
6](https://github.com/paperclipai/paperclip/actions/runs/42299858777/job/128175193320).
The unchanged test passes locally with the same limit. Its CI timeout
cause is not established. No assertion or timeout was changed. The fresh
CI run must recheck it.

- Review repair: commit `0db631b215` keeps the existing-chat and
unread-agent arrays stable when chat data and the active agent do not
change. A new 500-agent test fails before the repair and passes after
it. The test also checks count refreshes.
- The first CI typecheck failed on duplicate `IMAGE_ONLY` and
`TESTOPS_REQUESTED` keys in `tests/runner-e2e/workflow-target.test.ts`.
This was present in upstream code. Master commit `e482f0f632` removes
the duplicate keys. This branch now includes that commit. All nine
workflow-target tests pass locally.
- After the latest master merge: all 22 tests in the three affected
picker/sidebar suites pass. Before that merge, all 38 focused UI tests
passed.
- The current head is `05f5fa620134ad199a3d85b66fb2eb93d3bb6077`. All
current status checks are complete, the aggregate verify gate succeeds,
mergeability is CLEAN, and no unresolved review threads remain. The
maintainer has instructed normal merge after checks pass.

- After merging current master: 37 focused UI tests pass across
AgentChatPicker, CoordinatorAgentSwitcher, AgentConversationsSidebar,
useAgentChatRead, and SidebarAgentChats.
- After merging current master: all 38 proactive-chat integration tests
pass with a real disposable PostgreSQL database. They check counts of 2,
1, and 0 at exact read cutoffs.
- The prior revision passed UI typecheck and build, shared build,
isolated low-memory server typecheck, token gates, and 30 map/catalog
tests.
- The 500-agent test bounds index work and count lookups. Search does
not rebuild the existing-chat index.
- Browser checks used production components with simulated API data.
Dark, light, mobile, and Add chat cases passed. Counts of 3 and 12
stayed visible through search. Read agents had no badge. Cached opens
made no chat API requests.
- Twenty local cached opens took 34.5–57.6 ms, with a 38.4 ms median.
This is an unthrottled fixture measurement.
- Earlier full local checks could not complete because Rust cargo is
absent and the workspace has limited memory. The standard worktree
preflight also stops at missing cargo. Required CI must supply broad
checks for this PR head.
- The first server typecheck ran out of memory while Storybook was
active. Its isolated low-memory retry passed. No visual baseline
comparison or live provider delivery is claimed.

## Risks

- Low risk. The count replaces a boolean aggregate in the same grouped
query. Very long chat histories still need that existing scan.
- A count can be stale until the existing chat-list cache refreshes.
Read receipts still control the unread cutoff.
- The change adds no polling, per-agent history request, database
migration, or permission change.

## Model Used

OpenAI GPT-6 through Codex, with code editing, terminal tools, and
browser verification. The runtime did not expose a more specific served
model ID or context-window size.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 17:10:56 -06:00
DottaandClaude Opus 5.5 7941f6b051 fix(runner): show OpenRouter's invalid model ID as a model rejection (#19548)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs use the Paperclip runner with drivers such as OpenCode
> - When a provider rejects the selected model, the product shows "Model
unavailable" and offers a model choice
> - OpenCode reports provider HTTP errors as `APIError` with the message
under `data`, not at the top level
> - The runner then reported "Provider turn failed", so an invalid model
ended as a generic adapter failure
> - This pull request classifies OpenRouter's invalid model ID with a
closed provider-failure category, and makes the server keep that
classification
> - The benefit is that a person sees "Model unavailable" and the real
provider message, and can choose another model

## Linked Issues or Issue Description

**What happened?**
A chat evaluation selected a model ID that OpenRouter does not know.
OpenCode reported `turn.failed` with `error: { name: "APIError", data: {
message: "<model> is not a valid model ID", statusCode: 400,
isRetryable: false } }`. The run was saved with `errorCode:
adapter_failed`. The task chat showed "The OpenCode run failed before it
completed." and no "Model unavailable" marker. The provider notice said
only "OpenCode session failed.".

**Expected behavior**
A non-retryable rejection of the selected model is saved as
`native_provider_model_rejected`, so the product shows "Model
unavailable" and the model choice. The notice shows the provider's
message.

**Steps to reproduce**
1. Configure an OpenCode agent with an OpenRouter model ID that does not
exist.
2. Send the agent a message.
3. Before this change, the run fails as `adapter_failed` and the notice
says "OpenCode session failed.".

## What Changed

- `drivers/opencode/provider-failure.ts`: a non-retryable OpenCode
provider error whose message says the model is not valid (`not a valid
model`, `model_not_found`, model not found or not supported) gets the
closed category `model_unavailable`. A provider that allows a retry
keeps the existing category.
- `native-session-runtime.ts`: a non-recoverable failure with
`model_unavailable` is a model rejection. The runtime reads the closed
category, not provider text.
- `native-session-executor.ts`: a non-recoverable
`NativeProviderTerminalFailure` that the runner marked as a model
rejection is saved as `native_provider_model_rejected`. Before, every
such terminal was saved as `native_provider_terminal_failed` before the
marker was read.
- `drivers/opencode/opencode-server-driver.ts`: the provider notice
summary shows the provider message from `data` when there is no
top-level message. Provider text is not used as the turn failure
message, so it does not reach `run.error` or Sentry.
- Tests: provider-failure mapping (retryable and non-retryable), runtime
classification by category, driver notice summary with no failure
message, and an executor test for the saved code and terminal
disposition.

## Verification

- `npx vitest run src/native-session-runtime.test.ts
src/drivers/provider-failure.test.ts src/drivers/opencode` in
`packages/paperclip-runner`: 16 files, 412 tests passed.
- `npx vitest run
src/services/native-runtime/native-session-executor.test.ts` in
`server`: 1,152 tests passed.
- Negative controls: reverting the provider-failure mapping, the runtime
check, or the executor branch each makes its new test fail.

## Risks

- Low risk. Classification uses a closed category and a runner-set
marker, not provider text.
- The model check on the provider message is text-based inside the
OpenCode mapper. A provider that uses "not a valid model" for another
reason with a non-retryable error will show "Model unavailable".
- Provider text appears in the notice summary, as provider notices
already did for top-level messages.

> 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 17:08:04 -06:00
Nicky LeachandClaude Opus 5.5 f2299cb00f feat(ci): re-run failed PR jobs once when every failure was a lost runner (#19640)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Contributors wait for PR CI before merge, so CI wall-clock time sets
the pace of work
> - PR CI runs on self-hosted AWS runners, and these runners are
sometimes lost mid-job
> - On 2026-11-06, 21 of 51 failed jobs on commits that later passed
were runner loss. The losses came in fleet-wide bursts, for example 10
jobs on 10 instances in 51 seconds
> - After each loss, a person must notice the failure and click "Re-run
failed jobs"
> - This pull request adds a workflow that re-runs the failed jobs once,
only when every failure was a lost runner
> - The benefit is fewer manual re-runs and a shorter wait for CI to
pass

## Linked Issues or Issue Description

**What existing behavior does this improve?**

The recovery of PR CI runs that failed only because a self-hosted runner
was lost.

**Subsystem affected**

CI: the trusted PR workflow (`pr.yml` → `pr-trusted.yml`).

**Current behavior**

When an AWS runner is lost, the job fails with "The self-hosted runner
lost communication with the server" or "The runner has received a
shutdown signal". The PR stays red until a person re-runs the failed
jobs.

**Proposed behavior**

When the `PR` workflow completes with a failure on attempt 1, a new
workflow examines the failed jobs. If every failed job lost its runner,
it re-runs the failed jobs one time. If any job failed for another
reason, it does nothing.

**Reason and benefit**

A runner loss is not a code problem. The re-run starts without waiting
for a person to notice the failure.

**Breaking changes**

None.

## What Changed

- Add `.github/workflows/ci-runner-loss-rerun.yml`. It triggers on
`workflow_run` for `PR`, and it acts only on attempt 1 failures of
`pull_request` runs in this repository. It runs on `ubuntu-latest`, so
it does not depend on the AWS fleet.
- Add `.github/scripts/runner-loss-rerun.mjs`. The script:
- lists the attempt-1 jobs and ignores the aggregate jobs `ci / verify`
and `ci / e2e`;
- finds runner loss in the check-run annotations. If the only annotation
is "The operation was canceled.", it reads the `##[error]` lines in the
job log;
- calls `rerun-failed-jobs` only if every remaining failed job lost its
runner;
- does not re-run when the gate job failed, the PR head moved, the PR is
closed, or a newer run exists for the same commit;
- writes the decision and a job table to the step summary. A `--dry-run`
mode never posts.
- Add 16 tests, with trimmed fixtures from four real runs.
- Add a paragraph about the workflow to the "Trusted PR Workflow"
section of `doc/DEVELOPING.md`.

## Verification

- `node --test .github/scripts/tests/runner-loss-rerun.test.mjs`: 16/16
pass. The tests cover:
- re-run when all jobs were lost, including the case where the signal is
only in the log;
- no re-run for mixed failures, real failures, only aggregate failures,
attempt 2, a failed gate, or a stale run;
- pagination, incomplete listings, a cross-host link that is refused,
and the workflow guards.
- `node --test '.github/scripts/tests/*.test.mjs'`: 760/760 pass.
- A read-only dry run against the real GitHub API for six recent runs
classified all six correctly.
- The workflow can run on GitHub only after it is on master. Reviewers
can make sure that the first runner-loss failure after the merge
triggers one re-run.

## Risks

- **Trust:** a re-run of failed jobs uses the outputs of the gate job
that succeeded on attempt 1, including its AWS runner admission. A
person who clicks "Re-run failed jobs" gets the same result. The only
difference is that `github-actions[bot]` triggers attempt 2. The
workflow never checks out or runs PR code.
- **Failed gate:** if the gate job itself failed, the script does
nothing. A re-run by the bot would put all jobs on GitHub-hosted
runners.
- **Profiling:** `scripts/ci-profile/export.mjs` exports only attempts
triggered by authorized users. The bot-triggered attempt 2 is not
exported to Honeycomb. Attempt 1 is still exported.
- **Events:** GitHub may not create `workflow_run` events for attempts
that `GITHUB_TOKEN` started. Then other `workflow_run` consumers do not
react to attempt 2. Required checks still update.
- **Permissions:** `actions: write`, `checks: read`, `contents: read`
and `pull-requests: read`. The last one lets the script confirm that the
run still belongs to the open PR's current head.
- **Cancelled jobs:** a runner loss where the lost jobs end as
`cancelled` and only the aggregate jobs show `failure` is not re-run.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with tool use and
code execution. A Claude sub-agent wrote the change, and the parent
session reviewed it.

## 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:05:42 -08:00
Nicky LeachandClaude Opus 5.5 913102870d fix(environments): fill the managed instance default once, under a lock (#19632)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - On a managed instance under `enableManagedSandboxOnly`, boot points
the instance default at the managed sandbox when nothing deliberate is
set
> - That write read the default and wrote it later without a lock. Two
overlapping boots, or a boot and an admin write, could overwrite a value
the other had just committed
> - A revert path also reset that stamped default whenever the mode was
turned off, so a platform-filled default could change again later
> - A planned plugin-environment default should fill only an unset
default and never change a set one, which needs every instance's default
to stay set
> - This pull request makes the fill one locked transaction and removes
the revert, so a filled default is an ordinary set value
> - The benefit is a single, race-free rule: boot fills an unset default
once, and nothing changes a set default afterwards

## Linked Issues or Issue Description

Refs #19551 (the plugin-environment default that builds on this rule)

**Subsystem affected**
Server environments: the managed environment bootstrap and the instance
settings route.

**Problem or motivation**
`applyManagedEnvironments` read the instance default, decided, and wrote
it in separate steps with no lock, so a concurrent boot or admin write
could be overwritten. When `enableManagedSandboxOnly` was not declared,
a revert reset a default the platform had filled. That needed a
`managedDefaultStamped` marker, plus a marker clear on every admin
default write. A later boot could therefore change a default that had
already been set.

**Proposed solution**
Fill the default in one transaction that locks the instance default row
and rechecks it. Fill only an unset, local or deleted default. Remove
the revert and its marker, so a filled default behaves like any other
set value.

**Alternatives considered**
Keeping the revert and adding locks to it. A revert on a mode change
would still move an existing instance's default later, which a fill-only
plugin default cannot tolerate.

**Roadmap alignment**
This narrows existing managed-environment behaviour. It adds no product
surface.

## What Changed

- New `server/src/services/managed-instance-default.ts`:
`fillUnsetInstanceDefault(db, environmentId)`. In one transaction it
creates the settings row if needed, locks the instance default row (`FOR
UPDATE`), and rechecks the current value. It writes only when the
default is unset, names the local environment, or names a deleted
environment. Any other set default is kept.
- `server/src/services/managed-environments.ts`:
- Under `enableManagedSandboxOnly`, the managed sandbox fill now uses
that helper, through a new `fillInstanceDefault` test seam.
- The mode-off revert and the `managedDefaultStamped` marker are
removed.
- The unused `instanceSettings` seam option is removed, along with the
`getById`, `update` and `findManagedSandboxEnvironment` methods of the
environments seam.
- `server/src/routes/instance-settings.ts`: an explicit default write is
a plain `update` again. The marker clear and its transaction are gone.
- `doc/DEVELOPING.md`: the "Managed environments" section states the
fill rule.

Existing rows may still carry `managedDefaultStamped` in their metadata.
Nothing reads it any more, and it is not part of the managed stock hash.

## Verification

- `cd server && npx vitest run
src/__tests__/managed-instance-default.test.ts` (embedded Postgres):
- The fill writes an unset default, including on a fresh instance with
no settings row. It also writes a default that names the local
environment, or one whose environment was deleted.
- It never changes a default set to an ssh, plugin or other sandbox
environment, and leaves a default that already names the target alone.
- Controlled overlap: an uncommitted admin default write makes the fill
wait, and the fill then keeps the admin's value. This test fails without
the row lock.
  - Two overlapping fills: exactly one writes.
- Real boot pass: the default is filled under managed-sandbox-only and
stays the same after a boot with the mode off. A set default stays on
every boot.
- `npx vitest run src/services/managed-environments.test.ts
src/services/managed-environments-plugin-entries.test.ts`: the fill runs
only under managed-sandbox-only, with the managed sandbox row. It never
runs for a plugin environment. Nothing is written when the mode is off.
A failed fill does not fail the ensure.
- `npx vitest run src/__tests__/instance-settings-routes.test.ts`: an
explicit default write is a plain update that touches no environment
row.
- `npx vitest run src/__tests__/managed-plugin-environments.test.ts
src/__tests__/environment-routes.test.ts
src/__tests__/server-startup-feedback-export.test.ts
src/__tests__/instance-settings-service.test.ts`: pass.
- `pnpm --filter @paperclipai/server typecheck` passes.
- I did not run the full `pnpm test:run`, `pnpm -r typecheck` or `pnpm
build`. CI covers them.

## Risks

- Behaviour change: when `enableManagedSandboxOnly` is turned off for an
instance, a default the platform filled with the managed sandbox now
stays. Local execution is still allowed then, but it is no longer the
default unless an admin picks it.
- No schema change. Stale `managedDefaultStamped` keys stay in some row
metadata and are ignored.
- The fill holds the instance default row lock for one short transaction
at boot.

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

## Model Used

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

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 15:01:58 -08:00
Devin FoleyandPaperclip 8621bcf53e Cancel subscription sign-in requests when leaving agent setup (#19610)
Back during pending subscription sign-in could accept a late response and
reopen agent setup. Abort pending reads, release abandoned sessions, and
ignore stale responses. Wait for cleanup before starting a replacement,
and preserve sessions belonging to another account or environment.

Cover deferred resume/start/prompt responses for both providers, local
login cancellation, quick reselection, and browser request abort. Stabilize
Runner fixtures and normalization assertions exposed by integration CI.

Verified: 483 focused UI tests, six onboarding browser cases, UI typecheck
and token gates, Runner fixture checks, and the complete CI run on
221f2e3eb7. Greptile scored 5/5;
all review threads are resolved. Earlier failures and recovery evidence
remain in the PR description.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-11-06 14:31:11 -08:00
DottaandPaperclip fdda02a26e fix(ui): remove stale plan approvals from the composer (#19528)
## Thinking Path

> - Paperclip helps people manage AI agents and their work.
> - A task can ask a person to approve a saved plan.
> - The server refuses approval after a newer plan replaces that plan.
> - The document and request queries can finish at different times.
> - The composer can then keep controls for a plan that is already out
of date.
> - This change removes those controls when a newer saved revision is
known and refreshes the request state.
> - People can review the current plan without an obsolete approval
above the composer.

## Linked Issues or Issue Description

**What happened?**

An old plan approval can stay above the task composer after the plan
changes. Selecting Approve returns a superseded error. The failed action
does not refresh the request list.

**Expected behavior**

Remove the old approval and its pending count as soon as the UI knows
that the plan has changed. Keep a newer approval visible if its document
query still has an older revision. Preserve accepted decisions and
unrelated requests.

**Steps to reproduce**

1. Create a confirmation for a saved plan revision.
2. Save a newer plan revision while the old request remains in the
browser cache.
3. Read the composer before the request query finishes.
4. Try an old approval after the server has expired it.

**Paperclip version or commit**

Source checkout based on `5d25117ffa`.

**Deployment mode**

Source checkout. The regressions use the production React components and
query cache in jsdom.

Related work: Refs #14907 (replaced request receipts) and Refs #15025
(server replacement for requests from board users). This change fixes
the interval between document and request reads, plus the refresh after
a failed resolution. It does not change those server or history rules.

## What Changed

- Remove pending approvals for older saved plan revisions from the task
display and composer.
- Compare revision numbers, or document and request times for targets
that have only a revision ID. Keep newer requests when the document
cache is behind.
- Refresh request queries after document activity and settled approval
errors.
- Use the same plan freshness check for recovery retry. An old hidden
approval no longer blocks Retry agent.
- Test the recovery controls and the guards for missing plans, accepted
decisions, questions, other tasks, other companies, and older document
reads.
- Add regressions for both workspace presentations, task and
conversation modes, revision formats, unrelated requests, and accepted
history.
- Document the display behavior in the development guide.

## Verification

- TDD: five assertions failed before the main fix. Old controls stayed
in the composer, document activity did not refresh requests, and
superseded accept or reject responses did not read the new state.
- The previous session recorded 815 passing focused UI tests after the
recovery retry fix at `d95cf3235ed83dc1f6c7f51372dff408814eb15c`. UI
typecheck, the production UI build, and token gates also passed.
- The main fix at `5147fd29a03413596d5b2445ae3b480352cbff1a` passed all
CI gates. This includes typecheck, build, general tests, and eight E2E
shards. [CI
run](https://github.com/paperclipai/paperclip/actions/runs/42266101662).
- The prior affected local run passed 7,656 UI tests, with one skipped,
and 38 shared tests. Its CLI phase had two file-mode failures under the
agent's `0077` umask and a child-server timeout during first-use builds.
All three CLI files passed after a new check with `umask 022` and warm
build dependencies: 25 tests passed. No CLI source changed. The initial
full command did not pass or store a reusable pass.
- On this continuation, the local test command could not start because
the restored workspace has no installed Vitest dependency. No test
verdict came from this attempt.
- Final
[CI](https://github.com/paperclipai/paperclip/actions/runs/42302544154)
passes all applicable required gates for
`d95cf3235ed83dc1f6c7f51372dff408814eb15c`, including typecheck, build,
general tests, and eight E2E shards. Greptile and Paperclip reviews both
report 5/5 at that exact revision, with no unresolved findings. The
separate advisory review workflow remains queued.
- No live browser acceptance or deployment is claimed.

## Risks

- The UI must distinguish an older approval from an older document
cache. Tests cover both query orders and preserve newer approvals.
- This is a display and cache refresh change. Stored decisions, server
authorization, approval gates, and database schemas are unchanged.

## Model Used

OpenAI GPT-6 through Codex, with reasoning, tool use, and code
execution. The exact deployment model ID and context window size are not
exposed in this environment.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with 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 described the issue in-PR following the bug template
- [x] I have not referenced internal/instance-local Paperclip issues or
links
- [x] My remote 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 for the final revision
(scope and initial environment failures are stated 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 16:25:51 -06:00
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