Commit Graph
3239 Commits
Author SHA1 Message Date
Devin FoleyandClaude Opus 5.5 4033f7f17f fix(recovery): let a task Retry run keep its bounded transient retry (#19384)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A user can click Retry on a blocked task. Paperclip then admits one
new run that continues in the saved workspace
> - That run can fail before the provider starts, for example when the
download of the saved workspace snapshot gets a transient `ECONNRESET`
> - The scheduler classifies this failure as transient and tries to
schedule the normal bounded retry
> - The automatic-retry admission accepts only continuations that a user
comment started. It refuses a Retry continuation (`commentId: null`,
`taskRetryRequestId` set) with `continuation_user_authorization_missing`
> - So the task does not retry. Paperclip escalates the restore failure,
and the task becomes blocked again after one network reset
> - This pull request lets a plain task Retry renew its authority for
the bounded transient retry, with its own receipt for each new run
> - The benefit is that a transient setup failure after Retry uses the
normal retry budget and does not block the task

## Linked Issues or Issue Description

**What happened?**
A run started by the task Retry button failed during setup:
`remote_workspace_restore_failed: step=snapshot_download;
storage=transport_closed code=ECONNRESET`. The run event said "The
automatic retry could not revalidate the original user continuation."
(retry reason `transient_failure`, attempt 1 of 2). No retry was
scheduled, and the task became blocked.

**Expected behavior**
A transient setup failure of a Retry-admitted run gets the same bounded
automatic retry as a run that a user comment started.

**Root cause**
`admitExplicitContinuationRetry` in
`server/src/services/explicit-native-continuation.ts` required
`explicitUserContinuation.commentId` to be a GUID. A Retry continuation
has `commentId: null`. Also, dispatch in
`server/src/services/execution-continuation.ts` bound a Retry receipt to
exactly the run that it admitted, so a successor run could not prove its
authority.

Refs #19207 (it named remote restore failures and left a board recovery
action)
Refs #19340 (it retries quarantined tasks in their saved workspace)
Refs #19326 (same bounded retry path for an out-of-date Runner)

## What Changed

- `admitExplicitContinuationRetry` now accepts two kinds of authority.
The comment path is unchanged and moved into
`readCommentRetryAuthority`. The new `readTaskRetryRetryAuthority`
accepts a plain task Retry only. The Retry request must be coalesced,
from a user, with the same generation and actor, strategy `continue`,
and no chat, queued-message, canonical-intent or fresh-workspace
readiness. Interrupt and failed-run retries stay one-run.
- The successor run gets its own `explicit_user_continuation_retry`
receipt with the same `taskRetryRequestId` and actor and
`automaticRetry.sourceRunId` set to the parent. The Retry request stays
bound to the run that it admitted.
- Dispatch (`buildExecutionContinuation`) accepts a Retry successor only
from its own receipt, its exact `retryOfRunId` parent, the current
`executionRunId` and the unchanged Retry intent. The direct path for the
admitted run is unchanged.
- Tests in `server/src/services/task-retry.test.ts`: one regression for
the full retry chain, and eight refusal cases.

## Verification

- Red: before the fix, the new test failed with `{ outcome:
"not_scheduled", errorCode: "continuation_user_authorization_missing"
}`. This is the same refusal as the incident.
- Green: `pnpm --filter @paperclipai/server exec vitest run
src/services/task-retry.test.ts` gives 58/58.
- The regression test proves these points: the first retry is scheduled
with `{ previousRunId: parent, commentId: null, taskRetryRequestId }`;
the Retry request stays bound to the admitted run; dispatch accepts the
successor; a repeated schedule call adopts the same successor without a
second receipt; a second transient failure retries from its immediate
parent; the budget then stops with `retry_exhausted`; dispatch refuses
the successor when its own receipt is removed.
- The refusal cases prove that no successor and no receipt are created
after: wrong actor, unadmitted request, wrong generation, request bound
to another run, chat Retry, fresh-workspace readiness, reassignment,
superseding work.
- Nearby suites pass (601/601): `execution-continuation`,
`legacy-explicit-continuation-recovery`, `explicit-native-continuation`,
`execution-continuation-provenance`, `quarantined-task-retry`,
`issue-queued-comments-routes`, `task-retry`.
- I did not run the layered server typecheck locally because of a disk
limit in the sandbox. CI must confirm it.

## Risks

- Behavior change: a plain task Retry run that fails transiently now
retries automatically within the existing bounded budget (2 attempts).
The scheduler still decides which failures are transient. The same rule
already applies to runs that a user comment started.
- Chat, queued-message and fresh-workspace Retries, failed-run retries
and Interrupt keep their one-run contracts.
- No schema or migration change.
- Documentation gap: `doc/execution-semantics.md` (line 1210) says
"One-run Retry and Interrupt intents retain their separate admission
contracts; they are not renewable user-message authority." That sentence
must change to describe this rule. The author's publication path cannot
carry the full 320 KB file, so that one-sentence edit is still pending.
The proposed text: "A plain task Retry that continues in the saved
workspace renews the same way: the successor's receipt names the same
Retry request and actor, while the Retry request itself stays bound to
the run it admitted. Dispatch accepts that successor only from its own
receipt, its exact retry parent, and the unchanged admitted Retry
request, so a transient failure before or during that run, such as a
reset saved-workspace download, uses the normal bounded retry budget
instead of blocking the task. Chat, queued-message and fresh-workspace
Retries, failed-run retries, and Interrupt intents retain their separate
one-run admission contracts; they are not renewable authority."

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

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), used through Claude
Code as a Paperclip agent, with tool use and code execution (shell,
Vitest on embedded PostgreSQL).

## 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
- [ ] I have updated relevant documentation to reflect my changes (the
`doc/execution-semantics.md` sentence above is pending)
- [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-05 17:54:00 -08:00
c9c4949c86 build(deps): bump zod from 4.4.3 to 4.6.6 (#17364)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The shared validators use zod to check API input, and the Runner
contracts check some of the same values again
> - zod below 4.6.6 has a high-severity memory issue
(GHSA-mrhx-c7xc-m3w5), so this bump is necessary
> - zod 4.6 also changes string `min`, `max`, and `length`: they now
count Unicode code points, not UTF-16 units
> - The Runner question parser and the goal readers still use JavaScript
string length, so emoji text can pass zod and then fail later
> - This pull request bumps zod and keeps the string-length bound on
those schemas
> - The benefit is that we get the security fix, and these inputs still
fail at validation

## Linked Issues or Issue Description

Refs GHSA-mrhx-c7xc-m3w5.

**What happened?**
With zod 4.6, three CI tests failed on this bump. Two showed an emoji
body that is over the agent commentary ceiling and that zod now accepts.
The third is the scheduler timeout described under Verification.

**Expected behavior**
A dependency bump does not widen an API limit. Text over the limit gets
a validation error.

**Impact**
Emoji text between the two bounds passed validation and failed later:
- A question set is created, and then each answer gets a 422. The
question stays pending and nobody can answer it. A schema and parser run
confirmed the mismatch. This was not run over HTTP.
- The Muse route returns a 500 because `parsePaperclipQuestionSet`
throws an error that is not an `HttpError`. This comes from reading the
code. It was not run.
- A Runner goal normalizes to null and replaces the stored goal, or it
is marked blocked. This comes from reading the code. It was not run.

## What Changed

- Bump zod to `^4.6.6`. The lockfile resolves 4.6.7. (Dependabot)
- Add `maxStringLength` to `packages/shared/src/validators/text.ts`. It
checks zod `max` and JavaScript string length.
- Use `maxStringLength` for the question-set text fields, the Runner
goal objective, and the agent commentary body.
- Add regression tests for the question-set bound and the goal objective
bound.
- Dependabot recreated the branch on current `master` (`@dependabot
recreate`) to resolve `pnpm-lock.yaml` conflicts. This happened twice.
The lockfile is Dependabot's. Each time, the two fix commits were
applied again without changes.

## Verification

- Compared zod 4.4.3, 4.6.5, 4.6.6, and 4.6.7 directly. The change to
`max` is in 4.6.5 or earlier, and 4.4.3 does not have it.
- Passed locally: `packages/shared` tests `agent-commentary`,
`validators/issue`, and `runner-goal`, plus `tsc --noEmit` in
`packages/shared`.
- Passed locally on the first fix commit: server
`agent-commentary.integration`, `agent-commentary-helper`, and
`openapi-routes`. The later commit changes these schemas again. CI
passed these tests at `b5e9dc7352`.
- Local testing was focused. GitHub CI is the full regression check. CI
passed at `b5e9dc7352`, including `ci / verify` and `ci / e2e`.
- Greptile reviewed `b5e9dc7352`. The score is 5/5, with no inline
comments.
- After the recreate, the fix commits applied cleanly as `bad62c3ba5`
and `3294552e65` on Dependabot's `46789d1487`. Their diff is the same as
the diff Greptile reviewed. The lockfile still resolves zod 4.6.7. The
focused `packages/shared` tests (`validators/issue`, `runner-goal`) and
`tsc --noEmit` in `packages/shared` passed again locally. A second
`pnpm-lock.yaml` conflict came before CI finished at `3294552e65`.
- Second recreate: Dependabot ignored the first `@dependabot recreate`
(no reaction and no update job after 20 minutes). It acted on the second
command and pushed `83c328c15c`. The fix commits applied cleanly as
`dd3e87d93e` and `95b450048f`, with the same diff. The lockfile still
resolves zod 4.6.7. The same focused tests and `tsc --noEmit` passed
again locally. Greptile first scored `95b450048f` 4/5 with one P2: the
description did not explain the source changes. Greptile read
Dependabot's replacement description, which was live during that review.
The description was restored and the thread resolved. The new review
scored `95b450048f` 5/5 with no findings. CI failed at `95b450048f` in
two shards. Neither failure comes from the zod change:
- `server (9/12)`: `server/scripts/reporting-lock-policy.test.mjs` fails
with "Reporting lock changed; review the dependency correction before
install". #19283 added this test today. It requires the committed
`pnpm-lock.yaml` hash to match `tests/runner-e2e/report-lock.json`.
Every pull request that changes the lockfile fails it until the hashes
are renewed, for example #17172 and #17911.
- `workspaces-a (1/2)`:
`cli/src/__tests__/company-import-export-e2e.test.ts` timed out waiting
for its child server. The same test failed the same way on two unrelated
branches in the same hour (runs 42115721271 and 42109846042). A rerun of
this shard at `95b450048f` passed. The rerun of `server (9/12)` failed
again on the reporting-lock test.
- #19356 fixed the reporting-lock gate for Dependabot lockfiles and
merged. A re-run reuses the old merge commit, so the empty commit
`482d76686e` starts a run against current `master`. `pnpm-lock.yaml` and
the reporting-lock proof on `master` did not change since this branch's
base.
- CI at `482d76686e`: `ci / policy` passed, and `server (9/12)` skipped
the reporting-lock test as #19356 intends. `e2e shard (6/8)` failed once
in `cloud-auth.spec.ts` ("apiResponse.json: Response has been disposed",
a Playwright race in a route handler). The same test passed at
`95b450048f` with the same code. A rerun passed. All checks pass,
including `ci / verify` and `ci / e2e`.
- Earlier failure: `issue-monitor-scheduler.test.ts` ("lets the board
check a scheduled monitor on an in_progress task") timed out in CI at
`d60b5f56d8`. It passed 5 of 5 local runs with the first fix. The cause
is unknown. The test waits 3 seconds for heartbeat runs to settle, so CI
load is possible, but this is not proven. The test passed in the CI run
at `b5e9dc7352`. This shows recovery. It does not explain the earlier
timeout.

## Risks

- All other zod string bounds now count code points. Emoji text can be
up to 2N UTF-16 units and 4N UTF-8 bytes (it was N units and at most 3N
bytes). This pull request does not change those bounds.
- An audit of those bounds found no other concrete failure. It checked
index limits, byte limits, `.length` guards, provider limits, and
security `min` checks. It used code search and sampling, so it is not
exhaustive.
- Not verified: Slack `topic` and `purpose` use `.max(250)`, and we did
not confirm how Slack counts that limit.
- `.min(N)` is now stricter for emoji text. A single emoji no longer
passes `.min(2)`.
- `@dependabot rebase` or `@dependabot recreate` on this branch will
remove the manual commits. This happened twice to resolve lockfile
conflicts, and the commits were applied again each time.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), 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 (focused tests only; see
Verification)
- [x] I have added or updated tests where applicable
- [ ] I have updated relevant documentation to reflect my changes (no
documentation change needed)
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green (at `482d76686e`)
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
(5/5 at `95b450048f`)
- [x] I will address all Greptile and reviewer comments before
requesting merge

---

Bumps [zod](https://github.com/colinhacks/zod) from 4.4.3 to 4.6.6.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/colinhacks/zod/releases">zod's
releases</a>.</em></p>
<blockquote>
<h2>v4.6.6</h2>
<blockquote>
<p>Six malicious commits pushed to <code>main</code> on September 21
with a compromised git credential were reverted the same day in <a
href="https://redirect.github.com/colinhacks/zod/issues/6620">#6620</a>
and have since been removed from the history of <code>main</code>. None
were ever published to npm. The npm provenance for 4.6.6 names 284243fb,
the pre-rewrite copy of 9c219833 with identical contents.</p>
</blockquote>
<h2>Commits:</h2>
<ul>
<li>0cd3af5e9f50ad11bbcd4407a46a21b8848ba574 docs: load the en locale
directly in Zod Mini</li>
<li>9d5e60f162997b764b40d45aa02481888db50dea docs: import locales from
zod/locales, not z.locales</li>
<li>34a626f307871ecbb0123947559328dc52ed5e3e docs: tighten the z.locales
tree-shaking note</li>
<li>74304ce5b5796b5edb9a88ceb8e170b6ea3e82df release: stage npm versions
instead of publishing them (<a
href="https://redirect.github.com/colinhacks/zod/issues/6622">#6622</a>)</li>
<li>b2ef983e0711ff6517d6020b021071e2e5653efa docs: add Code for Japan to
bronze sponsors</li>
<li>8fb2faf0584828a6a027d8279db36f6f52784462 docs: crop the Code for
Japan logo and list it first in bronze</li>
<li>bacad518c41610d654658172d325874bf0ee6290 docs: square the Code for
Japan logo to match the other bronze tiles</li>
<li>c42596bd1c87e941d7241d10e012c5c196adcc10 release: hold the run on
the release-approval environment until the staged versions are approved
(<a
href="https://redirect.github.com/colinhacks/zod/issues/6638">#6638</a>)</li>
<li>a2ba4a67986a3e6b2b131be30084a2f460d9ea67 fix(types): declare record
output and input as members, not base type arguments (<a
href="https://redirect.github.com/colinhacks/zod/issues/6645">#6645</a>)</li>
<li>a0ebf970f2d752e5f58527cd2942ff48be4f46bd fix(v4): check array, set
and map size bounds before parsing elements (<a
href="https://redirect.github.com/colinhacks/zod/issues/6652">#6652</a>)</li>
<li>529f78dd9431b07f49647fee3aa0819b3f8b66b7 4.6.6</li>
<li>9c219833c3a3764fa1f1ea9c5ecec8254a63c0b1 ci: stage at verbose so a
failed OIDC token exchange says why</li>
</ul>
<h2>v4.6.5</h2>
<h2>Commits:</h2>
<ul>
<li>d2b135cfb7a3582b9eb515756b9166bcb9521f4a docs: add the 4.6.x patch
highlights to the 4.6 post</li>
<li>f1448f7cee00df9fe1e9ad84a000aa1828cc8bc1 docs: fold the 4.6.x patch
highlights into the 4.6 post's own sections</li>
<li>de65a5cb39ed22a507fac935788f718fa88d104f docs: lead the properties
section with the check and add a Zod Mini tab (<a
href="https://redirect.github.com/colinhacks/zod/issues/6598">#6598</a>)</li>
<li>56222cd1532c07bcb91b67df529cab4c0a215330 feat(instanceof): key the
.properties() shape off the instance type (<a
href="https://redirect.github.com/colinhacks/zod/issues/6600">#6600</a>)</li>
<li>ca0229a404818290e6cdcfefcd7eb2d04bcbb543 Revert &quot;feat: add
z.currencyCode() over a vendored ISO 4217 list, refreshed weekly by CI
(<a
href="https://redirect.github.com/colinhacks/zod/issues/6595">#6595</a>)&quot;</li>
<li>cc4cd4ee9c52fcaa10964e48cc144541e41a5ed9 Revert &quot;Revert
&quot;feat: add z.currencyCode() over a vendored ISO 4217 list,
refreshed weekly by CI (<a
href="https://redirect.github.com/colinhacks/zod/issues/6595">#6595</a>)&quot;&quot;</li>
<li>0f3f5ee3ca56c7574bf849e54f79e9a6e02562ee 4.6.5</li>
<li>59bbc03e10c636b9eb3c393dfeb552819774ec21 chore: re-pin the
integration peers to the workspace zod after the 4.6.5 bump</li>
</ul>
<h2>v4.6.4</h2>
<p>A patch on top of <a
href="https://github.com/colinhacks/zod/releases/tag/v4.6.3">4.6.3</a>.</p>
<ul>
<li><a
href="https://github.com/colinhacks/zod/commit/d6bc1e30"><code>d6bc1e30</code></a>
feat: add <code>z.currencyCode()</code> over a vendored ISO 4217 list,
refreshed weekly by CI (<a
href="https://redirect.github.com/colinhacks/zod/pull/6595">#6595</a>)</li>
<li><a
href="https://github.com/colinhacks/zod/commit/ad32d751"><code>ad32d751</code></a>
perf: <code>z.url()</code> rejects an invalid URL with
<code>URL.canParse()</code> instead of a throwing constructor, about 50x
faster; fewer allocations on the validation path (<a
href="https://redirect.github.com/colinhacks/zod/pull/6588">#6588</a>)</li>
<li><a
href="https://github.com/colinhacks/zod/commit/2bb08717"><code>2bb08717</code></a>
chore: re-pin the integration peers to the workspace zod after the 4.6.4
bump</li>
<li><a
href="https://github.com/colinhacks/zod/commit/f6e1701a"><code>f6e1701a</code></a>
chore(deps): bump next to 15.5.25 and vite to 7.3.6 (<a
href="https://redirect.github.com/colinhacks/zod/pull/6153">#6153</a>)</li>
</ul>
<h2>v4.6.3</h2>
<p>A patch on top of <a
href="https://github.com/colinhacks/zod/releases/tag/v4.6.2">4.6.2</a>.</p>
<ul>
<li><a
href="https://github.com/colinhacks/zod/commit/413cce9a"><code>413cce9a</code></a>
fix(v4): make z.properties() a check again (<a
href="https://redirect.github.com/colinhacks/zod/pull/6594">#6594</a>) —
removes the standalone <code>z.properties()</code> schema from 4.6.0;
<code>z.instanceof().properties()</code> and
<code>.check(...z.properties())</code> are unchanged</li>
<li><a
href="https://github.com/colinhacks/zod/commit/75d63ee1"><code>75d63ee1</code></a>
docs: show only the <code>.properties()</code> method form in the 4.6
post</li>
<li><a
href="https://github.com/colinhacks/zod/commit/46da9572"><code>46da9572</code></a>
docs: match the error-message examples to what the parsers emit</li>
</ul>
<h2>v4.6.2</h2>
<p>A patch on top of <a
href="https://github.com/colinhacks/zod/releases/tag/v4.6.1">4.6.1</a>.</p>
<ul>
<li><a
href="https://github.com/colinhacks/zod/commit/9446b5cc"><code>9446b5cc</code></a>
fix: preserve undefined prefault outputs and object keys (<a
href="https://redirect.github.com/colinhacks/zod/pull/6587">#6587</a>) —
closes <a
href="https://redirect.github.com/colinhacks/zod/issues/6585">#6585</a></li>
<li><a
href="https://github.com/colinhacks/zod/commit/0c483c58"><code>0c483c58</code></a>
docs: the <a href="https://zod.dev/blog/zod-4-6">Zod 4.6 announcement
post</a> (<a
href="https://redirect.github.com/colinhacks/zod/pull/6546">#6546</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/colinhacks/zod/commit/9c219833c3a3764fa1f1ea9c5ecec8254a63c0b1"><code>9c21983</code></a>
ci: stage at verbose so a failed OIDC token exchange says why</li>
<li><a
href="https://github.com/colinhacks/zod/commit/529f78dd9431b07f49647fee3aa0819b3f8b66b7"><code>529f78d</code></a>
4.6.6</li>
<li><a
href="https://github.com/colinhacks/zod/commit/a0ebf970f2d752e5f58527cd2942ff48be4f46bd"><code>a0ebf97</code></a>
fix(v4): check array, set and map size bounds before parsing elements
(<a
href="https://redirect.github.com/colinhacks/zod/issues/6652">#6652</a>)</li>
<li><a
href="https://github.com/colinhacks/zod/commit/a2ba4a67986a3e6b2b131be30084a2f460d9ea67"><code>a2ba4a6</code></a>
fix(types): declare record output and input as members, not base type
argumen...</li>
<li><a
href="https://github.com/colinhacks/zod/commit/c42596bd1c87e941d7241d10e012c5c196adcc10"><code>c42596b</code></a>
release: hold the run on the release-approval environment until the
staged ve...</li>
<li><a
href="https://github.com/colinhacks/zod/commit/bacad518c41610d654658172d325874bf0ee6290"><code>bacad51</code></a>
docs: square the Code for Japan logo to match the other bronze
tiles</li>
<li><a
href="https://github.com/colinhacks/zod/commit/8fb2faf0584828a6a027d8279db36f6f52784462"><code>8fb2faf</code></a>
docs: crop the Code for Japan logo and list it first in bronze</li>
<li><a
href="https://github.com/colinhacks/zod/commit/b2ef983e0711ff6517d6020b021071e2e5653efa"><code>b2ef983</code></a>
docs: add Code for Japan to bronze sponsors</li>
<li><a
href="https://github.com/colinhacks/zod/commit/74304ce5b5796b5edb9a88ceb8e170b6ea3e82df"><code>74304ce</code></a>
release: stage npm versions instead of publishing them (<a
href="https://redirect.github.com/colinhacks/zod/issues/6622">#6622</a>)</li>
<li><a
href="https://github.com/colinhacks/zod/commit/34a626f307871ecbb0123947559328dc52ed5e3e"><code>34a626f</code></a>
docs: tighten the z.locales tree-shaking note</li>
<li>Additional commits viewable in <a
href="https://github.com/colinhacks/zod/compare/v4.4.3...v4.6.6">compare
view</a></li>
</ul>
</details>
<br />

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

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nicky Leach <nicky@paperclip.ing>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 17:41:43 -08:00
Devin FoleyandPaperclip 8f79a32898 fix(connections): keep agent access consistent through setup and permission changes (#19352)
Task-hosted tool setup kept requester-only defaults, and audience edits could leave runtime installations restricted after permission bindings were widened. Large provider catalogs could also fail before creating an access card.

Use the normal agent audience defaults for new tool setup, preserve explicit selections through verified OAuth and draft resumption, and synchronize permission bindings and runtime installations atomically. Enforce agent-edit authority and preserve existing access during additive account reuse.

Validation: 915 focused regression tests, typechecks, and builds passed. All 54 CI checks passed and Greptile Apex scored the reviewed head 5/5 with no unresolved threads. Broad local test failures, recoveries, and incomplete heartbeat coverage are documented in the PR.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-11-05 17:38:32 -08:00
DottaandClaude Opus 5.5 36643d9378 fix(native): retire finished owners whose continued turn rotated authority (#19373)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The native runner runs per-turn agent sessions in managed sandboxes.
A controller keeps a durable session root for each session.
> - When a per-turn owner finishes without a saved runner checkpoint,
the next turn retires that owner. Retirement first checks that the
session root holds only known entries.
> - The runner transport writes two entries that the check does not
know: `authority-epochs/` and `runtime-context.json`. A continued turn
writes `authority-epochs/` when it rotates authority from the turn
before it.
> - So the third turn of an ephemeral-lease task cannot retire the
second turn's owner. The run fails with
`native_continuation_requires_reconciliation` and the task waits for a
user message.
> - This pull request classifies every session-root entry in one
contract module, and adds a test that scans the writers so the list
cannot drift again.
> - The benefit is that follow-up turns continue without manual help,
and any future refusal names the unexpected entry.

## Linked Issues or Issue Description

Refs #18846, Refs #19061, Refs #19355

**What happened?**
On staging, the third turn of every task for one agent (gpt-5.6-sol,
native runner, ephemeral sandbox lease) failed. The error was
`runner_state_identity_mismatch:
prior_owner_terminal_state_indeterminate;
retirement_refused=unexpected_root_entry`. The first two turns
succeeded. The task then went to `blocked` with "Saved work recovered.
Send a message to continue."

**Expected behavior**
The third turn retires the finished second-turn owner and continues the
conversation.

**Steps to reproduce**
1. Assign a task to a native agent that uses an ephemeral sandbox lease.
2. Let the first turn finish. Reopen the task with a comment so a second
turn runs and finishes.
3. Reopen the task again. The third turn fails with the error above.

**Paperclip version or commit**
Staging near 9a79384958; reproduced locally on master 7c0922a7b6.

## What Changed

- Add
`server/src/services/native-runtime/native-session-root-entries.ts`. It
is the single contract for top-level session-root entries:
- Continuity state (moves with a continuity break): the existing
entries, plus `authority-epochs` and `launch-preparation.json`.
- Launch entries (stay in place, carry no authority):
`runtime-context.json`, `diagnostics`.
- Held entries (retirement still refuses): `plugin-runner-state.json`,
`cleanup-activation.json`, `cursor-cloud`.
- `readSettledRunnerdProjection` uses this contract. The refusal now
names the entries. A held entry reports as
`held_root_entry(cursor-cloud)`. An unclassified entry reports as
`unexpected_root_entry(unrecognized-state)`. Names are bounded to three,
limited to safe characters, and have identifiers replaced with `<id>`.
- Add `native-session-root-entries.test.ts`. It scans every local root
writer (the executor, the Cursor Cloud runtime, the provider-pack
authority, the controller migration, the runner transport, and the
launch-preparation store). It reads string and template literals,
same-file constants, `posix.join` and `path.join`, and the harness
persistence directory names. It fails when a writer adds an unclassified
entry, or reaches the root through a new unresolved variable.
- Add three cases to `native-recovered-owner-followup.test.ts`: a
finished owner whose root has `runtime-context.json`, an archived
authority epoch, `diagnostics`, and `launch-preparation.json` is
retired, and the epoch and launch preparation move into the archive; an
unknown entry still holds the owner and is named; a held entry is named
as held.

## Verification

- Red before the fix (same string as staging): `retires a finished owner
whose continued turn left runtime context and an archived authority
epoch` received `runner_state_identity_mismatch:
prior_owner_terminal_state_indeterminate;
retirement_refused=unexpected_root_entry`.
- Green after the fix:
- `cd server && npx vitest run
src/services/native-runtime/native-recovered-owner-followup.test.ts
src/services/native-runtime/native-session-root-entries.test.ts` (34
passed)
- `cd server && npx vitest run
src/__tests__/native-harness-backup-expiration.test.ts
src/services/native-runtime/native-session-resume.test.ts
src/services/native-runtime/native-session-executor.test.ts` (1313
passed)
- Live confirmation on staging is pending. After deploy, run a
three-turn task on an ephemeral-lease codex agent. The third turn must
succeed. Its log must show "finished without a saved runner checkpoint;
starting a new runner".

## Risks

- Retirement now accepts two more entries. `authority-epochs` only holds
epochs that the transport archived after it verified a suspended runner,
so it carries no live owner claim. `runtime-context.json` and
`diagnostics` are launch inputs and logs. Unknown entries still refuse
retirement.
- A continuity break now also moves `authority-epochs` and
`launch-preparation.json` into the archive. This is the intended owner:
these files belong to the retired authority lineage. If they stayed, a
later cold rotation could read an archived epoch of the retired lineage.
- I could not read the staging sandbox. The evidence for the exact entry
is from code: the second turn's transport writes `authority-epochs/`
during authority rotation, and every launch writes
`runtime-context.json`. The refusal now names the entry, so any other
entry will be visible in the next failure.

## 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
- [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-05 19:37:01 -06:00
DottaandClaude Opus 5.5 de1143448b fix(queue): clear interaction receipts a run already delivered (#19376)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents ask people for input through thread cards: questions,
confirmations, plan approvals and connection requests
> - When a person resolves a card, Paperclip queues the response for the
agent. The task chat shows it above the composer as a read-only receipt,
for example "Answered: Questions"
> - A second wake can carry the same receipt. One example is a recovery
continuation that raced the response's own wake. The agent receives the
response from the first run, but the second wake stays deferred
> - The composer then shows a receipt that the agent already has. The
user cannot edit, discard or clear it. The same receipt also blocks the
agent's handback with `interaction_response_queued`
> - This pull request treats a receipt as pending only until a run that
carries the interaction has started
> - The benefit is that an answered card leaves the queue once the agent
has it, for every card kind

## Linked Issues or Issue Description

No public issue exists. A hands-on onboarding test on a staging cloud
workspace (Claude subscription path) found this bug.

**What happened?**
During onboarding, a queued item stayed above the task-chat composer. It
said "Answered: Questions", but the user had already answered the
questions. Later items also stayed after the interview answers and after
the user connected GitHub. The user could not trash, steer or edit them.

Evidence from the staging workspace (interactions, wake diagnostics and
run records for the onboarding task):
- The first questions card (no title, so its label is "Answered:
Questions") was answered at 17:37:48.
- Run `ba3bc7d6` (`issue.interaction.respond`) delivered that answer
from 17:37:48 to 17:38:27.
- A recovery wake (`issue.interaction_continuation_recovery`) for the
same card was requested at 17:37:48 too. It stayed deferred behind
`ba3bc7d6` and then ran again as `cacb095d` (17:38:33 to 17:39:17).
During that time the composer showed the stale "Answered: Questions"
receipt.
- The plan approval had the same pattern from 17:42:06 to 18:20 (fixed
at its source by #19332). The declined Slack connection receipt sat
behind that chain.

**Expected behavior**
A receipt leaves the composer queue as soon as a run has received it.
The agent can hand back the task after it has received the response.

**Steps to reproduce**
1. An agent asks a question card on a task.
2. A user answers it. The response wake starts a run that carries the
answer.
3. A second wake for the same card is deferred behind that run or a
later run.
4. `GET /api/issues/:id/queued-comments` returns the second wake's
receipt as an immutable entry until it runs.

## What Changed

- `server/src/services/queued-interaction-response.ts`:
`readQueuedInteractionResponse` returns no receipt when a run for the
same issue has started with this interaction in its context and is
`running` or `succeeded`. A cancelled or failed run does not count, so
that response is still owed. `hasQueuedInteractionResponse` (the
handback guard) uses the same rule.
- `server/src/routes/issues.ts`: `findQueuedCommentWake` skips a
delivered receipt, so a later undelivered receipt can still show.
- `server/src/__tests__/issue-queued-comments-routes.test.ts`: red/green
tests for every card kind and outcome: answered questions, accepted and
rejected confirmation, accepted plan (checkbox) approval, connected and
declined connection. Each runs with a `succeeded` and with a `running`
delivering run. Controls check that an undelivered receipt still shows,
that a later undelivered receipt shows instead of a delivered one, and
that the handback guard ignores a delivered receipt.
- N-02 (hover shows raw JSON): `master` already shows only the one-line
summary in the label and hover (`queuedMessageLabel`). This PR does not
change the UI.

## Verification

Red, on `master` code with only the new tests added:

```
$ cd server && npx vitest run src/__tests__/issue-queued-comments-routes.test.ts -t "receipt"
     × clears a ask_user_questions answered receipt once a succeeded run already delivered it 1309ms
     × clears a ask_user_questions answered receipt once a running run already delivered it 1041ms
     × clears a request_confirmation accepted receipt once a succeeded run already delivered it 1436ms
     × clears a request_confirmation accepted receipt once a running run already delivered it 829ms
     × clears a request_confirmation rejected receipt once a succeeded run already delivered it 666ms
     × clears a request_confirmation rejected receipt once a running run already delivered it 776ms
     × clears a request_checkbox_confirmation accepted receipt once a succeeded run already delivered it 1179ms
     × clears a request_checkbox_confirmation accepted receipt once a running run already delivered it 4227ms
     × clears a connection_intent accepted receipt once a succeeded run already delivered it 1954ms
     × clears a connection_intent accepted receipt once a running run already delivered it 1634ms
     × clears a connection_intent rejected receipt once a succeeded run already delivered it 1505ms
     × clears a connection_intent rejected receipt once a running run already delivered it 1064ms
     × shows a later undelivered receipt instead of an earlier delivered one 480ms
     × does not hold agent handback for a receipt the agent already received 1272ms
⎯⎯⎯⎯⎯⎯ Failed Tests 14 ⎯⎯⎯⎯⎯⎯⎯
AssertionError: expected [ 'Answered: Questions' ] to deeply equal []
AssertionError: expected [ 'Accepted: Approve hiring 2 agents' ] to deeply equal []
AssertionError: expected [ 'Rejected: Approve hiring 2 agents' ] to deeply equal []
AssertionError: expected [ Array(1) ] to deeply equal []
AssertionError: expected [ 'Connected: GitHub connection' ] to deeply equal []
AssertionError: expected [ 'Declined: Slack connection' ] to deeply equal []
AssertionError: expected [ Array(1) ] to deeply equal [ 'Declined: Slack connection' ]
AssertionError: expected true to be false // Object.is equality
      Tests  14 failed | 11 passed | 85 skipped (110)
```

Green, with the fix:

```
$ cd server && npx vitest run src/__tests__/issue-queued-comments-routes.test.ts -t "receipt"
      Tests  25 passed | 85 skipped (110)
```

Neighbouring suites, with the fix:
- `issue-queued-comments-routes.test.ts` (full file, on current
`master`): 110 passed.
- `question-response-delivery`, `queued-comment-consumption`,
`issue-thread-interaction-routes`, `satisfied-connection-intents`,
`issue-comment-reopen-routes`, `issue-execution-policy-routes` together
with the queued-comments file: 494 tests passed. One run hit an
embedded-Postgres `afterAll` cleanup timeout under 7-file parallel load.
The same file passed alone twice.
- `heartbeat-process-recovery.test.ts -t "plan approval|accepted
interaction|answered question|answered interaction|onboarding first
task"`: 14 passed.
- `pnpm --filter @paperclipai/server typecheck`: passes.

## Risks

- Low risk. The change only hides a receipt that a started run already
carries. An undelivered receipt keeps its current behavior.
- The redundant wake itself is not cancelled here. It still runs later
as a normal continuation, but it no longer holds the composer queue or
blocks handback. #19332 removes the main source of these duplicate
wakes.
- Each receipt read adds one indexed-by-company lookup on
`heartbeat_runs` for the issue.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used in Claude Code
with tool use (shell, file edits, browser inspection of the staging
workspace API).

## 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-05 19:28:45 -06:00
DottaandClaude Opus 5.5 9ceaec6f97 fix(issues): stop showing resource waits as failed attempts (#19369)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The task page shows one interruption banner. The server builds it
from the task's latest run.
> - An auto-retry that waits for a busy workspace, AI connection or
runner update is not a failure. The dispatcher re-checks the resource,
so the run can stay `scheduled_retry` after its retry time.
> - The projection did not know about these waits. After the retry time
passed, it fell through to "Couldn't finish this attempt" with a Retry
button.
> - Retry cannot free the resource. The banner cleared for the 10-second
grace and then came back. Users had no way to fix it.
> - This pull request projects a scheduled resource wait as `working`,
with no Retry.
> - The benefit is that the failed-attempt banner shows only for real
failures.

## Linked Issues or Issue Description

**What happened?**
During onboarding on Paperclip Cloud staging (Claude subscription and
Codex subscription paths), the onboarding task showed "Couldn't finish
this attempt. Your message is saved." with a Retry button. The banner
did not name an attempt. Retry cleared it briefly, then it came back.

**Steps to reproduce**
1. Start a task run while another run holds the same workspace.
2. The run is cancelled with `workspace_busy`, and a `scheduled_retry`
run is created with `scheduledRetryReason: "workspace_busy"`.
3. Keep the workspace busy after `scheduledRetryAt`. On the staging
company, one retry stayed scheduled for 16 minutes past its retry time.
4. Open the task. The failed-attempt banner shows. Click Retry. The
banner comes back.

**Expected behavior**
No failed-attempt banner while the system waits for a shared resource.
The run status already says "Waiting for workspace"
(`execution-projection.ts`).

## What Changed

- `server/src/services/task-interruption-presentation.ts`: a
`scheduled_retry` run whose `scheduledRetryReason` is `workspace_busy`,
`ai_connection_busy`, `ai_connection_pool_wait` or
`runner_revision_wait` (and that has no system recovery) projects as
`state: "working"`, `canRetry: false`. User gates and failed Retry
preparations stay authoritative because they are checked first.
- The batched query now selects `scheduledRetryReason`.
- Tests: a Postgres integration test (`task-interruption-query.test.ts`)
that seeds the staging run chain for each reason, and unit tests for the
guard cases (failure-lane retries, paused agent, failed Retry
preparation).

## Verification

Red on `master` (fix not applied), `cd server && npx vitest run
src/__tests__/task-interruption-query.test.ts -t "overdue"`:

```
     × does not present an overdue workspace_busy wait as a failed attempt 213ms
     × does not present an overdue ai_connection_busy wait as a failed attempt 25ms
     × does not present an overdue ai_connection_pool_wait wait as a failed attempt 13ms
     × does not present an overdue runner_revision_wait wait as a failed attempt 11ms
- Expected
+ Received
-   "canRetry": false,
+   "canRetry": true,
-   "state": "working",
+   "state": "failed",
      Tests  4 failed | 11 skipped (15)
```

Green with the fix, same tests plus the new unit tests:

```
 ✓ task-interruption-query.test.ts > does not present an overdue workspace_busy wait as a failed attempt
 ✓ task-interruption-query.test.ts > does not present an overdue ai_connection_busy wait as a failed attempt
 ✓ task-interruption-query.test.ts > does not present an overdue ai_connection_pool_wait wait as a failed attempt
 ✓ task-interruption-query.test.ts > does not present an overdue runner_revision_wait wait as a failed attempt
 ✓ task-interruption-presentation.test.ts > scheduled resource wait presentation > keeps an overdue workspace wait out of the failed-attempt UI
 ✓ task-interruption-presentation.test.ts > scheduled resource wait presentation > keeps failure retries, user gates and failed Retry preparation authoritative
```

Neighbouring suites: `task-interruption-presentation`,
`task-interruption-query`, `workspace-recovery`, `agent-project-access`,
`heartbeat-dependency-scheduling`: 5 files, 458 tests passed.

Server `tsc --noEmit` reports no errors in changed files. The only
errors locally come from the unbuilt `@paperclipai/paperclip-runner`
vendor package, which this change does not touch. No UI change, so `pnpm
check:token-gates` is not applicable.

Evidence: the staging onboarding task's run history shows
`workspace_busy` cancellations at 17:46, 17:52 and 18:10, each followed
by a `scheduled_retry`. One had `scheduledRetryAt` 17:53:57 and started
at 18:10:20. The next run succeeded at 18:20.

## Risks

- Low risk. Presentation only; dispatch and admission do not change.
- If a resource wait never clears, the task shows no failed-attempt
banner. The run status still shows "Waiting for workspace", and the
existing retry-attempt limits still end the wait.
- Not in scope: genuine failures without a provider or workspace
explanation still show the generic text, and an overdue failure-lane
retry still shows as failed.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 19:08:27 -06:00
DottaandClaude Opus 5.5 7440cfc9b1 fix(issues): accept queued Send while the previous run has not started (#19370)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A task chat queues a message while the agent is busy. The
queued-message bar shows a Send button when no turn is running.
> - Send calls `POST /issues/:id/queued-comments/interrupt` with no
target run.
> - The issue can hold an execution run that has not started yet
(`queued`) or that waits for a retry (`scheduled_retry`). The GET
response then shows a deferred queue with no target run, so the UI shows
Send.
> - The interrupt route refused that click with 409. The UI then showed
"Couldn't send. Message is still queued."
> - This pull request saves the click as interrupt intent in that state.
The queue is delivered when the previous run stops.
> - The benefit is that the button the UI offers now works, and the
message goes next.

## Linked Issues or Issue Description

**What happened?**
Cloud onboarding test, Codex subscription path (N-06): the tester
clicked Send in the onboarding task. The UI showed "Couldn't send.
Message is still queued."

**Expected behavior**
Send is accepted. The message stays in the queue and is delivered next,
when the previous run stops.

**Steps to reproduce**
1. Have an issue whose execution run is `queued` (not started) or
`scheduled_retry`.
2. Post a message. It is deferred behind that run.
3. Click Send on the queued message.
4. The route returns 409: "This queue does not support interruption"
(paperclip_runner) or "The previous run has not stopped" (legacy
adapters).

**Paperclip version or commit**
`master` at b68f69a801.

Root cause: `GET /queued-comments` resolves the target run from running
runs only, so the UI offers Send. `POST /queued-comments/interrupt`
locks `issue.executionRunId` at any status. It accepted only a terminal
run or no run. A queued or retry-waiting run fell between those two
rules.

Relation to other fixes: #19331 (declined connection receipt) changed
which wake the queue projects. It does not touch this check, and current
`master` still fails. The sibling N-07 problem (steering a queued
message on Codex) uses the `/steer` route. This PR does not change that
route. I found no shared cause.

Evidence limits: the tester's notes are the only evidence. The staging
company that I could read uses the Claude subscription path, so I could
not see the Codex run's status at the time of the click. The reproduced
state is the only server path that gives "Couldn't send" for a Send
click with no running turn. A slow-starting or retrying Codex run gives
that state.

## What Changed

- `server/src/routes/issues.ts`: the interrupt route no longer refuses a
Send when the issue's run is `queued` or `scheduled_retry`. For
paperclip_runner queues, a run that is not running is handled like a
stopped run. The legacy "previous run has not stopped" 409 is removed.
The route saves `queuedCommentInterrupt` intent, does not cancel the
not-yet-started run, and returns the queue in the `deferred` state. The
UI already shows "Messages are still queued. They will send when the
task is ready." for that response.
- `server/src/__tests__/issue-queued-comments-routes.test.ts`: new route
test for native and legacy queues, with `queued` and `scheduled_retry`
runs. It checks that GET offers Send, that the POST is accepted without
a run cancel, that the intent is saved, and that the queue is delivered
to one successor run after the previous run stops.

## Verification

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

```
pnpm vitest run server/src/__tests__/issue-queued-comments-routes.test.ts -t "accepts Send while"
     × accepts Send while the previous native run is queued and delivers it once that run stops
     × accepts Send while the previous native run is scheduled_retry and delivers it once that run stops
     × accepts Send while the previous legacy run is queued and delivers it once that run stops
     × accepts Send while the previous legacy run is scheduled_retry and delivers it once that run stops
AssertionError: {"error":"This queue does not support interruption"}: expected 409 to be 200
AssertionError: {"error":"The previous run has not stopped"}: expected 409 to be 200
      Tests  4 failed | 90 skipped (94)
```

Green, after the fix (same command):

```
 Test Files  1 passed (1)
      Tests  4 passed | 90 skipped (94)
```

Neighbouring suites:

- `server/src/__tests__/issue-queued-comments-routes.test.ts` (complete
file): 94/94 passed.
- `server/src/__tests__/agent-conversations.test.ts`,
`server/src/services/issue-queued-comment-queue.test.ts`,
`ui/src/components/task-chat/TaskChatQueuedMessages.test.tsx`: passed.
- When I ran the two embedded-Postgres route files in one Vitest
process, every test passed. Then the queued-comment file's `afterAll`
database cleanup timed out after 30 s, which is a local resource limit.
The file passes when it runs alone.
- `pnpm --filter @paperclipai/server typecheck`: passed.
- I made no UI changes, so I did not run `pnpm check:token-gates`.

## Risks

- Low risk. Send was refused only for `queued` and `scheduled_retry`
runs. Now that click is saved and the run is not cancelled. A running
turn is unchanged: paperclip_runner still steers it, and legacy still
interrupts it.
- When a run waits for a retry, a saved Send waits for that retry to
finish. The route does not skip the retry. This matches how the queue
already works for messages without a click.

## Model Used

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

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [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-05 18:59:16 -06:00
Devin FoleyandClaude Opus 5.5 33269e1fec fix(observability): report directory-merge lock holder diagnostics (#17755)
A directory-merge lock timeout (ERR_WORKSPACE_RESTORE_LOCK_TIMEOUT) already
carries allowlisted holder evidence, but server Sentry capture dropped it.

- Add directoryMergeLockTimeoutDiagnosticsFor in adapter-utils: closed
  labels, booleans and bounded durations; no owner PID or paths.
- server captureException: lock timeouts get an event-local
  directory_merge_lock context and error_code=workspace_restore_lock_timeout
  tag; default grouping and message unchanged.
- run-failure-diagnostics restoreLock* fields use the shared reader
  (same allowlist and order).
- Document the record in doc/workspace-restore-locks.md.

Diagnostics only; does not change lock acquisition, timeouts or retries.

Refs PAPERCLIP-BACKEND-4Q

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 16:48:46 -08:00
Nicky LeachandClaude Opus 5.5 3a8f95d928 fix(ci): let verified Dependabot lockfiles merge through the reporting-lock gate (#19356)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Only trusted environments may change `pnpm-lock.yaml`: the lockfile
refresh workflow and Dependabot. Contributors never commit it.
> - Paid evaluation jobs also install a lock only when the
reporting-lock proof (`tests/runner-e2e/report-lock.json` and `.patch`)
admits it.
> - #19283 added a CI test that checks the committed lock against that
proof. Only the refresh workflow writes the proof, so every Dependabot
lockfile pull request now fails CI. Examples are #17364, #17172, and
#17911.
> - A merged Dependabot lockfile also breaks the refresh workflow.
`prepare-report-lock.mjs --refresh` refuses to renew the proof for a
lock that the proof does not already admit.
> - This pull request lets a verified Dependabot lockfile pass, and it
keeps both lockfile rules strict. It also closes a gap in the
contributor rule.
> - The benefit is that Dependabot security updates can merge again, and
only trusted environments write lock content.

## Linked Issues or Issue Description

Refs #19283, #17364, #17172, #17911, #19303.

**What happened?**
`server/scripts/reporting-lock-policy.test.mjs` fails on each Dependabot
pull request that changes `pnpm-lock.yaml`. The error is "Reporting lock
changed; review the dependency correction before install". A Dependabot
pull request cannot change the proof files, so it can never pass. After
a merge, the lockfile refresh also stops at "Committed report lock is
not reviewed; refusing to refresh its proof".

**Expected behavior**
- Contributors cannot change `pnpm-lock.yaml` or the reporting-lock
proof.
- Dependabot can change `pnpm-lock.yaml`.
- The lockfile refresh workflow can change `pnpm-lock.yaml` and the
proof.
- After a Dependabot lockfile merges, the refresh workflow admits it and
renews the proof.

**Steps to reproduce**
1. Open a Dependabot pull request that changes `pnpm-lock.yaml`, such as
#17364.
2. Let CI run. The `server (9/12)` shard fails on the reporting-lock
policy test.

**Security gap found during this work**
The old rule skipped the lockfile check when `github.head_ref` was
`chore/refresh-lockfile`. A contributor could open a pull request from a
fork branch with that name and commit a lockfile change. The old rule
also exempted any pull request that Dependabot opened, including commits
that other people pushed to its branch.

## What Changed

- Add `.github/scripts/lockfile-trust.mjs`. It decides which writers may
change lock content:
- The refresh workflow: the head is `chore/refresh-lockfile` in this
repository, and `github-actions[bot]` opened the pull request.
- Dependabot: Dependabot opened the pull request from this repository,
and the head `pnpm-lock.yaml` matches a Dependabot commit. That commit
must have `dependabot[bot]` as its author, GitHub (`web-flow`) as its
committer, and a verified signature. The script reads these fields from
the GitHub API.
- The reporting-lock proof files count as lock content. Only the refresh
workflow may change them.
- `pr-trusted.yml` policy job: replace the inline `head_ref` and author
exemption with `lockfile-trust.mjs pr`. The job runs the script copy
from the base commit, so a pull request cannot change the check that
judges it. If the base predates the script, the job uses the earlier
rule.
- `refresh-lockfile.yml`: add `lockfile-trust.mjs merged`. It checks
that the last lockfile change on `master` is a GitHub-signed merge of a
Dependabot pull request, and that the lockfile at the pull request head
matches a verified Dependabot commit. If both checks pass, it gives the
lockfile SHA-256 to `prepare-report-lock.mjs --refresh
--trusted-lock-sha256`.
- `refresh-lockfile.yml` PR step: check `pnpm-lock.yaml` and both proof
files before it exits as unchanged. A refresh that renews only the proof
now opens a pull request.
- `prepare-report-lock.mjs` and its `.d.mts` declaration:
`refreshReportLock` accepts a trusted lock hash. If the trusted lock
already resolves, the proof admits it exactly with an empty patch. If
not, the proof records the resolution as the reviewed correction, as
before.
- `reporting-lock-policy.test.mjs`: skip on Dependabot pull requests.
The policy job verifies those lockfiles. The general test job gets
`PAPERCLIP_CI_PR_AUTHOR` for this purpose. This keeps the test, so it
still merges cleanly with #19303.
- `check-pr-lockfile.mjs` (advisory gate): allow Dependabot lockfile
changes, and flag changes to the proof files.
- `CODEOWNERS`: add the two proof files.
- `doc/RELEASE-AUTOMATION-SETUP.md`: describe the writers, the
`@dependabot recreate` path, and the proof renewal after merge.

## Verification

- `node --test '.github/scripts/tests/*.test.mjs'`: 707 passed. This
includes 17 new tests in `lockfile-trust.test.mjs` and 3 new tests in
`check-pr-lockfile.test.mjs`. The new tests run the `pr` command against
real git histories with a stub `gh`.
- `tests/runner-e2e/prepare-report-lock.test.ts`: 10 passed, including 3
new trusted-lock refresh tests. I ran it with a minimal Vitest config,
because the runner-e2e config builds a provider bundle first.
- `server/scripts/reporting-lock-policy.test.mjs`: passes normally, and
skips with `PAPERCLIP_CI_PR_AUTHOR=dependabot[bot]`.
- Live check of the `pr` mode against #17364 with the real GitHub API:
- With the real author, it reports "pnpm-lock.yaml matches verified
Dependabot commit 83c328c15c…" and `writer=dependabot`.
- With a contributor as the author and `chore/refresh-lockfile` as the
head ref, it fails.
- Live check of the `merged` mode at the real merge commits of #16018,
#16020, and #15361. All three report a trusted lock hash. For #16018 and
#15361, `master` changed after Dependabot's commit, so GitHub's merge
produced different lockfile bytes. An exact-bytes rule would refuse
them. That is why the rule trusts GitHub-signed merges of a verified
Dependabot head.
- Greptile reviewed `513ca2def8` at 4/5 with one P1: a proof-only
refresh was never committed, because the PR step checked only
`pnpm-lock.yaml`. `edbeb1ea0d` fixes it. A new test in
`lockfile-refresh-merge.test.mjs` runs the step's change check on a
proof-only change. The test fails without the fix.
- CI `Typecheck + Release Registry` failed at `edbeb1ea0d`:
`prepare-report-lock.d.mts` did not declare the new options argument
(TS2554 twice in `prepare-report-lock.test.ts`). `9139aaad85` updates
the declaration. A local `tsc -p tests/runner-e2e/tsconfig.json` shows
the 2 errors with the old declaration and none with the new one.
- CI at `9139aaad85` failed in six general test shards. None of them run
code that this pull request changes, and PR CI loads `pr-trusted.yml`
from `master`. The failures:
- `heartbeat-plugin-environment.test.ts`: "expected 'running' to be
'succeeded'"
  - `status-card-evaluation-work.test.ts`: a deadline comparison
  - `isolated-repository-rehome.test.ts`: timed out after 15 seconds
- `chat-channels.integration.test.ts`: GitHub webhook recovery
scheduling
- `company-import-export-e2e.test.ts`: child server start timeout, which
also fails on unrelated branches
- `workspaces-b`: every test passed, but an `EPIPE` error came after
`acpx-engine/execute.test.ts`
  I started a rerun of the failed jobs.
- Superagent found a P1 at `9139aaad85`: a verified signature proves who
committed, not who authored. A collaborator could sign a commit with
their own key and set Dependabot as the author. `4fe180d6c1` also
requires GitHub (`web-flow`) as the committer. New tests cover the
spoofed-author case. #17364, #16018, #16020 and #15361 still verify
against the GitHub API.
- `git merge-file` merges this test change with #19303 without
conflicts.
- Not run: the full repository test suite and typecheck. These changes
touch only CI scripts, workflows, one helper, and tests. GitHub CI
covers full regression.

## Risks

- `pr-trusted.yml` loads from `master`, so the new policy takes effect
only after merge. This pull request's own CI uses the earlier rule.
- A Dependabot pull request that is behind `master` now fails if someone
merges `master` into it and the lockfile changes. Use `@dependabot
recreate`. The policy job error says this.
- Paid evaluation jobs refuse a merged Dependabot lockfile until the
refresh pull request merges. The refresh merges without review when it
adds no new package versions. If the release-age gate holds it, the wait
lasts until a maintainer merges it or the daily refresh runs. During
that time, the reporting-lock policy test also fails on other pull
requests, which is the current behavior for any unrenewed lock.
- The rule trusts GitHub as the signer of Dependabot commits and of
merge commits (`web-flow`). Superagent uses the same trust root.
- People with write access can still push to `chore/refresh-lockfile`.
This was already true. The bot merges only its own head commit
(`--match-head-commit`), and code-owner review covers anything else.
- A future manual proof renewal, such as #19283, now needs the refresh
workflow or an admin merge. A contributor pull request that changes the
proof files fails.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), 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 (focused tests; see
Verification)
- [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-05 16:42:26 -08:00
Nicky LeachandClaude Opus 5.5 e6c74982fe fix(workspaces): report why a workspace provider operation failed (#19362)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A project can store its task workspaces with a workspace storage
provider plugin. Core calls the plugin's `workspaceOperation` method to
prepare, inspect, finalize and remove a workspace.
> - `callWorkspaceProvider` catches every rejection of that call and
throws the same conflict: "Workspace storage operation is incomplete;
retry the same workspace".
> - A provider error, an RPC timeout, a crashed worker and a worker that
never got the call all look the same in the run, the log and the API.
The message tells the user to retry, also when the failure is a fixed
configuration problem that a retry cannot fix.
> - In one diagnosis, three different configuration failures in a row
showed this same message. Finding each cause took trace span counts and
database reads.
> - This pull request lets a provider send a reason from a closed set,
and makes Core tell transport failures apart from provider failures.
> - The benefit is that a failed run says what failed and whether a
retry can help. Provider text and values still never cross into Core.

## Linked Issues or Issue Description

**What happened?**
A workspace provider's `workspaceOperation` failed. The run failed with
"Workspace storage operation is incomplete; retry the same workspace",
`errorCode: setup_failed`, and no reason in the run record or the
`heartbeat execution setup failed` log. Retrying did not help, because
the cause was a provider configuration problem.

**Expected behavior**
Core tells these cases apart: the worker did not receive the call, the
call timed out, the worker stopped during the call, or the provider
reported a failure. For a provider failure, Core shows a bounded reason.
Core tells the user to retry only when a retry can help.

**Steps to reproduce**
1. Install a workspace storage provider plugin whose
`onWorkspaceOperation` throws during `prepare` (for example, because of
missing provider settings).
2. Run an agent task in a project that uses that provider.
3. See the failed run: the error is the generic retry message, and the
run record and logs do not show the cause.

**Paperclip version / commit**
`master` at 5ac05871b5.

**Deployment mode**
Any deployment with a workspace storage provider plugin.

## What Changed

- **SDK reason contract.** New
`packages/plugins/sdk/src/workspace-operation-error.ts`:
`WORKSPACE_OPERATION_FAILURE_REASONS` (11 reasons),
`PluginWorkspaceOperationError`, `workspaceOperationErrorData` and
`readWorkspaceOperationFailureReason`. It uses the same envelope design
as `environment-task-error.ts`. The worker host attaches the envelope
only to `workspaceOperation` errors. It copies only the reason, never
the message.
- **Duck-typed reason.** The worker host also forwards an own
`workspaceFailureReason` property from any error, when the value is in
the closed set. A provider that builds against an older SDK can set the
reason before it can import the class. An inherited or unknown value is
ignored. The value can only select one of Core's fixed messages.
- **Core classification.** New
`server/src/services/workspace-provider-failure.ts`.
`callWorkspaceProvider` maps each rejection to a
`WorkspaceProviderFailureError` (409) with `kind`, `reason` and
`retryable`:
- `worker_unavailable`: no request was sent. The
`PluginWorkerUnavailableError` stays as the cause, so run setup retries
it as `plugin_worker_unavailable` (the existing bounded bootstrap
retry).
- `timeout` and `worker_exited`: the outcome is unknown, so the message
still says to retry the same workspace.
  - `transport`: any other rejection. The message is the old message.
- `provider` with a reason: a fixed message per reason. Only transient
reasons say to retry.
- `provider` without a known reason: "Workspace storage provider
reported a failure; retry the same workspace".
- **Run settlement.** `failRunSetup` records
`resultJson.workspaceProviderFailure` (`kind`, `reason`, `retryable`).
The `heartbeat execution setup failed` log carries the same fields and
the run's `errorCode`. The run keeps `errorCode: setup_failed` (except
for the worker-not-running case above), so the existing setup-failure
retry, recovery and chat paths do not change.
- **Span allowlist.** The host accepts a `plugin.workspace.failure` span
and a `plugin.failure_reason` attribute. The value must be a reason from
the closed set or `unknown`.
- **Docs.** `doc/plugins/PLUGIN_RUNNER_EXECUTION.md` describes the
failure kinds and the reasons. `doc/observability.md` lists the new span
and attribute.

## Verification

- `cd packages/plugins/sdk && npx vitest run`: 21 files, 261 tests pass.
The new `tests/workspace-operation-error.test.ts` sends errors through
the real worker RPC host.
- `cd server && npx vitest run
src/__tests__/workspace-provider-failure.test.ts
src/__tests__/plugin-host-services-span.test.ts
src/services/heartbeat/run-completion.test.ts`: 3 files, 118 tests pass.
These tests cover:
- every transport kind, and that a worker refusal is still found as the
cause;
  - a real worker RPC rejection with a reason, and without a reason;
  - a fixed message and retry decision for every reason;
  - span clamping of `plugin.failure_reason`;
- run setup for a provider reason (no retry, reason in `resultJson` and
in the log) and for a stopped worker (bounded retry).
- `cd server && npx vitest run` for the 4 plugin workspace integration
suites (`plugin-workspaces`, `plugin-fresh-workspaces`,
`project-workspace-provider`, `workspace-delivery-cleanup`): 91 tests
pass. Their lost-reply fixtures still get the old "incomplete" message.
- `npx tsc --noEmit` in `packages/plugins/sdk`: no errors. `npx tsc
--noEmit -p server/tsconfig.json` shows no errors in the changed files.
The remaining errors come from workspace packages that I did not build
locally.
- Not run locally: the full `pnpm -r typecheck`, `pnpm test:run` and
`pnpm build`. CI runs them.

## Risks

- **Behavior change for a stopped worker.** A `workspaceOperation` call
that the worker did not receive now gets `errorCode:
plugin_worker_unavailable` and the bounded bootstrap retry, as other
plugin calls already do. Before, it was a `setup_failed` run with no
retry. No request reached the provider, so the retry is safe.
- **Message changes.** Provider-reported failures and timeouts get new
fixed messages. Code that matches the exact old message only sees it for
`transport` and `remote_incomplete` failures. I did not find a match on
this message outside tests.
- **No new error codes.** Provider failures keep `setup_failed`. Retry,
recovery and chat-retry routing does not change.
- **Duck typing.** Any error with an own `workspaceFailureReason` from
the closed set is forwarded. A provider can only select one of Core's
fixed messages. It cannot send text or values.
- **Observability.** The new span attribute is a closed set and stays
inside the host allowlist. No Telemetry change.

## Model Used

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

## Checklist

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

Related:
[paperclipai/paperclip#19326](https://github.com/paperclipai/paperclip/pull/19326)
added the same envelope design for Runner revision mismatches. I found
no duplicate PR for this change.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 16:28:49 -08:00
f2593b7905 feat: make Coordinator and Agent Chat standard and speed up navigation (#19360)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - People enter that work through the workspace, agent conversations,
and tasks.
> - Coordinator and Agent Chat still require experimental enrollment.
> - Task link prefetches do not warm the compact queries used by the
default task view. Chat also waits for a request when its navigation
list already holds the conversation.
> - This pull request makes Coordinator and Agent Chat standard and
reuses the reads needed for navigation.
> - People can choose Classic workspace in Experimental settings. Cached
work can appear while fresh data loads.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

Workspace selection, Agent Chat availability, and navigation between
Chat, Tasks, and task details.

**Subsystem affected**

Cross-cutting: UI, workspace preferences, instance settings, and the
database migration.

**Current behavior**

People must enable Coordinator and Agent Chat in Experimental settings.
Opening a task can repeat requests because prefetch warms the full
detail and comment queries, while the default view reads compact task
data and a recent comment window. Opening Chat waits for a detail
request even when the current person's conversation list contains it.

**Proposed behavior**

Coordinator is the standard workspace for everyone. Experimental
settings offer Classic workspace, off by default. Old Coordinator
experiment values do not select Classic. Agent Chat is available to
everyone, including instances with an old saved or managed off value.
Task link intent warms the queries used by the mounted view. Chat can
use the current person's company-scoped conversation list during
refresh.

**Reason and benefit**

People can use the standard workspace without setup. Repeat navigation
can show cached content while the server refreshes it.

**Breaking changes**

Existing Coordinator experiment and Agent Chat off values no longer
disable these surfaces. Classic requires an explicit new opt-in. The
workspace API keeps its existing response shape and audit checks. The
migration retains legacy values and adds a separate Classic preference.
Execution permissions, company access, and chat privacy checks still
apply.

**Additional context**

Roadmap check: this graduates existing surfaces and improves their
navigation. It does not add a new agent execution capability. Duplicate
searches found no open Coordinator workspace PR. Related server cache
work, Refs: #17828, addresses compact issue-list invalidation; this PR
addresses client navigation queries.

## What Changed

- Add a default-off Classic workspace preference. Derive the existing
workspace API response from that choice. Keep personal persistence,
account isolation, and audited writes in the workspace preference
service.
- Replace the Coordinator experiment control with Classic workspace. Use
Coordinator while preferences load. Update the existing settings
examples and navigation recipe.
- Retire the Agent Chat experiment control. Ignore old stored and
managed off values. Retain the compatibility key and the existing
execution and access checks.
- Reuse account- and company-scoped chat list data with its original
freshness timestamp.
- Share compact task and recent-comment query options between prefetch
and the task view. Keep full-history prefetch for Classic tasks and
expanded history.
- Add upgrade, replay, cache, account isolation, and browser switching
regressions. Update product, database, development, and roadmap
documentation.

## Verification

- Targeted preference, settings, migration, cache, and UI regression
tests passed during implementation. The cache tests hold refresh
requests pending and check that content still renders. They also check
account isolation and stale refresh behavior.
- A real browser test verified fresh Coordinator defaults, Classic
opt-in persistence, and return to Coordinator. It used an isolated
source checkout with fresh data on macOS.
- Token, migration numbering and safety, map, and Storybook catalog
checks pass after the rebase.
- Repository typecheck and build pass. Typecheck and token gates also
pass after review repairs. The broad local run passed all six chat
integration shards (1,931 tests). It then found the host-CLI-dependent
recovery fixture described below. Its remaining duplicate work was
stopped after the full current-head CI matrix passed; this is not a
claim that the broad local command completed green.
- Review repairs passed 419 UI/migration fixture tests, 19 Coordinator
navigation tests, and all 214 tests in the five affected server suites.
The 24 Chat/workspace browser tests pass, including standard Coordinator
switching and explicit Classic persistence.
- The broader browser repair run passed 85 tests. It retained two failed
assertions and skipped five later serial cases. The affected files then
passed all 11 tests after the fixes: reopen the desktop panel after
reload and open More for Audit and Connectors. This includes the five
serial cases that had not run.
- The broad local suite exposed a recovery fixture that executed the
host Codex CLI during a simulated remote probe. On a host with Codex
0.154.0, model fallback broke its fixed-model assertion. The fake
sandbox now reports Codex 0.160.0. All 20 selected recovery/provider
compatibility cases pass after the repair.
- A CLI company import/export suite timed out during server startup in
the second CI run. The port fallback is intentional in that fixture. No
root cause was established. That suite passes on the final commit in CI.
This records recovery, not a claimed product fix for the earlier
timeout.
- The first CI run exposed remaining assumptions about the retired flags
and Classic UI. The repair commit updates those fixtures and navigation
steps. It also restores the missing enabled Cases destination in
Coordinator. Earlier local recovery attempts used the wrong fixture
notification and advanced fake time during live execution; the corrected
tests use the subscribed recovery queue and wall time during execution.
- Greptile reviewed commit `98972b030d4505d838427a2fca6b196b8cad6d47` at
**5/5** with no new comments. The original finding is resolved.
- Current-head CI is green: **56 successful checks**, with three checks
skipped by workflow filters. This includes typecheck, build, the
complete Vitest matrix, all eight browser shards, Runner verification,
release checks, and security checks. [Final CI
run](https://github.com/paperclipai/paperclip/actions/runs/42132651414).
- Typecheck caught missing `pid` and `startedAt` fields in the simulated
command result. The fixture now returns the complete process contract,
and the server test-layer typecheck passes.
- Storybook catalog alignment was checked. Storybook play tests were not
run. Hosted latency and cold bundle loading were not measured.

## Risks

- The new default changes the workspace for people who previously left
the Coordinator experiment off. They can opt into Classic in
Experimental settings.
- The additive migration must run before the new server code. It is safe
to replay and preserves explicit Classic choices.
- Cached content can be stale during background refresh. Existing query
invalidation and source timestamps still control freshness.
- These changes reduce avoidable loading waits. They do not establish a
measured improvement in hosted request latency or initial bundle
download time.

## Model Used

OpenAI Codex, based on GPT-6, with reasoning, repository tools, code
execution, and browser automation. The exact serving revision 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: Scott Tong <scott@scottsmbpm5max.lan>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-05 16:28:25 -08:00
Devin Foley 1a36195868 fix(storage): name the S3 read when a response body is cut off (#19284)
Record read_object_body with HTTP status, S3 request id and attempts when a GetObject body stream errors after send() resolved, and add the storage fields to run failure execution diagnostics. Diagnostics only; no retry behavior changes.
2026-11-05 16:19:26 -08:00
Devin FoleyandPaperclip 8080620fc9 fix(workspaces): size archive deadlines and preserve resume budgets (#19364)
Give large workspace archives a bounded per-transfer deadline without changing
provider settings. Preserve configured post-upload command limits and the saved
provider budget when resuming an exact retained sandbox.

Verification: 341 focused Core/SDK/adapter tests and 588 Daytona tests passed.
Workspace and provider typecheck/build passed. Hosted CI: 54 successful checks,
two intended Storybook skips. Greptile 5/5 with no unresolved review threads.
The bounded full local run passed all six chat shards and stopped at twenty
minutes during general server suites; that full local run remains incomplete.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-11-05 16:09:32 -08:00
DottaandClaude Opus 5.5 a20dfbc695 fix(recovery): continue a message sent during Stop settlement after recovery (#19355)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A user can stop or interrupt a running native task and then send a
message to give new direction
> - When the stopped run left a workspace hold, the dispatch gate
withholds the follow-up run until recovery saves the work
> - The gate must save the user's message on its receipt so that the
deferred-receipt worker continues it after recovery
> - Two conditions made the gate skip the message instead of saving it,
so the task stayed in progress with "Saved work recovered. Send a
message to continue." although the user already sent one
> - This pull request saves the message in both conditions
> - The benefit is that the message the user already sent continues
once, automatically, after recovery

## Linked Issues or Issue Description

**What happened?**
After a user stopped or interrupted a native task with an isolated
sandbox workspace and sent a message, the follow-up run was cancelled
with `execution_reconciliation_required`. Its error said "Saved work
recovered. Send a message to continue." Nothing re-woke the task. In the
interrupt case, a periodic sweep started a run about four minutes later.
In the case where the follow-up first waited for the stopped run's
workspace save (`workspace_busy`), each later retry parked on the same
resolved recovery action, and the task did not continue.

**Expected behavior**
When the recovery that the follow-up waited on resolves, the message
that the user already sent continues once, automatically. The product
does not ask for a second message.

**Steps to reproduce**
1. Run a native task in a managed sandbox with an isolated workspace.
2. Stop the run (or send a comment with interrupt), then send a message
while the stop and workspace save are still settling.
3. Wait for the workspace recovery to resolve.
4. See that the follow-up run is cancelled with
`execution_reconciliation_required` and that no run continues the
message.

**Paperclip version or commit**
`master` before this change.

**Root cause**
`deferUnstartedUserComment` (run dispatch) refused to save the withheld
message in two cases:
1. It compared the message time with the source run's `finishedAt`. A
native run finishes only after its provider stop and workspace save
settle, which takes seconds. A message written after the Stop request
but before that settlement was treated as input written before the Stop.
The receipt was marked `skipped`.
2. It required the run's own receipt to be user-owned. A workspace-busy
retry or an admission retry carries the user's message under a
system-owned receipt, so the message was dropped with the retry.

## What Changed

- Add `userStopBoundaryAt(run)` in `run-cancellation.ts`. For a run
cancelled by a user Stop or interrupt, it returns the recorded Stop
request time (`cancellation.recordedAt`) when that is earlier than
`finishedAt`. For all other runs it returns `finishedAt`.
- Use this boundary in the dispatch gate's message deferral, in native
explicit-continuation admission, and in the remote-Stop continuation
check, so the three places agree on "message written after the Stop".
- When the withheld run is a system-owned retry, walk a bounded chain
(max 8) of retries that did not reach the provider (`workspace_busy`
with `providerWorkStarted: false`, or a verified pre-dispatch
`execution_reconciliation_required`) to the originating user receipt.
Save the message on that receipt with the user's original wake context.
Mark the retry's system receipt `skipped`.
- Update the "input written before the Stop" test so that the Stop
request, and not only the settlement, comes after the message.

## Verification

- New regressions in `server/src/services/workspace-recovery.test.ts`
(`continues a message ... without a second message, after recovery
already resolved`): a direct follow-up and a workspace-busy retry. Each
runs recovery to completion first, then lets the gate withhold the
follow-up, then runs only the deferred-receipt worker. Each asserts that
exactly one successor run is queued with the user's message and the
user's authority, and that no provider work starts.
- Red before the fix: both tests fail because the receipt is `skipped`
instead of `deferred_issue_execution`. Green after the fix.
- `npx vitest run src/services/workspace-recovery.test.ts` (384 passed).
- `npx vitest run src/services/explicit-native-continuation.test.ts
src/modules/run-dispatch src/services/execution-continuation.test.ts
src/services/run-cancellation.test.ts` (581 passed).
- 19 more heartbeat, retry, queued-comment, and native recovery suites
(1272 passed). One failure, `heartbeat-process-recovery > redacts opaque
environment-bound credentials from Sentry diagnostics: result`, also
fails on unchanged `master` and is not related.

## Risks

- A message written after the Stop request but before the stopped run
settled now continues. The stopping run cannot consume new input after
its Stop request, and admission still removes comments that a run
already delivered (`undeliveredLegacyUserCommentIds`). All admission
gates (holds, stop proofs, approvals, budget) still apply.
- Only user Stops and interrupts move the boundary. Other cancellations
keep `finishedAt`.
- The retry walk is bounded, stays in one company, agent, and issue, and
accepts only retries without provider work. It revives a user receipt
only from `cancelled` or `skipped`, so a second retry cannot save the
same receipt twice.
- Not in scope: a run that fails during native preparation with no user
message (for example `runner_state_identity_mismatch:
prior_owner_terminal_state_indeterminate;
retirement_refused=unexpected_root_entry` on a system wake). After its
recovery resolves, the task still waits for a user message. That is a
separate root cause in the native session executor's prior-owner
retirement proof.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 18:08:24 -06:00
DottaandPaperclip 7c0922a7b6 fix(native): preserve provider backups during slow workspace saves (#19359)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Remote native sessions save provider state so another web server can
continue the same conversation.
> - A provider can suspend while its accepted result is still saving a
large workspace.
> - The run has released its live lease at that point. A new
backup-owner lookup rejects this uncommitted state.
> - This pull request captures the real owner during backend creation
and retains it with the current session authority.
> - The existing database checks still reject a changed boot,
generation, result, or successor before inspection and during
publication.
> - The benefit is that a slow workspace save no longer prevents the
provider backup.

## Linked Issues or Issue Description

**What happened?**

A remote session can fail to save its provider backup with
`native_harness_storage_owner_missing`. The provider has suspended, but
its accepted result is still in `workspace_finalizing`. The close
callback tries to acquire a new owner after the run released its lease.
A later allocation cannot use a backup that was never published.

**Expected behavior**

Save the suspended provider with the authority acquired during
execution. Recheck that authority before provider inspection and before
and after upload. Keep result finalization and provider-work permissions
unchanged.

**Steps to reproduce**

1. Start a remote native session with durable provider storage.
2. Accept its result and release the live lease while its workspace save
remains pending.
3. Close the provider transport. The old owner lookup rejects the save
before provider inspection.

**Paperclip version or commit**

Reproduced on master `bf10bc695abaef6b20e85b287f456b5ce2edb266`.

**Deployment mode**

A web server with object storage and remote managed Runner sessions.

Related: #19225 retains failed-turn ownership through close. #18436
restores an external controller checkpoint before a changed warm process
is replaced. This change fixes late backup-owner acquisition while an
accepted result saves its workspace.

## What Changed

- Capture the provider backup owner when the remote backend still owns
its execution lease.
- Bind that owner to the current session authority. An older close
callback selects the current warm attachment's owner. Revalidate the
captured fence before inspecting or skipping provider state.
- Add a regression for the close callback during workspace finalization.
- Restore encrypted provider files with a captured owner while workspace
finalization remains pending. Reject boot, generation, result, and
successor changes during upload.
- Document the ownership contract.

## Verification

- Observed the callback regression fail on the old code with
`native_harness_storage_owner_missing`. The same test passes with this
fix.
- `cd server && node ../node_modules/vitest/vitest.mjs run --config
vitest.config.ts
src/services/native-runtime/native-harness-backup-storage.integration.test.ts`:
59 passed, using real PostgreSQL and encrypted object write/readback.
- `node scripts/check-module-boundaries.mjs`: passed.
- Full workspace typecheck: passed with the complete production fix.
- Native execution: 1,151 passed. Managed warm lifecycle: 9 passed.
These tests require a lost-owner error during shutdown, including before
an incomplete provider checkpoint is skipped. The final change to the
lifecycle test also requires rejection after lease renewal is lost.
- Full local `pnpm build`: passed on the final head.
- The full local `pnpm test:run` stopped in chat shard 6 on five
existing deadline assertions. Earlier chat shards passed; the PR CI chat
shards passed. This is not a full local-suite pass.
- The first CI server shard 4 attempt failed the unchanged legacy
environment-inheritance fixture at its five-second completion wait. The
same failure reproduced locally on the repair head and on an earlier
checkout without this repair. One unchanged CI retry passed. No fixture
assertion or timeout change is shipped.
- Latest-head CI has passed. Greptile reviewed the final head at 5/5
with no open threads. The separate Paperclip Review is paused. This PR
does not claim staging acceptance.

## Risks

The backup owner now lives with its session authority. A wrong authority
selection must fail rather than publish provider state. Database
publication still checks the original company, task, run, boot,
generation, and accepted result before and after upload. Tests change
the owner during upload and verify that the selected backup stays
intact. No schema, data migration, or new provider-work permission
changes.

## Model Used

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

## 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-05 18:04:47 -06:00
Devin FoleyandClaude Opus 5.5 0f2c01b63a fix(server): retry one Cloud portfolio connection reset (#19033)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - On Cloud instances, `GET /api/cloud/stacks` gets the user's stack
portfolio from the Cloud control plane.
> - The route sends one `fetch` to `/v1/tenant/portfolio`. It has a 10 s
deadline.
> - The Cloud edge can reset a pooled keep-alive connection when the
request is written. The request then fails in a few milliseconds and the
user gets a 502.
> - The diagnostics from #15340 now show this failure mode on staging.
> - This pull request retries the idempotent GET one time for a
pre-response connection reset, inside the same deadline.
> - The benefit is that a transient connection reset no longer shows an
error to the user. Real Cloud failures and deadlines are still reported.

## Linked Issues or Issue Description

Refs #15340 (bounded portfolio diagnostics). Refs #10850 (portfolio
proxy). I searched the open and closed PRs for "portfolio". No other PR
fixes this failure.

**What happened?**

On the hosted staging pool stack, one `GET /api/cloud/stacks` returned
502. The diagnostics were `phase: fetch`, `networkCode: ECONNRESET`,
`upstreamStatus: null`, `elapsedMs: 19`. The Core process had started
about 7 minutes before. CPU and memory were normal.

The Railway HTTP logs for the Cloud harness show no failed or missing
portfolio request at that time. All portfolio requests in a 12-minute
window returned 200 in 15–91 ms. The next Core portfolio fetch, 0.4 s
later, reached Cloud and returned 200. Thus the failed request did not
get to the Cloud application. The connection was reset between Core and
the Cloud edge.

This failure is different from the older stalls on the same stack. Those
failures had `DEADLINE_EXCEEDED` after more than 60 s, during
whole-process freezes. This change does not retry those failures.

**Steps to reproduce**

1. Start an HTTP server that resets (or closes) the socket for the first
request and returns JSON for the second request.
2. Set `PAPERCLIP_CLOUD_API_ORIGIN` to that server and call `GET
/api/cloud/stacks` with the native `fetch`.
3. Before this change, the route returns 502 with `networkCode:
ECONNRESET` (or `UND_ERR_SOCKET`). Native `fetch` does not retry.

**Expected behavior**

The route retries the idempotent GET one time when the connection is
reset before a response. The retry uses the same 10 s signal. Other
failures stay unchanged.

## What Changed

- `server/src/routes/cloud.ts`: If the fetch phase fails with
`ECONNRESET` or `UND_ERR_SOCKET`, the route sends the same GET one more
time. The retry uses the same `AbortSignal.timeout(10_000)`. The total
time stays at or below 10 s.
- The route classifies the first error with the existing bounded
`CloudPortfolioError` diagnostics. Thus a reset after the deadline stays
`DEADLINE_EXCEEDED` and gets no retry.
- The first reset gets a warning log with the same safe fields
(`Paperclip Cloud portfolio connection reset; retrying once`). It does
not send a Sentry event. Thus the logs still show how frequently this
occurs.
- A second reset, a deadline, HTTP error statuses, body failures,
`ECONNREFUSED`, `ENOTFOUND`, connect timeouts and unknown errors behave
as before: one report and no retry. Failures are still not cached.
- `server/src/__tests__/cloud-routes.test.ts`: The old test "classifies
a fetch reset without retrying" now expects one retry. New tests cover a
second reset, the codes that get no retry, a reset after the deadline,
and two real-socket cases.

`doc/observability.md` describes the diagnostic context. Its fields and
semantics do not change. Per `CONTRIBUTING.md`, the change adds no code
comments; the retry rationale is in this description.

## Verification

- Red before the fix: with the old route and the new tests, `pnpm exec
vitest run server/src/__tests__/cloud-routes.test.ts` gave 5 failed, 19
passed. The failures were the two mocked retry cases, the second-reset
case and the two real-socket cases. Thus native `fetch` on Node 25.9
does not retry these resets itself.
- Green after the fix: `pnpm exec vitest run
server/src/__tests__/cloud-routes.test.ts
server/src/__tests__/cloud-portfolio-error.test.ts` gave 31 passed.
- Real dependency: the real-socket tests use `node:http` and the native
global `fetch`. `socket.resetAndDestroy()` gives `ECONNRESET`.
`socket.destroy()` gives `UND_ERR_SOCKET`. Each test makes sure that the
second request reaches the server with the same authorization, the route
returns 200, and no Sentry event occurs.
- The second commit only removes the four-line comment that Greptile
flagged. It has no runtime effect; a syntax check passed locally and CI
reruns the suite.
- Not run locally: `server/src/__tests__/sentry.test.ts` fails to load
in my sandbox on the unchanged base and with this change. The error is
the same: `Failed to resolve entry for package
"@paperclipai/plugin-sdk"`. The workspace packages were not built. The
full `tsc -p server` was stopped by the 8 GB sandbox memory limit. A
typecheck of only the changed files gave the same output on the base and
with this change, apart from a 5-line offset. CI runs the full typecheck
and test suite.
- Remaining limit: the staging evidence shows that the request did not
reach the Cloud application. It does not show if the reset socket was a
reused keep-alive connection or a new connection. The retry is correct
in both cases.

## Risks

- Low risk. The retry applies only to an idempotent GET that has no side
effects on Cloud. It occurs only for a fetch-phase reset before a
response.
- The worst case is one more portfolio request to Cloud for each reset.
The deadline is the same as before.
- If the Cloud edge resets all connections, the user sees the same 502
as before, after two fast attempts.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), through Claude Code, with tool
use (shell, code execution, Sentry, Railway and GitHub connectors).

## 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
- [ ] 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-05 15:39:18 -08:00
Devin FoleyandClaude Opus 5.5 db95539d46 fix(agents): refuse agent purge when cost history references it (#19022)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A board user can permanently delete (purge) a terminated agent
> - The purge deletes the agent's heartbeat runs, but cost and finance
events still point to those runs
> - Postgres then rejects the delete with a foreign key error, and the
API returns a 500
> - Cost and finance events are accounting history. The purge must not
remove them
> - This pull request makes the purge check for this history first and
return a clear 409
> - The benefit is a clear, expected response instead of a 500, and
accounting history stays intact

## Linked Issues or Issue Description

**What happened?**
Permanent agent deletion failed with `Failed query: delete from
"heartbeat_runs" where "heartbeat_runs"."agent_id" = $1`. The cause is
`update or delete on table "heartbeat_runs" violates foreign key
constraint "cost_events_heartbeat_run_id_heartbeat_runs_id_fk" on table
"cost_events"`. The server reported this as an unexpected error. Sentry:
PAPERCLIP-BACKEND-4Z.

**Steps to reproduce**
1. Run an agent so that a heartbeat run records a cost event.
2. Terminate the agent.
3. Permanently delete (purge) the agent.

**Expected behavior**
The purge stops before it deletes anything. It returns a clear conflict,
and the agent stays terminated. This matches the existing
`agent_case_history_retained` behavior.

**Actual behavior**
The transaction fails on the `heartbeat_runs` delete with a raw foreign
key error (500).

Related, older approaches: #9130 and #1987 tried to delete cost and
finance rows during agent removal. This PR keeps the ledger instead.

## What Changed

- `deleteAgentDependencies` now checks for `cost_events` and
`finance_events` rows that reference the agent or one of its heartbeat
runs. It does this before any destructive cleanup.
- If such rows exist, it throws `409 agent_cost_history_retained`.
Termination stays available.
- The activity-log cleanup reuses the same agent-runs subquery.
- New embedded-Postgres regression test
`server/src/services/agent-cost-history-deletion.test.ts`. It covers a
cost event on a run, a cost event attributed to the agent, a finance
event on a run, and a normal purge with no cost history.

## Verification

- Without the fix, 3 of the 4 new tests fail with the exact Sentry
error. Examples: `cost_events_heartbeat_run_id_heartbeat_runs_id_fk`,
`cost_events_agent_id_agents_id_fk`,
`finance_events_heartbeat_run_id_heartbeat_runs_id_fk`.
- With the fix, all 4 pass.
- Related suites pass (8 files, 249 tests): `lifecycle.test.ts`,
`execution-admission/adapters/postgres.test.ts`,
`decision-models.test.ts`, `prompt-injection-prevention.test.ts`,
`fast-responses.test.ts`, `agent-timer-deadlines.test.ts`,
`case-deletion.test.ts`, and the new test.
- `tsc --noEmit` in `server/` reports 0 errors.

## Risks

- Behavior change: agents with recorded cost or finance history can no
longer be purged. Before this change, these purges already failed with a
500, so no purge that used to succeed is now blocked.
- No schema change and no migration.
- Docs follow-up: `doc/SPEC-implementation.md`, `doc/DEVELOPING.md` and
`maps/agent-access-map/agents.md` describe
`agent_case_history_retained`. They do not yet mention the new
`agent_cost_history_retained` code. I can add that in this PR if
reviewers want it.

## Model Used

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

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 15:30:36 -08:00
DottaandClaude Opus 5.5 002455ea06 fix(issues): finish a reassignment whose native stop settles after the request (#19354)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A user can stop or interrupt a running native task and then send a
message to give new direction
> - When the stopped run left a workspace hold, the dispatch gate
withholds the follow-up run until recovery saves the work
> - The gate must save the user's message on its receipt so that the
deferred-receipt worker continues it after recovery
> - Two conditions made the gate skip the message instead of saving it,
so the task stayed in progress with "Saved work recovered. Send a
message to continue." although the user already sent one
> - This pull request saves the message in both conditions
> - The benefit is that the message the user already sent continues
once, automatically, after recovery

## Linked Issues or Issue Description

**What happened?**

After a user stopped or interrupted a native task with an isolated
sandbox workspace and sent a message, the follow-up run was cancelled
with `execution_reconciliation_required`. Its error said "Saved work
recovered. Send a message to continue." Nothing re-woke the task. In the
interrupt case, a periodic sweep started a run about four minutes later.
In the case where the follow-up first waited for the stopped run's
workspace save (`workspace_busy`), each later retry parked on the same
resolved recovery action, and the task did not continue.

**Expected behavior**
When the recovery that the follow-up waited on resolves, the message
that the user already sent continues once, automatically. The product
does not ask for a second message.

**Steps to reproduce**

1. Run a native task in a managed sandbox with an isolated workspace.
2. Stop the run (or send a comment with interrupt), then send a message
while the stop and workspace save are still settling.
3. Wait for the workspace recovery to resolve.
4. See that the follow-up run is cancelled with
`execution_reconciliation_required` and that no run continues the
message.

**Paperclip version or commit**
`master` before this change.

**Root cause**
`deferUnstartedUserComment` (run dispatch) refused to save the withheld
message in two cases:
1. It compared the message time with the source run's `finishedAt`. A
native run finishes only after its provider stop and workspace save
settle, which takes seconds. A message written after the Stop request
but before that settlement was treated as input written before the Stop.
The receipt was marked `skipped`.
2. It required the run's own receipt to be user-owned. A workspace-busy
retry or an admission retry carries the user's message under a
system-owned receipt, so the message was dropped with the retry.

## What Changed

- Add `userStopBoundaryAt(run)` in `run-cancellation.ts`. For a run
cancelled by a user Stop or interrupt, it returns the recorded Stop
request time (`cancellation.recordedAt`) when that is earlier than
`finishedAt`. For all other runs it returns `finishedAt`.
- Use this boundary in the dispatch gate's message deferral, in native
explicit-continuation admission, and in the remote-Stop continuation
check, so the three places agree on "message written after the Stop".
- When the withheld run is a system-owned retry, walk a bounded chain
(max 8) of retries that did not reach the provider (`workspace_busy`
with `providerWorkStarted: false`, or a verified pre-dispatch
`execution_reconciliation_required`) to the originating user receipt.
Save the message on that receipt with the user's original wake context.
Mark the retry's system receipt `skipped`.
- Update the "input written before the Stop" test so that the Stop
request, and not only the settlement, comes after the message.

## Verification

- New regressions in `server/src/services/workspace-recovery.test.ts`
(`continues a message ... without a second message, after recovery
already resolved`): a direct follow-up and a workspace-busy retry. Each
runs recovery to completion first, then lets the gate withhold the
follow-up, then runs only the deferred-receipt worker. Each asserts that
exactly one successor run is queued with the user's message and the
user's authority, and that no provider work starts.
- Red before the fix: both tests fail because the receipt is `skipped`
instead of `deferred_issue_execution`. Green after the fix.
- `npx vitest run src/services/workspace-recovery.test.ts` (384 passed).
- `npx vitest run src/services/explicit-native-continuation.test.ts
src/modules/run-dispatch src/services/execution-continuation.test.ts
src/services/run-cancellation.test.ts` (581 passed).
- 19 more heartbeat, retry, queued-comment, and native recovery suites
(1272 passed). One failure, `heartbeat-process-recovery > redacts opaque
environment-bound credentials from Sentry diagnostics: result`, also
fails on unchanged `master` and is not related.

## Risks

- A message written after the Stop request but before the stopped run
settled now continues. The stopping run cannot consume new input after
its Stop request, and admission still removes comments that a run
already delivered (`undeliveredLegacyUserCommentIds`). All admission
gates (holds, stop proofs, approvals, budget) still apply.
- Only user Stops and interrupts move the boundary. Other cancellations
keep `finishedAt`.
- The retry walk is bounded, stays in one company, agent, and issue, and
accepts only retries without provider work. It revives a user receipt
only from `cancelled` or `skipped`, so a second retry cannot save the
same receipt twice.
- Not in scope: a run that fails during native preparation with no user
message (for example `runner_state_identity_mismatch:
prior_owner_terminal_state_indeterminate;
retirement_refused=unexpected_root_entry` on a system wake). After its
recovery resolves, the task still waits for a user message. That is a
separate root cause in the native session executor's prior-owner
retirement proof.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 17:02:39 -06:00
Nicky LeachandClaude Opus 5.5 f9d366acdf fix(runner): require a managed AI connection for plugin Runner-task environments (#19341)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A plugin can provide Runner execution through an environment driver
that declares `supportsTasks: true`. Agent computers use this path.
> - Core let an agent without a managed AI connection run on that path.
The run then used the account that was installed on the plugin's
execution host.
> - That is not a supported credential source. When the host Runner did
not match, the run failed three times, 30 seconds apart, and then
blocked the task with a generic error. The error did not name the fix.
> - The plugin cannot refuse these runs itself. `describe` has no
connection data, and Core replaces plugin error text before the user
sees it.
> - This pull request makes a managed AI connection a requirement for
every plugin Runner-task environment. Core refuses a run without one
before it acquires a lease or calls the plugin.
> - The benefit is one clear notice that tells the operator to choose an
AI connection, and no wasted retries or plugin work.

## Linked Issues or Issue Description

Refs #19326. That pull request makes a Runner revision mismatch retry.
It noted that runs without a managed AI connection reach the Runner
check on a different path, which still blocks after 3 attempts. This
pull request removes that path for plugin Runner-task environments.

**Problem or motivation**
A `paperclip_runner` agent with no managed AI connection can run in a
plugin environment that provides Runner tasks. The run uses the account
installed on the execution host. If the host Runner does not match, the
native session retries 3 times and then blocks the task with
"Plugin-provided Runner execution operation unavailable". The operator
cannot tell what to fix.

**Proposed solution**
Refuse these runs in Core before any lease, plugin call, secret
resolution, or provider work. Use one stable reason,
`ai_connection_required`, for run admission, the setup probe, and agent
verification. Show a task notice that names the fix.

**Alternatives considered**
A refusal in the plugin. It cannot see the connection at `describe`,
which runs first. Its error text is also replaced twice before the user
sees it. A new structured plugin error code would still not cover
`describe`.

## What Changed

- Add `server/src/services/plugin-runner-ai-connection.ts`. It holds the
`ai_connection_required` code and message, the probe result, and the
manifest lookup for a Runner-task driver. The lookup moves here from
`agent-runner-selection.ts`, so run admission and runner selection use
the same rule.
- `heartbeat/execution-configuration.ts`: refuse a new
`paperclip_runner` run in a Runner-task environment when it has no
managed AI connection. This also applies when `homeMode:
"preconfigured"` drops the connection. The run fails as
`configuration_incomplete` with reason `ai_connection_required` and an
action URL to the agent's runtime settings. The check runs before
observability, secrets, the managed runtime, the workspace, and the
lease. A resumed native run keeps its admitted contract.
- `recovery/stranded-notice.ts`: add an "AI connection required" task
notice.
- `run-failure-report.ts`: treat this pre-dispatch refusal as an owner
action, not an unexpected failure.
- `adapters/registry.ts`: the unmanaged probe for a Runner-task target
now returns a failed `ai_connection_required` check. It calls no task
operation. Managed probes still go through `testManagedEnvironment`.
- `agent-harness-verification.ts` and
`agent-lifecycle/application/failure.ts`: classify the check as
`ai_connection_required` with a fixed message. Verification waits for
the configuration to change instead of retrying.
- Docs: `PLUGIN_RUNNER_EXECUTION.md` states the requirement.
`PLUGIN_ENVIRONMENT_TASKS.md` no longer mentions installed accounts.
`agent-config-ui.md` notes the requirement.
- Tests: the "plugin-owned account" heartbeat test described the
behavior that this change removes. It now checks the assembled heartbeat
refusal: no plugin call, no native boundary, no adapter, no workspace
files.

## Verification

- `npx vitest run` in `server/` for these files. All tests passed:
- `src/__tests__/heartbeat-execution-configuration.test.ts`. New cases:
refused without a connection, refused for a preconfigured home, managed
passes, resumed native input passes, non-task plugin driver passes. The
refused cases also check that observability, secrets, and the managed
runtime did not start.
- `src/__tests__/heartbeat-plugin-environment.test.ts`. One test,
"inherits the instance default environment across companies…", also
fails on unmodified `master` in my environment. This change does not
touch it, because its driver does not declare `supportsTasks`.
- `src/adapters/registry.test.ts`,
`src/__tests__/agent-harness-verification.test.ts`,
`src/modules/agent-lifecycle/__tests__/diagnostics.test.ts`,
`src/services/recovery/stranded-notice.test.ts`, and
`src/services/__tests__/run-failure-report.test.ts`.
- 17 more suites that use plugin Runner-task environments, with 705
tests: `plugin-runner-task.integration`,
`plugin-warm-tasks.integration`, `task-creation-recovery.integration`,
`native-startup-cleanup-recovery`,
`environment-task-authority.integration`,
`plugin-task-credential-writeback`, agent-lifecycle `lifecycle`,
`setup`, and `environment-plugins`, `workspace-preparation`,
`agent-runner-selection`, `agent-harness-verification-provider-api`,
`agent-harness-verification-remote`, `plugin-environment-driver-seam`,
`environment-task-runtime`, `agent-test-environment-routes`, and
`adapter-registry`.
- `pnpm typecheck` in `server/` reported no errors in the changed files.
Its only errors came from sandbox-provider packages that my local
install did not link.
- Not run: `plugin-runner-probe.test.ts`, because it needs a Cargo
build. This change does not modify `probePluginRunnerEnvironment`. Also
not run: the full `pnpm test:run` and `pnpm build`. CI runs them.

## Risks

- Behavior change: an agent in a plugin Runner-task environment with no
managed AI connection now stops before dispatch. Before, it ran with the
host's installed account. The agent's owner must choose an AI
connection. This includes agents with `homeMode: "preconfigured"` in
these environments.
- Runs that were already admitted and resume from saved native input are
not refused. They keep the earlier behavior until they finish.
- The check reads the plugin manifest on each new run admission for
plugin environments. This is the same lookup that runner selection
already does.
- This pull request and #19326 both edit `stranded-notice.ts` and
`failure.ts`. The changes are additive, but whichever merges second
needs a small rebase.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, in Claude Code with
tool use (file edits, shell, test runs). Default context window,
extended thinking 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-05 14:59:40 -08:00
DottaandClaude Opus 5.5 bf10bc695a feat(task): show the agent's context window in task properties (#19273)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents run through a harness (Codex app-server, or Claude through
ACPX), and the runner turns harness events into Paperclip events
> - Both harnesses report how full the model's context window is. Codex
sends `modelContextWindow` with its token usage. Claude sends `used` and
`size` in ACP `usage_update`
> - Paperclip dropped both values. The task view could not show how
close an agent is to compaction, and a compaction left no visible mark
in the task chat
> - This pull request carries the context-window occupancy through the
runner, stores it with the existing usage events, and shows it as a
"Context" row in the task Properties panel
> - It also shows a "Context compacted · 230k → 38k tokens" divider in
the task chat when the agent compacts its context
> - The benefit is that operators can see when a long task is near its
context limit and when the agent lost detail to a compaction

## Linked Issues or Issue Description

Refs #19252 (same pattern: a canonical harness event turned into
task-chat UI). Refs #19267 (status notices; it turns Claude's compaction
banner into a notice, and this PR reads that notice too).

**What existing behavior does this improve?**
The task view shows no context-window information, and a compaction is
invisible in the task chat.

**Subsystem affected**
Runner usage normalization (Rust and TypeScript transport), the PRP
usage schema, a read-only issue API, the task Properties panel and the
task-chat transcript.

**Current behavior**
Codex `modelContextWindow` and ACP `used`/`size` are dropped in the
runner. `context.compacted` always has null sizes, Claude compactions
emit no `context.compacted` event at all, and the live runner turn hides
every marker except "interrupted".

**Proposed behavior**
`usage.reported` carries an optional `context {usedTokens,
windowTokens}`. The task Properties panel shows a Context row ("10% of
258k") with a hover breakdown (tokens used, input and cached input,
output, cost, compactions). The transcript shows a compaction divider,
with sizes from the usage before and after the compaction.

**Reason and benefit**
Operators can see when a long task is close to its context limit, and
they can see where the agent's earlier context was summarized.

**Breaking changes**
None. The new schema field is optional, and the new endpoint is
read-only.

## What Changed

- PRP `usage.schema.json`: add an optional `context {usedTokens,
windowTokens}` object. Regenerate the schema bundle and the protocol
manifest.
- Rust `provider_events.rs`: add `codex_context_usage` (last request
`totalTokens` and `modelContextWindow`) and `acpx_context_usage` (ACP
`used`/`size`). ACPX `usage_update` now sets `context`.
- Rust `provider_backend.rs`: Codex `thread/tokenUsage/updated` sets
`context` on live (non-historical) usage.
- Rust `provider_events.rs`: emit `context.compacted` for the completed
`context_compaction` provider notice that #19267 produces from Claude's
compaction banner. Only this path marks a Claude compaction, so the
server counts each compaction once.
- `runnerd-codex-transport.ts`: `rehydrateRunnerdUsageNotification`
keeps a valid `context` on `tokenUsage`, so it reaches the stored
`item.completed` usage item.
- Server: new `GET /api/issues/:id/context-usage` (issue read access is
required) and `issueContextUsage` service. It reads only the assignee's
current session (the session of its newest usage report), and returns
that session's latest context reading, non-zero totals, cost, and the
count of compactions across all of the session's runs. Added to OpenAPI.
Shared type `IssueContextUsage`.
- DB: migration 0432 adds two partial indexes on `heartbeat_run_events`,
built concurrently. One indexes usage items by run and time. The other
indexes `context.compacted` events by company and session. `EXPLAIN`
shows that both lookups use them.
- UI: new `ContextUsageMeter` (bar, percentage, hover card) and
`IssueContextUsageRow` in the Execution section of Properties. The row
refreshes every 8 s while a run is active, and once more when the run
ends.
- UI transcript: `native-run-events.ts` maps `context.compacted` to a
`context` activity entry with pre/post sizes from neighbouring usage
items. `transcript-adapter.ts` turns it into a new `context_compacted`
marker variant. The live runner turn keeps that marker as its own
timeline row.
- Storybook: `task-context-window.stories.tsx` (Properties panel in a
task, all states, hover breakdowns, divider).

## Verification

- `cargo test -p paperclip-runner-core --lib provider_events` (new
tests: ACPX context, Codex context, banner compaction, notice
compaction)
- `pnpm --filter @paperclipai/paperclip-runner exec vitest run
src/live/runnerd-codex-transport.test.ts`
- `cd ui && npx vitest run src/components/ContextUsageMeter.test.tsx
src/components/transcript src/components/task-chat
src/components/issue-properties`
- `cd server && npx vitest run
src/__tests__/issue-context-usage.test.ts` (embedded Postgres)
- `node scripts/typecheck-low-memory.mjs`, `pnpm check:token-gates`,
`pnpm --filter @paperclipai/paperclip-runner run check:protocol-types`
- Live check on a local worktree instance through the runnerd path:
- A Codex `gpt-5.5` task showed "10% of 258k" (27k of 258k, input 77k
with 49k cached, output 1.1k).
  - A Claude Sonnet 4.6 task showed "17% of 200k" (34k of 200k).
- Storybook: Tasks and conversations / … / Task chat / Context window.
- Not verified live: a real compaction. The divider is covered by unit
tests through the same projector path the task chat uses.

## Risks

- The schema field is optional, and older runners omit it. The UI
renders nothing until a harness reports a window.
- Migration 0432 builds two partial indexes concurrently on
`heartbeat_run_events`, which is a large table. The indexes cover only
usage items and compaction events, so they stay small. They add a little
write overhead to those events.
- Claude in-progress usage rows still record zero tokens with `requests:
1`. The final turn usage is correct. This PR does not change accounting.
- A "Compact now" action is out of scope. It needs a compaction-only run
that does not produce a run result. It will come in a follow-up.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code with tool use (shell, file edits, in-app browser for Storybook and
the live instance).

## 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-05 16:54:27 -06:00
Nicky LeachandClaude Opus 5.5 a6ab6a301c fix(server): retry a run when its remote Runner is out of date (#19326)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - An agent can run on a remote Runner that an environment plugin
provides
> - A release can update the app before the remote Runner, so for a
short time the app sees an older Runner
> - In that window the plugin can refuse the task, but Core cannot tell
this refusal from other errors
> - Today the run fails as `setup_failed` with no retry, and the task
goes to board recovery. Before #19246, it was also reported as a project
authentication conflict
> - Agent verification also counts the refusal as a failed harness test,
and it parks the agent after three tries
> - This pull request gives the plugin a stable error code for a Runner
mismatch, and Core waits and retries when it gets that code
> - The benefit is that a release no longer blocks tasks or parks
agents, and a real authentication conflict keeps its own clear notice

## Linked Issues or Issue Description

Refs #19246 (it stopped reporting every precheck error as a project
authentication conflict)
Refs #19249 (the same bounded bootstrap retry for a stopped plugin
worker)
Refs #19302 (shares `environment-task-error.ts`; its diagnostic envelope
is unchanged)
Refs #19172 (related open PR: it reports an undescribable plugin Runner
as a retryable check in the same precheck)
Refs #18667 (a real `ai_connection_incompatible` conflict, which now
gets its own notice)

**What happened?**
A run started while its remote Runner was older than the app. The
environment plugin refused the task because the installed Runner did not
match the revision it expects. Core recorded a generic setup failure,
scheduled no retry, and blocked the task. Before #19246, Core reported
this as a project authentication conflict. An agent in `verifying`
counted the same refusal as a failed harness test. After three tries it
stopped verification until a person chose Retry.

**Expected behavior**
Core treats a Runner mismatch as temporary. A run retries without
blocking its task. A `verifying` agent checks again until the Runner
matches. Only a real `ai_connection_incompatible` result blocks the
task, with a notice that names the conflict.

**Steps to reproduce**
1. Use an agent on a plugin environment whose installed remote Runner
revision differs from the revision the plugin expects.
2. Wake the agent on a task, or put a new agent through verification.
3. The run fails with `setup_failed` and the task is blocked.
Verification parks after three tries.

**Paperclip version or commit**
master at 377e98f1a2.

**Deployment mode**
Authenticated deployment with a plugin environment that provides a
remote Runner.

## What Changed

- Plugin SDK: add `PluginEnvironmentTaskError` and
`ENVIRONMENT_TASK_ERROR_CODES` with one code,
`runner_revision_mismatch`. The worker RPC host sends it as structured
`environmentTask` error data with the schema
`paperclip/environment-task-error-code/v1`
(`environmentTaskErrorCodeData`, read with
`readEnvironmentTaskErrorCode`). This uses the same envelope method as
the other environment RPC errors. Core reads only the code, never the
message. The source refusal diagnostic from #19302 keeps its own schema
and API unchanged, and it takes precedence on a source `submit`.
- `executeEnvironmentTask` still replaces each worker error with a
generic error. After the #19302 diagnostic check, it now keeps only that
closed code.
- Run setup: a Runner mismatch before provider dispatch gets the error
code `runner_revision_mismatch`. Setup schedules a bounded retry with
its own reason, `runner_revision_wait`, and budget (10 attempts, 60 s
apart). The reason is in the same resource-wait lane as workspace and AI
connection waits, so a wait does not spend the failure retry budget. The
guards are the same as for `plugin_worker_unavailable`. Recovery and
legacy reconciliation treat the code like the other bootstrap-only
codes. `scheduleBoundedRetryForRun` uses the Runner wait reason and
budget for a mismatch run when a caller gives no reason, and the
recovery sweeper reports exhaustion from that budget
(`failedRunRetryBudgetSpent`). An answered interaction that hits a
mismatch also waits for the Runner, and its successor keeps the resolved
interaction continuation identity.
- Move the managed project auth classification from `heartbeat.ts` to
`classifyManagedProjectAuthFailure` in `run-preparation.ts`. Only a
found `ai_connection_incompatible` conflict becomes a configuration
failure. All other errors keep their own code.
- Recovery notices: `ai_connection_incompatible` now has its own notice
instead of the "secret/env bindings" text. An exhausted Runner wait gets
the notice "Remote Runner is out of date".
- Agent verification: the Runner probe and the managed project auth
precheck report a mismatch as a retryable check.
`assertHarnessTestPassed` maps it to the new lifecycle code
`runner_revision_mismatch`. That code never counts toward the
three-failure park. The agent checks again every 60 s and becomes ready
when the Runner matches.
- Document the error code in `doc/plugins/PLUGIN_ENVIRONMENT_TASKS.md`.

## Verification

- `cd packages/plugins/sdk && npx vitest run
tests/environment-task-error-code.test.ts
tests/environment-task-error.test.ts tests/worker-rpc-host.test.ts`: the
code crosses the real `environmentTask` RPC. A plain error with the same
message gets no code. Other methods get no envelope. The #19302
diagnostic tests still pass.
- `cd server && npx vitest run
src/services/heartbeat/run-completion.test.ts`: a fresh or wrapped
mismatch gets `runner_revision_mismatch` and the runner retry budget,
with no configuration failure. A started, untrusted (message only) or
native run keeps `setup_failed` with no retry. A real
`ai_connection_incompatible` stays `configuration_incomplete` with no
retry. Other errors, such as an unreachable execution host, are not
reported as an authentication conflict.
- `npx vitest run src/services/execution-recovery-attempt.test.ts`:
Runner waits do not change `failureRetries`, and earlier failures do not
shorten the wait.
- `npx vitest run src/services/heartbeat/retries.test.ts
src/modules/run-dispatch` (embedded Postgres): a recovery caller without
options gets the Runner wait budget and its exhaustion. An answered
interaction waits for the Runner, keeps its continuation identity and
does not spend failure retries.
- `npx vitest run src/__tests__/environment-task-runtime.test.ts`:
sanitization keeps only the code. No worker text or cause survives.
- `npx vitest run
src/modules/agent-lifecycle/__tests__/verification-retry.test.ts`
(embedded Postgres): a `verifying` agent keeps checking after five
mismatches. It never gets `retryRequired`. It becomes ready when the
next check passes.
- `npx vitest run
src/services/native-runtime/plugin-runner-probe-runner-mismatch.test.ts
src/__tests__/agent-harness-verification-remote.test.ts
src/services/recovery/stranded-notice.test.ts
src/services/recovery/service.pause-durability.test.ts
src/services/legacy-execution-recovery.test.ts`.
- `npx tsc --noEmit` passes for `packages/plugins/sdk` and `server`.
- One test in `src/__tests__/heartbeat-process-recovery.test.ts`
("redacts opaque environment-bound credentials from Sentry diagnostics")
fails on my machine. It fails the same way on the base commit without
this change.
- I did not run the full `pnpm test:run`, `pnpm -r typecheck` or `pnpm
build`. CI covers them.

## Risks

- Low to medium. The change adds one new setup failure code and one
lifecycle code. Other failures keep their current behavior.
- A Runner mismatch must come from a plugin that throws
`PluginEnvironmentTaskError`. Until an environment plugin does that,
behavior is the same as today.
- The run retry budget is 10 minutes. If a Runner update takes longer,
the task goes to board recovery with the Runner notice.
- A remote Runner run without a managed AI connection reaches the Runner
check only after native dispatch. That configuration is not supported,
so this change does not handle it. Such a run keeps the native session
retry (3 attempts, 30 s apart).
- #19172 edits the same precheck call in `agent-environment-test.ts`.
Whichever PR merges second needs a small rebase. Keep the mismatch check
before its generic retryable check.

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

## Model Used

- Claude Opus 5.5 (Anthropic), model ID `claude-opus-5-5`, used through
Claude Code with tool use (code search, shell and test runs). Reasoning
mode: standard agentic coding.

## 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-05 14:34:23 -08:00
DottaandClaude Opus 5.5 4ece2107b8 fix(connections): offer aggregator Slack sign-in on Cloud instead of a customer OAuth app (#19328)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Connectors let people give agents tools, such as Slack, from the
Connectors catalog or from an agent's inline "Connect" card
> - On Paperclip Cloud, the only Slack path is "Slack needs its own
OAuth app". The user must create a Slack app, paste a callback URL, and
enter a Client ID and secret
> - Managed Slack chat setup exists, but it is behind the experimental
Chat connectors setting, which is off on Cloud. Composio's and Arcade's
managed Slack sign-in exists, but the native Slack row hides it
> - In an onboarding test on Cloud, a tester got this card from the
onboarding agent, spent 7 minutes on it, and declined it. No Slack
connection was made
> - This pull request follows the existing Google Workspace pattern: on
Cloud, a native connector whose every setup path needs a
customer-created OAuth app yields new setup to the aggregators' managed
sign-in, in the catalog and in agent connection discovery
> - The benefit is that Cloud users can connect Slack with a sign-in
instead of creating a Slack app

## Linked Issues or Issue Description

**What happened?**
On a Paperclip Cloud instance, the onboarding agent asked the user to
connect Slack. Connectors > Slack > Connect showed "Slack needs its own
OAuth app. Slack does not let Paperclip register itself automatically,
so this connector needs an OAuth app you create." The form asks for a
callback URL, a Client ID and a Client secret. It has no link to Slack
app settings. The method description said "Use the provider-hosted
connection for the quickest setup." The tester gave up. The original
notes said that the cloud connector asked for an HTTPS server. That is
not correct: no HTTPS or deployment-mode check is in this path. The real
blocker is the customer OAuth app requirement.

**Expected behavior**
Slack can be connected from the supported Cloud flow without setup steps
that only an operator can do.

**Steps to reproduce**
1. On a Cloud instance with Chat connectors off (the Cloud default),
open Connectors and search for "Slack".
2. Click Connect on Slack. The "needs its own OAuth app" form opens.
3. Or, let an agent call `connections_search("Slack")`. It returns
native `slack` first, and its card opens the same form.

**Additional context**
Evidence: the Slack gallery entry on the Cloud instance had
`ownershipAvailability` `platform_shared: false`, `platform_provisioned:
false`, `customer: true`. `GET
/api/companies/:id/chat-slack/setup-options` returned 403 "Chat
connectors are disabled".

## What Changed

- `packages/shared`: add `nativeConnectorRequiresCustomerOAuthApp(app,
settings)`. It is true when every available setup path of a native
connector needs a customer-created OAuth app: no Paperclip-managed
client, no dynamic client registration, and no enabled chat setup.
- `ui/src/pages/apps/Browse.tsx`: on Cloud, such a connector yields to
aggregators, the same as the built-in Google connectors. The catalog
shows the "Connect through Composio or Arcade" row, and its Connect
button opens the provider choice. A saved native connection, a chat
endpoint, Chat connectors on, a self-hosted instance, or no aggregator
route keeps the native row.
- `server/src/services/connection-intents.ts` and new
`connection-native-yield.ts`: agent `connections_search` uses the same
rule. On Cloud, it does not prefer native Slack for new setup, so the
agent asks the provider-choice question (Composio, Arcade or Zapier).
The rule applies the advertised Paperclip Cloud connector profiles, so
apps with a managed Cloud client (for example Asana) do not yield. If
the profiles cannot be read, the native connector stays.
- `slack.json`: correct the Slack tool method copy, which said "quickest
setup". Add a "register" console link to `https://api.slack.com/apps`,
so the form now shows "Open Slack app settings".
- Tests in all three layers.

## Verification

Red/green. I committed the tests first (`2386fd83df`) and ran them
against unchanged source code.

**Red (before the fix):**

```
server › aggregator connection fallback › Slack on Paperclip Cloud
× offers aggregator Slack routes while native Slack only supports a customer-created OAuth app
AssertionError: expected [ 'slack', 'google-calendar', …(7) ] to deeply equal [ 'via:composio:slack', …(2) ]
+   "slack", "google-calendar", "google-docs", ...
Tests  1 failed | 3 passed | 45 skipped (49)

ui › Connectors landing page
× offers aggregator Slack sign-in on Cloud while the built-in Slack connector needs a customer OAuth app
AssertionError: expected 'SlackSend and read messages in your t…' to contain 'Connect through Composio or Arcade.'
Received: "SlackSend and read messages in your team's channels.Connect"
Tests  1 failed | 5 passed | 92 skipped (98)

shared › Slack tool connection guidance
× describes the customer-created Slack app honestly and links to Slack app settings
AssertionError: expected 'Use the provider-hosted connection fo…' not to match /quickest/i
Tests  1 failed | 3 passed (4)
```

**Green (after the fix):**

```
✓ server › Slack on Paperclip Cloud › offers aggregator Slack routes while native Slack only supports a customer-created OAuth app
✓ server › Slack on Paperclip Cloud › keeps native Slack when Chat connectors provide its own setup path
✓ server › Slack on Paperclip Cloud › keeps native Slack for a company that already saved a Slack connection
✓ server › keeps native Slack on self-hosted instances, where operators register their own Slack app
✓ ui › offers aggregator Slack sign-in on Cloud while the built-in Slack connector needs a customer OAuth app
✓ ui › keeps the built-in Slack connector when Chat connectors offer its setup path
✓ ui › keeps the built-in Slack connector when the instance is self-hosted
✓ ui › keeps a saved built-in Slack connection on its native row on Cloud
✓ shared › describes the customer-created Slack app honestly and links to Slack app settings
✓ shared › flags Slack only while its chat setup is unavailable
✓ shared › does not flag connectors with a managed, dynamic-registration, or credential path
```

Neighbouring suites:
- `packages/shared` (all): 98 files, 1060 tests passed
- `ui` `src/pages/apps` + `src/features/connections`: 54 files, 980
tests passed
- `server` `connection-aggregator-fallback` 49/49,
`connection-intents-service`, `native-connection-baseline-fixture`,
`runner-connection-eval-fixtures` passed. `satisfied-connection-intents`
52/52 passed when run alone. It had a suite-setup error only when three
embedded-Postgres runs went in parallel.
- `pnpm check:token-gates`: clean. `tsc --noEmit` shows no errors in
changed files. Local runs show unrelated errors because the
`paperclip-runner` workspace package is not built. CI runs the full
typecheck.

Still needs a live Slack workspace to prove:
- Complete Slack sign-in through Composio or Arcade on a Cloud instance,
and an agent tool call to Slack.
- An agent run on Cloud that asks the provider-choice question for Slack
and then connects through the selected aggregator.

## Risks

- Behaviour change on Cloud only, for native connectors whose every path
needs a customer-created OAuth app and that an aggregator serves. Today
that is Slack (when Chat connectors are off), and Box and Xero if the
aggregator catalog has them. On Cloud, these move to the aggregator
route for new setup, the same as the built-in Google connectors. Saved
connections keep their native row and discovery.
- Cloud users who want their own Slack app can still open
`/apps/connect?source=slack`, but the catalog row no longer links to it
while the rule applies.
- Self-hosted instances keep the native connector unchanged.
- Follow-up, not in this PR: enable managed Slack chat on Cloud (Chat
connectors Cloud default and `PAPERCLIP_SLACK_MANAGED_SETUP_ENABLED`).
This is a flag and operations change. It needs the Cloud Slack broker to
be proven first. When it is enabled, this rule stops yielding Slack
automatically.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with
tool use (shell, browser inspection of a staging instance, file
editing).

## 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-05 16:12:45 -06:00
DottaandPaperclip 789a71bc17 fix(runner): retry quarantined tasks in their saved workspace (#19340)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native sessions retain execution ownership and action history across
turns.
> - Task Retry must prove that old execution has stopped before it
starts another turn.
> - An attempt can inherit its predecessor's identity and fail before it
launches a provider.
> - The cleanup proof searched under that failed attempt and compared
unrelated event sequences.
> - This pull request verifies the retained owner and starts one fresh
authorized turn.
> - Saved results and uncertain action outcomes remain intact.

## Linked Issues or Issue Description

**What happened?** Task Retry accepted repeated clicks but kept the
canonical request deferred with `executionWait.reason: local_cleanup`.
It created no successor, although the retained local worker and provider
had stopped.

**Expected behavior:** After exact cleanup proof, admit one fresh turn.
Preserve the old journal and result. Keep unresolved action outcomes
unknown.

**Steps to reproduce:**
1. Finish a local native turn with retained cleanup state and a saved
result.
2. Let the next attempt inherit its identity and fail with
`native_session_cleanup_quarantined` before any launch.
3. Upgrade across a tool-contract change, then click task Retry.
4. The old code requires the failed attempt's stop receipt and journal
identity. It also requires the current tool fingerprint and equal
sequence numbers across raw and normalized events. Those requirements
cannot match the retained owner.

**Paperclip version or commit:** Reproduced on `31bf6e4482`.
**Deployment mode / installation method:** Authenticated, self-hosted
source checkout.
**Agent adapter(s) involved:** Native `paperclip_runner`, local Codex
app-server. The shared physical-stop proof also covers ACPX.
**Database mode:** Embedded PostgreSQL 18.1.
**Access context:** Board task Retry.
**Node.js version / operating system:** Incident: Node 24.20.0, Linux
ARM64. Local verification: macOS ARM64.
**Relevant logs or output:** Accepted Retry stays in
`deferred_issue_execution`; no new run or provider launch occurs.
**Relevant config:** Per-turn local native execution. The old tool
contract differs from the current contract.
**Additional context:** Related: #19311 prevents the oversized tool
result that can lead to retained cleanup. #18217 covers broader cleanup
isolation and retained-result maintenance. #19134 admits stopped Runner
sources under the current run lease. This patch fixes explicit
continuation admission on current master.

## What Changed

- Resolve the retained journal's owner under the same company, task,
agent, workspace, native session, and runner instance.
- Require terminal ownership, settled controllers, released local
leases, and exact inherited process birth. Refuse any launch evidence on
the failed attempt.
- Accept a current host's verified process absence from its exact launch
receipt when an older server lacks a stop receipt. A later launch
invalidates that observation.
- Validate normalized receipts against their own IDs, sequences, and
digests. Match ordered provider identities to the independent raw
journal.
- Keep tool compatibility at the session reuse boundary. Explicit
continuation always uses a fresh session.
- Bind the stop proof to its exact successor and canonical Retry
receipt. Reuse the proven local workspace through normal preparation.
Refuse changed bindings, closed or remote workspaces, pending
operations, and other live leases. Projectless run-ID placeholders keep
the existing fresh-workspace fallback.
- Retain journals, results, files, and uncertain action outcomes. Add
regression and opt-in live acceptance coverage.
- Make the split-handoff browser test wait for both child-completion
notices and all run settlement before teardown. Assert the final task
set stays unchanged.

## Verification

- Red: canonical Retry on the original code accepts one saved intent but
creates zero successors and records `local_cleanup`.
- Red: distinct raw and normalized event sequences reproduce that wait
after owner resolution alone. After admission was repaired, live
acceptance then exposed the empty-attempt workspace problem. A
PostgreSQL regression also reproduced that wrong workspace selection.
- Green at `6684df5e82`: **479 targeted cases**, including 26 new
PostgreSQL cases for duplicate clicks, exact process and workspace
ownership, controller/lease fences, changed state, and independent
receipt integrity. The complete 1,150-test native executor suite passed
earlier.
- `pnpm -r typecheck` and `pnpm build` pass again at final head
`432a52bea8`, including its build stamp. Token gates pass. Master now
contains the unrelated directory-fixture type correction found during
the first typecheck.
- Real authenticated HTTP Retry with Codex `gpt-6.1-sol`, high effort,
and per-turn execution passes again at final head `432a52bea8` (93.3
seconds). It preserves the first file line, appends the second once,
starts a fresh session in the existing workspace, keeps the retained
journal unchanged, commits finalization, and clears task locks.
- The strengthened split-handoff browser scenario passes **three
consecutive local runs**. Its original CI teardown and one same-head
rerun failed when cleanup cancelled an active legacy
completion-notification run and hit a PostgreSQL row-lock timeout. The
scenario now waits for the complete handoff before cleanup. [Initial
failure](https://github.com/paperclipai/paperclip/actions/runs/42106466518/job/127523693634),
[same-head
rerun](https://github.com/paperclipai/paperclip/actions/runs/42106466518/job/127531698321).
These failures remain part of the verification record.
- The local full `pnpm test:run` passed all six chat integration shards.
Its obsolete pre-rebase attempt was stopped during the general server
phase; no complete local-suite pass is claimed. Full CI, including both
required gates, passed at `6684df5e82`. At final head `432a52bea8`, all
initial CI groups passed except the unchanged status-card deadline test,
whose post-wakeup clock assertion missed its bound by 10 ms. That exact
case passed three unchanged local repetitions (16.8/27.0/18.3 seconds);
the single isolated shard rerun passed all **2,218 tests** (123 files),
with no source changes. [Initial timing
failure](https://github.com/paperclipai/paperclip/actions/runs/42114473652/job/127550722187).
- Greptile reviewed `6684df5e82` at **5/5**, with no actionable
findings. A second reviewer identified the supported projectless
placeholder case. A new real PostgreSQL regression reproduced its
workspace-binding failure before the one-line fallback correction; all
**45 focused cases** now pass, including default environment selection
and changed binding/cwd refusal. Greptile reviewed final head
`432a52bea8` at **5/5**, with no blocking findings. The second
reviewer’s thread is resolved.
- The patched Linux build verified the actual retained worker/provider
ownership and process absence in a transaction that was rolled back. No
retained state was retired or provider started during that
pre-deployment check.

## Risks

- This changes an execution admission proof. Incorrect ownership could
permit a second executor. The proof retains process, controller, lease,
scope, journal, and receipt fences, and duplicate admission is tested
through the real queue.
- Missing or conflicting evidence remains deferred. Windows, remote
execution, automatic replacement, and session reuse retain their
existing policies.
- No database migration or UI change. The original affected task still
needs deployment of the fix before recovery can be claimed.

## Model Used

- OpenAI GPT-6 through Codex, with reasoning, repository tools, code
execution, SSH diagnostics, and live provider testing. This session does
not expose the exact backend model build or context-window size. The
live test agent uses OpenAI `gpt-6.1-sol` at high effort.

## 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-05 16:04:38 -06:00
DottaandPaperclip 15249a689d fix(server): preserve sessions across cosmetic sandbox updates (#19349)
## Thinking Path

> - Paperclip manages agent work and saves provider sessions between
turns.
> - Session and workspace compatibility include the selected environment
revision.
> - The managed sandbox reconciler also updates names, descriptions, and
stock metadata.
> - Those cosmetic updates advanced the revision even when the execution
configuration stayed the same.
> - A later turn therefore discarded an otherwise compatible saved
session.
> - This pull request keeps that revision stable for cosmetic
reconciliation and retains resets for real configuration changes.

## Linked Issues or Issue Description

Related: #18943 and #18552.

**What happened?**

A managed sandbox rename or description update made a later task turn
start a fresh provider session. The driver, provider, snapshot, and
agent configuration could all remain unchanged. Both session and
workspace freshness use the environment's `updatedAt` as its
configuration revision.

**Expected behavior**

Cosmetic managed reconciliation preserves compatible task sessions.
Actual sandbox configuration or availability changes retain the normal
replacement checks.

**Steps to reproduce**

1. Create a managed sandbox and save a task-session fingerprint for it.
2. Reconcile the same sandbox configuration with a new name,
description, or managed metadata.
3. Evaluate the saved session against that environment. Before this fix,
the decision resets the session and reports the environment category.

**Paperclip version or commit**

Reproduced on master `d3713d827f70ddba63a323e57c562384dbd648ba`.

**Deployment mode**

Managed sandbox server. The regression uses an isolated PostgreSQL
database and the actual reconciler and session-freshness resolver.

## What Changed

- Keep the existing revision for an active managed sandbox when its
execution configuration stays equal.
- Continue advancing the revision when configuration or availability
changes.
- Retain activity records and managed-stock binding updates for cosmetic
reconciliation.
- Add a regression that checks session reuse, audit history, and
rejection after a real snapshot change.
- Document the freshness contract.

## Verification

- The new PostgreSQL regression failed before the production change: it
reported `reset: true` and changed category `environment`.
- All 53 environment-service cases pass after the fix.
- All 190 session-freshness and assembled heartbeat session-preparation
cases pass.
- Full local typecheck passes. The full local Vitest run remains active,
with its local build queued afterward. Initial typecheck setup failures
were retained and resolved by repairing generated SDK links; no source
workaround was added.
- [Full current-head
CI](https://github.com/paperclipai/paperclip/actions/runs/42114984839)
passes, including the sharded test suites and build: 55 successful
checks, 3 skipped, and 1 neutral.
- Greptile and Ponytail both reviewed this commit at 5/5 with no
findings. No review threads remain unresolved.
- Deployed verification remains pending normal delivery. No pinned
release or manual rollout is required.

## Risks

- Cosmetic managed reconciliation keeps `environments.updatedAt`
unchanged, so it does not reorder that environment by this timestamp.
Activity records and managed-resource bindings still record the update.
- The comparison uses canonical configuration hashes, so JSON property
order cannot hide a real change or create false drift.
- Existing fingerprint formats and saved session records remain
compatible. Real configuration and availability changes still advance
the revision.

## Model Used

OpenAI GPT-6 through Codex desktop, with reasoning and terminal/browser
tools. The host does not expose a more specific deployment suffix 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-05 16:03:30 -06:00
DottaandClaude Opus 5.5 da3084a05d fix(recovery): do not race a just-accepted interaction's own continuation wake (#19332)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents ask humans for decisions through thread interactions, such as
a plan approval
> - When a human accepts an interaction, the accept route enqueues a
continuation wake for the agent
> - A recovery sweep also re-wakes the agent when an accepted
interaction appears to have no continuation
> - The sweep can read the issue in the short gap between the accept and
the accept-time wake, so it queues a second continuation for the same
approval
> - That redundant wake carries the interaction receipt, so the composer
queue shows an immutable "Accepted: <plan>" item that the user cannot
edit or discard
> - This pull request makes the sweep wait a short grace window after
resolution before it treats an accepted interaction as stranded
> - The benefit is that one approval produces one continuation, and the
queue state agrees with the approval

## Linked Issues or Issue Description

No public issue exists. This bug was found in a hands-on onboarding test
on a staging cloud workspace.

**What happened?**
After the user approved the plan, a queued item stayed in the composer
queue. The user could not edit or discard it. The same item stayed.
Approval did not create a new item each time.

Evidence from the staging workspace (wake diagnostics and run records):
- The plan approval resolved at 17:42:05.692.
- The accept-time wake and its run were created at 17:42:05.992. That
run delivered the approval and succeeded at 17:42:51.
- The recovery sweep (`source: issue.interaction_continuation_recovery`)
requested a second wake for the same approval at 17:42:06.201. Its
`retryOfRunId` fell back to the interaction's source run. This shows
that the sweep saw no run after the resolution, so it read the issue
before the accept-time wake existed.
- The redundant wake was deferred behind later runs and a busy shared
workspace. It ran only at 18:19. During that time, `GET
/issues/:id/queued-comments` returned it as an immutable interaction
entry ("Accepted: Approve storefront prototype plan").

**Expected behavior**
One accepted interaction produces one continuation. A delivered approval
does not stay in the composer queue.

**Steps to reproduce**
1. An agent asks for plan approval on a task.
2. A user accepts the plan.
3. The stranded-issue recovery sweep runs within the moment between the
accept and the accept-time wake enqueue.
4. The sweep queues a second continuation that carries the same
approval. The composer queue shows it as an uneditable item until that
duplicate run finishes.

## What Changed

- `server/src/services/recovery/service.ts`: the accepted-interaction
recovery branch now skips the requeue while the interaction resolved
less than `INTERACTION_CONTINUATION_RECOVERY_GRACE_MS` (2 minutes) ago.
The check applies only when no successful run has delivered the
interaction yet. All other recovery branches for the issue keep their
behavior.
- The skip reports the grace deadline through `onDeferredDeadline`. The
sleep-aware recovery scheduler then rechecks the issue at that deadline,
so a lost accept-time wake still recovers without another write.
- `server/src/__tests__/heartbeat-process-recovery.test.ts`: two
regression tests.
- `does not race a just-accepted plan approval before its accept-time
wake is visible` reproduces the staging race and is the red/green test.
It also asserts that the skip registers the deadline, and that a sweep
after the deadline requeues the approval.
- `does not requeue an accepted plan approval whose accept-time wake is
deferred behind another run` guards the deferred accept-time wake case.
This test already passed on `master`.
- `dispatches the onboarding first task once the user answered the
opening card` now seeds an answer 3 minutes old instead of "now". The
test checks that a lost continuation is requeued. An answer inside the
grace window is not yet lost, so the older seed keeps the test's intent.

Related: the Slack connector cancel bug from the same onboarding test (a
stuck steering item after the user declined the connector) is likely the
same redundant plan-approval wake. At 17:52:10, right after the decline,
the item still queued was a retry of this duplicate wake (it carried the
plan approval, not the Slack interaction). This PR removes the
duplicate, so it most likely covers that symptom too. That bug has its
own investigation.

## Verification

Red, on `master` code with only the new tests added:

```
$ cd server && npx vitest run src/__tests__/heartbeat-process-recovery.test.ts -t "plan approval"
     × does not race a just-accepted plan approval before its accept-time wake is visible 1029ms
 FAIL  src/__tests__/heartbeat-process-recovery.test.ts > heartbeat orphaned process recovery > does not race a just-accepted plan approval before its accept-time wake is visible
AssertionError: expected 1 to be +0 // Object.is equality
 Test Files  1 failed (1)
      Tests  1 failed | 3 passed | 411 skipped (415)
```

Green, with the fix (the final commit also checks the registered
deadline and the requeue after it):

```
$ cd server && npx vitest run src/__tests__/heartbeat-process-recovery.test.ts -t "plan approval"
 Test Files  1 passed (1)
      Tests  4 passed | 411 skipped (415)
```

Neighbouring interaction-recovery tests on the final commit (`-t "plan
approval|accepted interaction|answered question|answered interaction"`):
9 passed and 1 failed. The failing test, `schedules bounded retries for
failed accepted interaction continuation wakes`, is a timing flake. It
uses a resolution time in March 2026, so the grace window does not apply
to it. It failed 3 of 3 runs with the original `service.ts` on the same
machine under load, and it passed 1 of 2 runs with the fix.

- `pnpm --filter @paperclipai/server... typecheck`: passes.
- `-t "onboarding first task|plan approval"` on the final commit: 8
passed.
- CI runs the full `heartbeat-process-recovery` suite in the serialized
server shards.

## Risks

- Low risk. If an accept-time wake is really lost, recovery now re-wakes
the agent up to 2 minutes later than before. The sweep already runs on
an interval, so this adds at most one sweep of delay.
- The grace window does not change behavior when a successful run
already delivered the interaction.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used in Claude Code
with tool use (shell, file edits, browser inspection of the staging
workspace API).

## 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-05 15:47:46 -06:00
DottaandClaude Opus 5.5 2bd2e650fe fix(connections): let agents outside a grant request scoped access to large catalogs (#19327)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Agents use company app connections, such as GitHub, through
`connections_search` and `connection_request`.
> - A per-user OAuth connection is shared only with the agent that
requested it. Other agents must request their own scoped access.
> - In a hands-on onboarding test, the CEO agent connected GitHub. A
Frontend Engineer hired earlier did not get access. It asked for access
and got a refusal that it could not act on.
> - The engineer then reported "no GitHub access" while the CEO reported
"GitHub is connected". The user saw this as inconsistent and unreliable.
> - This pull request makes the refusal actionable. It also makes an
agent with existing access get `ready`.
> - The benefit is that each agent can request, and then accurately
report, its own access to a connection.

## Linked Issues or Issue Description

**What happened?**

An onboarding test on staging connected GitHub through the CEO agent's
connection request. The grant was delegated and installed only for the
CEO. The Frontend Engineer (hired two minutes earlier) called
`connection_request({ service: "github" })` and then
`connection_request({ service: "github", connectionId })`. Both calls
returned 422 `Specify the tools needed for this task (up to 20)`. The
GitHub catalog has 46 active tools. The refusal gave no tool names.
`connections_search` gave `connectionId: null` for that agent. The agent
told the user that it had no GitHub access. The CEO said GitHub was
connected.

**Expected behavior**

Each agent accurately reports, and can request and use, the connector
access granted to it.

**Steps to reproduce**

1. Connect a per-user GitHub connection (more than 20 indexed tools)
through agent A's connection request.
2. Assign a task that needs GitHub to agent B, which existed before the
grant.
3. Agent B calls `connection_request` with `service: "github"`.
4. The call fails with the bare "up to 20" message, and agent B cannot
find the tool names to request.

**Deployment mode**

Paperclip cloud (staging).

**Agent adapter(s) involved**

Not adapter-specific (core bug).

**Additional context**

Root cause from the staging evidence:

- Connection `GitHub` (oauth, `credentialPolicy: per_user`, 46 active
tools). One user grant. Delegations and installs: CEO at 17:45, Frontend
Engineer at 18:18.
- Frontend Engineer run, 17:53–18:10: git push logged `GitHub
capability_missing`. Two `connection_request` calls returned the 422
above.
- The engineer found the names through `call_api` later and filed a
scoped card at 18:10. The user approved it at 18:18. After that, the
engineer had access.
- The grant scope is intentional: a per-user grant is not shared
automatically with other agents. The defect is the dead-end refusal, not
the scope.
- Repository creation after the grant (a separate onboarding report) is
not in this PR.

## What Changed

- `server/src/services/connection-intents.ts`: when a scoped access
request needs more than 20 tools, the 422 now names the eligible
connection id and lists its indexed tool names (first 100). It tells the
agent to retry with only the needed names. No access is granted.
- `server/src/services/connection-intents.ts`: a request without
`toolNames` from an agent that already has allowed tools on the
connection now reports `ready`. Before, it demanded a grant for every
remaining catalog tool.
- The guessed-tool-name path keeps its existing 20-name bound.
- `server/src/__tests__/connection-intents-service.test.ts`: a
regression test that reproduces the staging setup. The setup is a
per-user GitHub grant shared with the CEO only, 46 tools, and an
engineer outside the grant.

## Verification

Red (test added first, service unchanged):

```
pnpm --dir server exec vitest run src/__tests__/connection-intents-service.test.ts -t "hired before a GitHub grant"

 FAIL  connectionIntentService > lets an agent hired before a GitHub grant request the access the grant left out
AssertionError: expected 'Specify the tools needed for this tas…' to contain '6c2d6faf-e059-4578-9ac4-8a5fbd81ecfa'
Expected: "6c2d6faf-e059-4578-9ac4-8a5fbd81ecfa"
Received: "Specify the tools needed for this task (up to 20)"
 Tests  1 failed | 24 skipped (25)
```

Green (after the fix; the same test):

```
 Test Files  1 passed (1)
      Tests  1 passed | 24 skipped (25)
```

The test checks these steps:
1. The engineer's search gives `needs_user_action`.
2. The refusal names the connection and the needed tools
(`create_repository`, `push_files`).
3. The scoped retry creates a card.
4. The user approves the card.
5. A plain `connection_request("github")` gives `ready` for that
connection.

During development, an intermediate version of the fix failed step 5 of
the same test. That failure caused the second change.

Neighbouring suites:
- `connection-intents-service.test.ts`: 25 passed.
- `connection-aggregator-fallback.test.ts`: 45 passed. This includes the
bounded guessed-name recovery tests.
- `satisfied-connection-intents.test.ts`: 52 passed. Its `afterAll` hook
times out locally after 30 s. This also occurs on unchanged master code,
so it is not caused by this change.

`tsc --noEmit` for `server` gives no errors in the changed files. The
local workspace had other errors because some workspace packages were
not built. CI runs the full typecheck.

## Risks

- Low risk. The refusal message is longer, and it lists up to 100 tool
names. It lists names only for a connection that the requesting identity
is already eligible to use (same check as before).
- Behaviour shift: an agent with partial access that asks without
`toolNames` gets `ready` and no new access card. To get more tools, the
agent must name them. This matches the tool description ("pass
connectionId and the exact required toolNames").
- No schema, API shape, or permission change. No access is granted
without human approval.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, in Claude Code with
tool use (shell, browser inspection of the staging run history, 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-05 15:41:08 -06:00
Nicky LeachandPaperclip 9a79384958 Relay bounded Runner source refusal diagnostics (#19302)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - An environment plugin can submit a Runner continuation from a
retained source.
> - The host can refuse that continuation when its source is unverified
or has an unresolved operation.
> - The worker and Core currently discard that bounded reason.
> - Raw provider errors can contain credentials, text or foreign
identities.
> - This pull request relays only a closed diagnostic and keeps the
existing admission and redaction rules.

## Linked Issues or Issue Description

Refs #19134 and #19157. This independent fix is based on master. A
search found no duplicate relay.

**What happened?**

A source submit can receive HTTP 409 with `SESSION_STATE_UNAVAILABLE`.
The worker drops its structured data. Core then replaces the error with
a generic message. The caller cannot distinguish the two known
diagnostic reasons safely.

**Expected behavior**

Carry only the fixed status, code and reason through SDK worker RPC and
Core. Keep all provider text and identities out of the relayed error. A
diagnostic must not authorize a retry or change state.

**Steps to reproduce**

Submit an authorized retained source through a task provider. Have the
provider throw the recognized state-unavailable error. Read the error
after worker RPC and Core dispatch.

## What Changed

- Export `PluginEnvironmentTaskErrorDiagnostic`,
`environmentTaskErrorData` and `readEnvironmentTaskErrorDiagnostic`.
- Accept only `httpStatus: 409`, `errorCode:
"SESSION_STATE_UNAVAILABLE"` and reason `"unresolved_operation"` or
`"source_state_unverified"`.
- Attach the versioned envelope only to an environment task `submit`
with `resumeFrom`. Replace its provider message with a fixed message.
- Revalidate and reconstruct the closed error in Core. Keep other
failures under the existing generic redaction.
- Document older SDK behavior and the diagnostic-only boundary. No
grant, retry, discard, lifecycle or admission rule changes.

## Verification

- Rebased head `7d02bb3e4c` includes master’s UI fixture fix from
#19342. The sole conflict was in the worker integration test imports. It
now imports both the physical-task result schema from #19300 and the
diagnostic reader and RPC error type from this PR. The 39 SDK/worker
cases and 57 Core/runtime cases passed, including PostgreSQL worker
integration. The SDK build and Runner/server TypeScript checks passed.
[Current-head
CI](https://github.com/paperclipai/paperclip/actions/runs/42111889388)
passed without retries. All 56 check-rollup entries were green,
including Snyk and Socket; [standard Greptile review
2](https://github.com/paperclipai/paperclip/pull/19302#issuecomment-6665823815)
scored this exact head **5/5**, with zero unresolved threads. The PR was
marked ready on November 5 at 21:37 UTC.

- Both new worker relay cases fail against the baseline worker and pass
with the fix.
- 46 SDK/worker cases passed, including both reasons, malformed/foreign
envelopes, extra identities, unsafe getters and non-source operations.
- 51 Core/runtime cases passed. The integration uses real SDK
serialization and dispatch with PostgreSQL through the existing
authorized retained-source submit. It verifies both reasons, malformed
RPC rejection, extra-field removal, preserved source outcome and
existing dispatch bookkeeping. A valid envelope on an ordinary submit or
status stays generic.
- Runner and SDK builds and server TypeScript check passed. An initial
Runner build lacked its eval-kernel build prerequisite; it passed after
that prerequisite was built.
- The first expanded DB fixture used a shared environment name and
failed its unique constraint. It now uses its unique fixture ID; both
complete focused files pass.
- Diff and added-line secret scans passed. [GitHub workflow
42083410387](https://github.com/paperclipai/paperclip/actions/runs/42083410387)
passed on its second attempt. All CI checks completed, and Snyk passed.
Standard Greptile scored exact head `38559ec4df8c` 5/5 with no
unresolved threads. That result applies to the previous head.
- The first GitHub attempt failed in the unchanged concurrent-budget
case in `decision-models.test.ts` (one provider call observed, two
expected). That exact case passed locally without code or assertion
changes. The workspace shard also passed 4,960 tests before the
unchanged CLI company import/export fixture exceeded its 30-second
child-server startup wait. No port-collision cause is claimed; that
fixture deliberately reserves the requested port. Both initial logs are
retained. One failed-job-only retry passed without source changes.

## Risks

Older SDKs keep the generic failure. The envelope is diagnostic evidence
only. Provider operation and call identities are never accepted.
Specific user-visible operation reporting must use Core's retained event
or receipt under its existing authority.

Master does not contain the new claimed predecessor orchestration. This
PR tests the existing retained-source submission boundary. It does not
claim the #19157 source recovery or old-journal end-to-end
qualification. Runtime pins and deployments remain separate.

## Model Used

OpenAI GPT-6.1 Sol (`gpt-6.1-sol`), high reasoning. Used repository
tools, TypeScript, SDK RPC and real PostgreSQL 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 described the issue in-PR using the bug template
- [x] I have not referenced internal or instance-local Paperclip issues
or links
- [x] My branch name describes the change
- [x] I have run the focused tests locally and they pass
- [x] I have added tests for the new behavior and rejection paths
- [x] I have updated relevant documentation
- [x] I have considered and documented risks
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no unresolved findings
- [x] I will address reviewer comments before requesting merge

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-05 13:38:55 -08:00
DottaandClaude Opus 5.5 00c7738513 fix(task-chat): keep messages sendable after a declined connection request (#19331)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The task chat composer shows a queue of saved input that waits for
the agent's next turn
> - When a person declines a connection request (for example Slack), the
server queues a read-only receipt so the agent learns the outcome
> - The composer showed that receipt with its raw JSON receipt in hover
text, and the receipt hid the person's next message
> - The next message could not be edited, discarded, or sent, so the
chat looked stuck after a simple "Decline"
> - This pull request shows a readable status for receipts and keeps the
person's own later messages visible and actionable
> - The benefit is that declining a connection resolves cleanly and the
conversation continues

## Linked Issues or Issue Description

**What happened?**
During an onboarding test on staging, a person declined the agent's
"Connect Slack" request. A queued steering item then stayed above the
composer. It could not be edited. Its hover text showed raw JSON.
Sending the next message produced an error.

**Steps to reproduce**
1. Give a task to a Paperclip runner agent whose run is active or waits
on a busy workspace.
2. Let the agent request a connection, then decline it.
3. See the queued item "Rejected: Connect Slack". Hover it: the title is
the full JSON receipt.
4. Send another message. The message never appears in the queue. Its row
controls stay disabled. Send or Interrupt on the receipt queue returns
`409 This queue does not support interruption`.

**Expected behavior**
A declined connection request resolves cleanly. It shows a readable
status. The next message can be sent.

**Evidence (staging wake diagnostics for the test task)**
- 17:52:09: the decline wake for the Slack request. It stayed deferred,
was never claimed, and was skipped at 18:21.
- 17:52:10: a later retry wake for the plan-approval continuation.
- `GET /issues/:id/queued-comments` projects the oldest matching wake.
From 17:52 to 18:21 the composer showed the Slack receipt.

Refs: related to the separate plan-approval queued-message fix, which
changes the recovery sweep. This PR only touches the queue projection
and the receipt label.

## What Changed

- `server/src/services/queued-interaction-response.ts`: add
`queuedInteractionSummary()`. A receipt now has one readable line, for
example `Declined: Slack connection`, `Connected: GitHub connection`, or
`Accepted: Build the app`. The entry `source.summary` carries it. The
steering body for the agent still includes the typed JSON receipt.
- `server/src/routes/issues.ts` (`findQueuedCommentWake`): a queued
user-message wake now takes precedence over a receipt wake. A receipt
shows only when it is the only queued input. The receipt wake itself is
unchanged and is still delivered to the agent.
- `server/src/modules/wake-queue/adapters/postgres.ts`
(`findExistingDeferredWake`): skip interaction-receipt wakes when a new
message picks the deferred wake it merges into. Before, the receipt was
the oldest deferred wake, the merge was skipped, and every later message
started its own wake. Only the first message then appeared in the queue.
Receipts still never absorb user input.
- `packages/shared/src/types/issue.ts`: add the optional `summary` field
to `IssueQueuedCommentEntry.source`.
- `ui/src/components/task-chat/TaskChatQueuedMessages.tsx`: add
`queuedMessageLabel()`. The row text, the hover title, and all
aria-labels use the summary. They never use the raw steering body.

## Verification

Red/green tests were written first and run against the unchanged code.

New tests:
- `server/src/__tests__/issue-queued-comments-routes.test.ts`
- `shows a declined connection receipt as a readable status, not its
JSON`
- `keeps the next message visible and actionable after a declined
connection (running run | stopped run)`: declines Slack, posts the next
message, and asserts that the queue lists that message with
`canEdit`/`canDiscard`, that the discard works, and that the receipt
wake is still pending for the agent.
- `queues every later message together after a declined connection
(legacy run | stopped run)`: two messages after a decline both appear in
one editable queue, and the receipt wake keeps no message ids.
- `ui/src/components/task-chat/TaskChatQueuedMessages.test.tsx`
- `shows a declined connection request as readable text, never its raw
JSON receipt (BUG-08)`

**Red (unchanged code):**
```
server: × shows a declined connection receipt as a readable status, not its JSON
        AssertionError: expected { kind: 'interaction', …(3) } to match object { …, summary: 'Declined: Slack connection' }
server: × keeps the next message visible and actionable after a declined connection (running run)
server: × keeps the next message visible and actionable after a declined connection (stopped run)
        AssertionError: ["Declined: Slack connection"]: expected undefined to match object { canEdit: true, canDiscard: true }
        (the queue lists only the receipt; the new message is missing)
        Tests  3 failed | 85 skipped (88)
ui:     × shows a declined connection request as readable text, never its raw JSON receipt (BUG-08)
        AssertionError: expected 'Reorder queued message: Rejected: Con…' not to contain '{'
        Tests  1 failed | 23 skipped (24)
```
An exploratory probe on the unchanged code also showed the send error on
this path: `POST /queued-comments/interrupt` on the receipt queue with a
running native run returns `409 {"error":"This queue does not support
interruption"}`. The exact UI text that the tester saw was not
available.

Second commit (review follow-up), unchanged `findExistingDeferredWake`:
```
server: × queues every later message together after a declined connection (legacy run)
        AssertionError: expected [ Array(1) ] to deeply equal [ …(2) ]
        Tests  1 failed | 1 passed | 88 skipped (90)
```
With the change: `Tests  2 passed | 88 skipped (90)`.

**Green (with this change):**
```
pnpm --filter server exec vitest run src/__tests__/issue-queued-comments-routes.test.ts
  Test Files  1 passed (1)   Tests  90 passed (90)
server: heartbeat-comment-wake-batching + src/modules/wake-queue
  Test Files  11 passed      Tests  322 passed (322)
server: retry-scheduling, wake-diagnostics, question-response-delivery, comment-wake-batching, dot-runner, responsible-user-invariant, stale-queue-invalidation, queued-comments routes
  Test Files  8 passed       Tests  357 passed (357)
pnpm --filter ui exec vitest run src/components/task-chat/TaskChatQueuedMessages.test.tsx src/lib/issue-queued-comment-queue.test.ts
  Test Files  2 passed (2)   Tests  31 passed (31)
server: question-response-delivery, connection-intents-service, satisfied-connection-intents
  Test Files  3 passed       Tests  199 passed (199)
```
A first full run of the route file hit a 30s `beforeAll` timeout while
embedded Postgres started on a loaded machine. One new stopped-run case
was also flaky, because the agent could dispatch the new message before
the assertion. I fixed the flake: the test now occupies the agent on
another task (`maxConcurrentRuns: 1`), which is the existing pattern in
this file. I confirmed again that this test fails without the route fix.

Typecheck: `pnpm --filter @paperclipai/shared typecheck`, `pnpm --filter
@paperclipai/server typecheck`, and `pnpm --filter @paperclipai/ui
typecheck` pass. `pnpm check:token-gates`: all gates clean. I did not
run the full `pnpm test:run` or `pnpm build` locally. CI runs them.

## Risks

- Behavior change: when a receipt and a user message wait at the same
time, the composer now shows the message queue. The receipt is no longer
shown there until it is the only queued input. The receipt wake and its
delivery do not change. The resolved card in the thread still shows the
outcome.
- The first line of the steering body for connection receipts changes
from `Rejected: Connect Slack` to `Declined: Slack connection`. The JSON
receipt for the agent does not change.
- Out of scope (unchanged on master, reproduced without any receipt):
while a native run is running, each new message gets its own queued run,
and the queue shows only the first one. Messages that join a
`scheduled_retry` run's context also do not appear in the queue
projection.
- Overlap: the plan-approval queued-message fix (BUG-03) changes the
recovery sweep, not this code. Both fixes touch the same symptom area.
They do not edit the same functions.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with
tool use (shell, browser inspection of staging, 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-05 21:03:37 +00:00
b929ad43de fix: keep dependency waits neutral and explain GitHub access failures (#19315)
## Thinking Path

> - Paperclip manages agent work and shows users what can move it
forward.
> - Task status and tool approval cards describe different parts of that
work.
> - A queued dependency wait has no provider attempt to retry.
> - GitHub can reject an approved action while other connection tools
still work.
> - These cases showed a false Retry banner and an unhelpful provider
error.
> - This pull request keeps dependency waits neutral and gives GitHub
access failures a direct repair path.

## Linked Issues or Issue Description

Related: #19268 preserves pending Retry preparation and current failure
causes.

**What happened?**

A queued wake cancelled by the dependency gate appeared as a failed
attempt with Retry. A GitHub MCP permission denial appeared as raw HTTP
error text after an approved repository action.

**Expected behavior**

Keep the task's dependency state. Show GitHub access failures with a
clear explanation and a link to the selected connection's Permissions
page. Preserve actual failures, provider details, and approval gates.

**Steps to reproduce**

1. Queue a wake, then leave its task dependent on unfinished work before
dispatch.
2. Open the task after the dependency gate cancels that wake.
3. Separately approve a GitHub action whose MCP result contains `403
Resource not accessible by integration`.
4. Reopen the saved tool approval.

## What Changed

- Recognize both queues' unstarted dependency-wait receipts in
cancellation provenance and task projections. No dispatch or recovery
gate changes.
- Classify GitHub's specific permission error after MCP result
normalization. Keep result validation, the raw diagnostic, and working
tools available.
- Apply the same display code to both historical GitHub error codes.
Identify the provider from company-scoped connection metadata, including
renamed apps, without rewriting stored decisions.
- Show a token-based inline access card with **Manage GitHub access**
and collapsed **Error details**.
- Give agents trusted recovery steps after an approved GitHub action
fails. Keep provider text in the untrusted result and prevent automatic
replay.
- Add regressions, a production-component Storybook case, and
execution/connector documentation.

## Verification

- Red before the fix: cancellation provenance, dependency-wait
projection, the real queue cancellation, approved MCP failure
classification, saved interaction projection, and the error card
assertions failed at the expected behavior.
- Green focused tests: queue cancellation, one ready-task executor,
approval-before-dispatch, one failed GitHub request, connection reuse,
retained provider diagnostics, saved interaction reads, task list/detail
projection and redaction.
- UI: 106 component tests passed. Token gates and Storybook catalog
checks passed.
- `node scripts/typecheck-low-memory.mjs`: passed.
- Local source instance: verified a saved dependency wait stays neutral
after reload; a saved GitHub error shows repair guidance, expands its
original error, and opens the correct connection Permissions page.
PostgreSQL and HTTP APIs were real. The GitHub denial was a controlled
MCP fixture; no account or provider permission changed.
- `pnpm build`: passed for all workspace packages, the Runner binary,
server, CLI and UI. The server build and all server typecheck layers
also passed after the review repair.
- Additional safety checks passed for realistic repository arguments,
active recovery, failed Retry preparation, and bounded provider
diagnostics.
- Review repair: 45 pure tests and 7 PostgreSQL cases passed. These
cover both historical error codes, renamed apps, and
company/provider/application identity boundaries. A second local browser
check verified the renamed app, retained error detail, and correct
Permissions destination.
- Final review regressions: the dispatcher and metadata cases failed
before the repair. Afterward, 25 focused cases and 59
projection/identity cases passed, including connection-key precedence.
The native wait and app-only GitHub source also passed a local browser
check.
- The historical read adds at most one connection query per interaction
read batch. It selects IDs and the provider key only and runs only for
candidate permission failures. It adds no provider requests.
- Full local `pnpm test:run` stopped at the chat suite's `afterAll`
cleanup timeout. All 268 assertions in that group and the three prior
chat groups passed. The failure is retained here; it is not counted as a
full-suite pass. CI passed on `e8a692e1e2` after one workspace-startup
rerun. Fresh CI is pending for the combined head. Its first workspace-a
attempt timed out in the CLI company import/export test's child-server
startup wait; 4,968 UI tests and 287 CLI tests passed in that job. The
same shard passed on the preceding commit. The workspace shard passed on
its one rerun. All other test, build and browser jobs passed. The [first
failure](https://github.com/paperclipai/paperclip/actions/runs/42093823928/job/127478869941)
is retained in the Actions history. Greptile reviewed the preceding head
at 5/5. Fresh review is pending for the combined head.

## Risks

The GitHub classifier matches the documented permission-denial phrase,
not every 403. Rate limits, missing resources and other providers retain
their existing error behavior. Historical normalization affects display
only. A dependency cancellation needs an unstarted run and a supported
queue receipt; current user gates, Retry preparation and recovery remain
authoritative. No schema migration or credential changes.

## Model Used

OpenAI GPT-6, through the Codex agent. The exact runtime model ID and
context-window size are not exposed in this session. Used reasoning,
repository tools, TypeScript, PostgreSQL, MCP fixtures and browser
verification. The parallel agent-recovery commit records Anthropic
Claude Opus 5.5 as co-author.

## 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 described the issue in-PR using the bug template
- [x] I have not referenced internal or instance-local Paperclip issues
or links
- [x] My branch name describes the change
- [x] I have run the focused tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation
- [x] I have considered and documented risks
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open findings
- [x] I will address reviewer comments before requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 20:55:34 +00:00
Nicky LeachandPaperclip 2506bcdaf4 Admit exact stopped Runner sources under the current run lease (#19134)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native execution keeps provider state and a durable controller
journal.
> - A completed process can stop before the next run attaches.
> - The current run must own recovery of that exact source.
> - An old controller must lose permission to change the consumed source
journal.
> - This pull request adds explicit source admission and protected task
dispatch.
> - The benefit is a narrow recovery boundary that preserves the
original successful result.

## Linked Issues or Issue Description

Native prerequisites #19132 and #19153 are merged into master. This PR
targets master and updates the TypeScript Runner driver contract
alongside Core and SDK admission. This change adds the SDK and Core
admission boundary. Executor selection and fresh source preparation are
a separate change. Related #17916 supplies the workspace export holds
preserved here.

## What Changed

- Plugins advertise `codexSourceRecoveryVersion: 1` for Codex App Server
and `acpxSourceRecoveryVersion: 1` for ACPX. Hosts can qualify either,
both, or neither. Core selects the matching field, because a
stopped-source check for one provider cannot establish the other
provider’s recovery guarantees. ACPX orchestration is tracked separately
in #19196. Use the existing exact source identity and `resumeFrom` under
the authenticated current run. No public recovery mode is added. Require
the claimed source, current account and prepared launch at protected
dispatch.
- Keep the first source binding pinned on the consumer lease, including
after uncertain dispatch. A newly acquired coordinator can refresh only
the same source.
- Claim one stopped successful source under the current run's acquired
native lease. Store the token-free consumer reference in existing lease
metadata. Preserve the source status and result.
- Check the exact six source identity fields, encrypted journal header,
company, task, agent, environment, driver, workspace, account selection,
and current coordinator generation. Keep workspace export and legacy
settlement holds.
- Serialize claims and checkpoint writers in the same coordinator lock
order. Reject stale source writes after consumption. Check the current
consumer before recovered source writes.
- Check source admission before and after protected worker dispatch.
Keep the request and lease owned by the current run while the native
identity remains the source.
- Allow the same owner to prepare B after a fresh source submission,
using the exact consumed-source proof instead of a generic warm-reuse
flag. Recheck that proof after dispatch.
- Preserve ordinary cached adoption and repeated preparation after
launch. The current authenticated owner can differ from the adopted PRP
identity without requesting a new source launch. Keep ordinary suspended
recovery and live task attachment checks. Extract the shared source
resolver to avoid a module cycle, and make ordinary selection honor the
same durable source consumer.

## Verification

- Current head `cac9cf188a` includes merged #19300 and the symmetric
capabilities introduced in `b7e25a64b4`. The task-owner and warm-rebind
fixtures now identify their physical task. All 29 SDK schema cases, 95
Core ownership and source-admission cases, 25 native Codex and ACPX
recovery cases, and the binary metadata check passed on this head. The
SDK and Runner builds and server typecheck passed. Both native providers
reject the other provider’s capability. GitHub CI is green and [standard
Greptile scored this commit
5/5](https://github.com/paperclipai/paperclip/pull/19134#issuecomment-6645574114)
with no open threads.

- Previous head `bda49271a4ae362a15baf3fe6d6d297e25fd2000` is directly
based on master after #19132 and #19153 landed. [GitHub workflow
42072192023](https://github.com/paperclipai/paperclip/actions/runs/42072192023)
passed on its first attempt. All check runs completed successfully or as
intentional workflow skips; Snyk passed. Standard Greptile scored this
exact head 5/5. That review had no unresolved findings.
- Runner and SDK TypeScript builds and server typecheck passed. The
focused SDK, task-owner, task-runtime, PostgreSQL source and authority
suites passed all 161 cases. The production transport-factory getter
regression and 14 native-lane selection checks also passed.
- The Linux native lane executed the new ordinary warm A→B process case.
It kept one Runner/provider process and completed two turns on the same
thread. The eager-getter mutation fails with
`native_runner_source_attachment_pending`; preserving the live getter
passes. The production factory test changes the current owner after
transport creation and checks that same options object.
- PostgreSQL cases cover both concurrent source-writer/claim orders,
exact source submission under the current caller, source-switch denial
after uncertain dispatch, same-source coordinator refresh, stale
same-boot generation, corrupted journal headers, competing consumers,
account changes and export holds.
- The protected dispatch sequence submits A, prepares B and retries B
preparation. It rejects undispatched sources, changed accounts and
foreign target bindings. Source status and accepted result remain
unchanged.
- Ordinary adopted A preparation under current B, repeated ordinary
preparation after launch and ordinary suspended continuation retain
their existing path. Absent or false claims preserve ordinary identity
denial; malformed claims fail closed.
- Earlier development attempts exposed ordinary preparation regressions
and the live-getter omission; the final regressions cover those fixes.
The first embedded PostgreSQL attempt needed missing library aliases
repaired in ignored dependencies. Those attempts are not passing
evidence.
- Historical, superseded CI heads had unchanged UI fake-timer and
Playwright health-response-lifetime failures. Their isolated cases and
scoped retries passed. The response-lifetime cause remains unproven. No
UI/test timeout was changed. The final rebased-head workflow above
passed without a retry.
- Whitespace and added-line secret/PII scans passed. Full workspace
checks ran on GitHub. No live vendor probe or deployment was performed.

## Risks

- Any driver that used the earlier Codex TypeScript option must use
`codexSourceRecoveryVersion`. The new plugin description rejects the
generic spelling. The Launcher’s existing Codex-specific HTTP header can
map to this property when the plugin adopts the new SDK. The change
requires no journal migration or ROM change.

- This is an inactive admission foundation. Executor orchestration is
separate in #19157. Native restore integrity and provider
stopped-source/account validation must be present before executor
activation. The native prerequisite uses ordinary recovery followed by
attachment. The composed executor proofs are in #19157.
- The source claim is durable. A later controller must recover the same
consumer or keep the hold. It cannot select another source after an
ambiguous launch.
- Admission adds bounded database checks around protected startup calls.
Busy issue locks fail closed; other lock waits have a one-second bound.
- No migration, snapshot format, proactive receipt, recurring worker, or
deployment is included. Full workspace checks and platform coverage run
in CI. These tests do not prove vendor history durability or a hosted
rollout.

## Model Used

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

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-05 12:55:06 -08:00
DottaandClaude Opus 5.5 22281d4b3a feat(task-chat): approve harness-proposed plans (Codex plan mode, Claude ExitPlanMode) (#19291)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents run through the native runner. Codex and Claude Code each
have their own plan mode, and each produces a proposed-plan document.
> - Paperclip already has a planning work mode, an issue `plan`
document, and a `request_confirmation` approval gate for plans.
> - These parts were not connected. The Codex proposed-plan item was
transcript text only. The server path never put Codex into plan mode.
Claude was never put into plan mode, and its ExitPlanMode plan was
dropped. A planning run could close its task as done with no approval.
> - This pull request turns a harness's own proposed plan into the
issue's Plan document and the existing approval gate, for Codex and
Claude.
> - The benefit is that users approve or send back the agent's real plan
from the task chat. Approve resumes the agent. Request changes sends the
reason back to the agent.

## Linked Issues or Issue Description

Refs #19252

**Subsystem affected**
Native runner provider events (Codex and ACPX Claude), the runner Codex
provider (Rust), server native plan synchronization, and the task chat
UI.

**Problem or motivation**
Codex and Claude both produce a proposed-plan document in their native
plan mode. Paperclip did not use it. In live tests on the server path, a
Codex planning run was not in Codex plan mode, saved its own
`implementation-plan` document, and marked the task done without
approval. Claude planning runs were not in Claude plan mode, and the
ExitPlanMode plan text never reached Paperclip.

**Proposed solution**
Add a canonical `plan.proposed` event for a harness's proposed plan. Put
Codex and Claude into their native plan mode for planning runs. Save a
completed proposal as the issue `plan` document with the existing
`request_confirmation` gate. Agents can also propose a plan on a
standard task; the work mode stays standard.

**Alternatives considered**
Reuse `plan.updated`. Its schema is the within-turn checklist and is not
the durable Plan document, so a separate event keeps the two clear.
Stronger prompt text without native plan mode. Live tests showed that
the model kept its own document workflow.

**Roadmap alignment**
This reuses the issue plan document, planning work mode and approval
gate that already exist. It adds one canonical provider event.

## What Changed

- `packages/paperclip-runner/protocol/schemas/*`: add the
`plan.proposed` event (`paperclip.plan.proposed.v1`: `proposalId`,
`status`, `markdown` up to 60k characters, `source`). Regenerate the
schema bundle, validators and manifest.
- `packages/paperclip-runner/src/provider-events.ts`: map the Codex
`plan` item and a `<proposed_plan>` block in a completed Codex message
to `plan.proposed`. Add `claudePlanProposalEvent`.
-
`packages/paperclip-runner/runner/crates/runner-core/src/codex_provider.rs`:
send Codex's native `collaborationMode: plan` on `turn/start` for a
read-only (planning) Codex run. Before this change the server path never
sent it.
- `runner-core/src/acpx_event_payload.rs`: admit `plan.proposed` as ACPX
rich display activity.
- `drivers/acpx/codex-runtime-adapter.ts`: set Claude Code to native
plan mode for a read-only run. Capture Claude's ExitPlanMode plan from
its permission request, record it, and decline the native "Ready to
code?" prompt.
- `drivers/acpx/codex-acpx-driver.ts`, `cli/acpx-runtime-sidecar.ts`:
emit the captured proposal as a rich event. Complete that turn instead
of reporting it as cancelled.
- `backends/codex-native-backend.ts`: per-harness plan-mode
instructions, and a short opt-in hint for Claude on standard tasks.
- `server/src/services/native-runtime/native-session-executor.ts`:
synchronize a completed `plan.proposed` into the `plan` document and a
`request_confirmation` in planning and standard mode. Do not change the
issue status during the run (that revoked the run's lease). The run
finalizer turns the pending confirmation into the review wait.
- `server/src/services/heartbeat-run-summary.ts`: keep a
`<proposed_plan>` block out of the posted reply.
- UI: new `plan_proposal` provider family. The task chat shows a live
"Writing plan" card while a proposal streams and a one-line record when
it completes. Resolved plan reviews keep a receipt ("Approved Plan v1",
"Requested changes to Plan v1: reason"). Plan previews skip section
headings.
- Storybook: `Tasks and conversations/.../Task chat/Proposed plan
approval` shows the journey.
- `doc/SPEC-implementation.md`: describe harness-proposed plan approval.

## Verification

- `cargo test -p paperclip-runner-core --lib` (new:
`read_only_codex_turns_use_native_plan_collaboration_mode`)
- `cd packages/paperclip-runner && npx vitest run
src/provider-events.test.ts
src/drivers/acpx/codex-runtime-adapter.test.ts`
- `cd server && npx vitest run
src/__tests__/native-plan-proposal-sync.test.ts
src/__tests__/heartbeat-run-summary.test.ts`
- `cd ui && npx vitest run
src/components/transcript/native-run-events.test.ts
src/components/task-chat`
- Live test on a local worktree instance with real harnesses:
- Codex (`gpt-5.6-sol`), planning task: Codex asked one question, then
produced a native plan item. The task showed Plan v1 with Approve plan /
Request changes and moved to in review. Approve changed the work mode to
standard. Codex then implemented the plan and its tests passed.
- Codex, Request changes with a reason: Codex woke in planning mode and
produced Plan v2 that includes the change.
- Claude (`claude-opus-5-5`), planning task: Claude was in plan mode.
ExitPlanMode became Plan v1 and the turn completed. Approve resumed
Claude in standard mode, and Claude implemented the plan.
- Claude, standard task that asks for a plan first: Claude stopped
before code changes and asked for plan approval. The work mode stayed
standard.
- Storybook: open `Proposed plan approval` and check each stage.

## Risks

- Codex planning runs now really use Codex plan mode on the server path.
Codex can ask a clarifying question before it proposes. This matches
Codex plan mode.
- Claude planning runs now use Claude plan mode. A declined ExitPlanMode
ends the Claude turn; the runner reports it as completed. Claude's tool
row for that request records a rejection from Claude's side.
- Codex does not stream the proposed plan on the server path yet, so the
live card shows a placeholder until the plan completes.
- A `<proposed_plan>` block in any completed Codex message on a standard
task now asks for approval. This is the documented opt-in path.

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

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), via Claude Code with
tool use (shell, file edits, browser). Long context. No extended
thinking mode was used.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 20:53:33 +00:00
Svetlana ZolotenkovaandPaperclip 1b0f78100d fix(access): return membershipRole in the company user directory (#15183)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Agents often check a human approval. The check must confirm the role
of the approving user in the company.
> - An agent can read the company user directory. The directory removes
the membership role from the server record.
> - The admin member list requires `users:manage_permissions`. An
approval check must not need that permission.
> - This pull request returns `membershipRole` in the existing user
directory and keeps all access limits.
> - The benefit is that agents can verify an approver's role without
admin permissions.

## Linked Issues or Issue Description

**What happened?**

`GET /api/companies/{companyId}/user-directory` returns active users
without `membershipRole`. An agent that checks the authority of an
approving user cannot tell an `owner` from other roles. `/members`
requires `users:manage_permissions`.

**Expected behavior**

The directory returns the exact `membershipRole` from
`companyMemberships`. A missing role stays `null`. Reading this field
does not require admin permissions. Member management keeps its current
limits.

**Steps to reproduce**

1. Use a company with an active `owner` user and an active user without
a role.
2. Call `/user-directory` with an agent key.
3. Before this change, neither record has `membershipRole`. After this
change, the field is `owner` and `null`.
4. The same agent still cannot read `/members` or change a member.

## What Changed

- Add `membershipRole` to the select and result of
`loadCompanyUserDirectory`.
- Test the exact `owner`/`null` values, agent read access, denied admin
read and change, denied other company, and denied unauthenticated
request.
- Keep the existing `assertCompanyAccess` and the company, user type,
and active filters.
- The client type includes a nullable `membershipRole`. The management
test rejects only `users:manage_permissions` and checks the exact calls.
The first review findings are fixed in a0d1ad3.

## Verification

- `pnpm --filter @paperclipai/server typecheck`: pass.
- `pnpm --filter @paperclipai/server build`: pass.
- `pnpm --filter @paperclipai/ui typecheck`: pass.
- `pnpm exec vitest run
server/src/__tests__/company-user-directory-route.test.ts
server/src/__tests__/access-service.test.ts
server/src/__tests__/access-routes-hidden-floor.test.ts`: 16 passed, 17
skipped. `ui/src/lib/company-members.test.ts` is added.
- The regression test fails on the old `access.ts` because
`membershipRole` is missing. It passes after the change.
- `git diff --check`: pass.

## Risks

The directory response gets one more field. There are no migrations. The
role comes directly from the server record, with no `owner` fallback.
User management permissions do not change.

## Model Used

OpenAI Codex, GPT-6, with shell tools, code editing, and test runs. The
environment does not expose the exact model identifier.

## 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-05 14:43:02 -06:00
DottaandClaude Opus 5.5 d807887534 fix(ui): use one status glyph mapping for every task view (#19329)
## 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: sidebar rows,
the task header, the status menu, lists, the inbox, pills and chat
cards.
> - One task state rendered different status icons in different views. A
blocked task that waits on an active sub-task showed the blue "in queue"
glyph in the task header. The status menu, the chat chip and the sidebar
rows showed the red "Blocked" glyph for the same task.
> - The cause: each view passed its own subset of the presentation
fields to `StatusIcon`. Many views dropped `blockerAttention` and some
dropped `externalConversationState`. The status menu rendered its
current row from the bare status. The summaries API did not return
`blockerAttention`, so the recent-task sidebar could not know the state.
> - This pull request puts the state-to-glyph/label mapping in one
resolver and makes every task view pass all of its fields.
> - The benefit is that the same state uses the same icon and label
everywhere.

## Linked Issues or Issue Description

No public issue. Found in a hands-on onboarding test on a staging cloud
workspace.

**What happened?**

The sidebar and the task pane/status menu showed different status icons
for the same state. On the test task (blocked, waiting on an active
sub-task), the task header and the status button showed the blue "in
queue" glyph. The selected "Blocked" row in the status menu showed the
red blocked glyph. The coordinator sidebar "Recents" and project rows
also showed the red blocked glyph, and the chat "Subtask created" chip
showed a red "Blocked" chip.

**Expected behavior**

The same underlying state uses the same icon and label everywhere.

**Steps to reproduce**

1. Create a task with a sub-task. Block the parent on the sub-task, and
let the sub-task's agent run (the parent is "blocked, covered").
2. Open the parent task. The header shows the blue "in queue" glyph.
3. Open the status menu. The current "Blocked" row shows the red glyph.
4. With the coordinator sidebar on, look at the task in Recents or in
its expanded project. It shows the red glyph.

**Paperclip version or commit**

`master` at 03eb49053f.

## What Changed

- Add `ui/src/lib/task-status-presentation.ts`.
`resolveTaskStatusPresentation()` is the one mapping from `status`,
`displayStatus`, `externalConversationState` and `blockerAttention` to
glyph, label and accessible copy. `taskStatusIconProps()` picks all four
fields from a task record.
- `StatusIcon` and `IssueStatusBadge` use the resolver. The status menu
draws the current row with the trigger's presentation (in queue, idle,
monitoring).
- `IssueStatusBadge` now accepts `blockerAttention`.
`taskStatusVar.in_queue` maps to the existing in-progress chip hue, the
same as the existing `--status-task-icon-in_queue` icon token.
- Task call sites spread `taskStatusIconProps(task)`: coordinator
sidebar recents and project rows, classic recent tasks, lists, rows,
columns, inbox, blocked inbox, my issues, dashboard, properties,
relation pills, search, sibling navigation, chat chips, case summaries,
status cards and user profile. Step/check statuses in protocol cards
stay unchanged because they are not task statuses.
- `GET /api/companies/:companyId/issues/summaries` returns
`blockerAttention` for blocked rows (at most five IDs, through the
existing `listBlockerAttention` service). `IssueSummary` gets the
optional field.
- Recent-task snapshots store and refresh `blockerAttention`. Sidebar
project rows request the `rows` projection, which includes blocker state
(the `index` projection skips it).
- Storybook: the Foundations/Status Language story gets a "Same state,
every view" section.

## Verification

Red on `master` (tests written first, then run against the unchanged
code):

```
cd ui && npx vitest run src/components/StatusIcon.picker.test.tsx src/components/CoordinatorSidebar.test.tsx src/lib/recent-tasks.test.ts
     × keeps the covered-blocker state so the sidebar shows the same queued glyph as the task pane (BUG-07)
     × marks the current row with the same glyph as the trigger for a covered-blocked task
     × marks the current row with the same glyph as the trigger for a waiting review and a monitored task
     × shows a covered-blocked recent task with the same queued glyph and label as the task pane
     × shows project tasks with their full status presentation
 FAIL  StatusIcon.picker.test.tsx > ... covered-blocked task
AssertionError: expected 'var(--status-task-icon-blocked)' to be 'var(--status-task-icon-in_queue)'
 FAIL  StatusIcon.picker.test.tsx > ... waiting review and a monitored task
AssertionError: expected 'var(--status-task-icon-in_review)' to be 'var(--status-task-icon-backlog)'
 FAIL  CoordinatorSidebar.test.tsx > ... covered-blocked recent task ...
AssertionError: expected 'var(--status-task-icon-blocked)' to be 'var(--status-task-icon-in_queue)'
 FAIL  CoordinatorSidebar.test.tsx > ... project tasks with their full status presentation
AssertionError: expected "vi.fn()" to be called with arguments: [ 'company', ObjectContaining{…} ]
 FAIL  recent-tasks.test.ts > ... keeps the covered-blocker state ...
AssertionError: expected undefined to deeply equal { state: 'covered', …(7) }
      Tests  5 failed | 32 passed (37)

cd server && npx vitest run src/__tests__/issue-summaries-routes.test.ts
     × projects the covered-blocker state so every task view picks the same queued glyph (BUG-07)
AssertionError: expected undefined to match object { state: 'covered', …(2) }
      Tests  1 failed | 14 passed (15)
```

Green with this change (same commands):

```
 Test Files  3 passed (3)
      Tests  37 passed (37)
 Test Files  1 passed (1)        # server summaries route
      Tests  15 passed (15)
```

Neighbouring and broader checks:

- `StatusIcon`, `StatusBadge`, `StatusGlyph`, picker, sidebar and
recent-task suites: 73/73 pass.
- Full UI Vitest run: 11106 pass, 1 skipped. Three `IssuesList.test.tsx`
tests timed out at 5 s under full-suite load. The same file passes 73/73
when run alone.
- `tsc --noEmit` for `ui` and `server`: no errors in changed files.
Remaining errors are pre-existing (unbuilt
`@paperclipai/paperclip-runner`, generated Storybook catalog).
- `pnpm check:token-gates`: all gates clean.

Screenshots. The staging task before the fix: the status button is blue,
the current "Blocked" row in the menu is red.

| Staging task (before) | Staging status menu (before) |
| --- | --- |
| ![staging
before](https://raw.githubusercontent.com/paperclipai/paperclip/75e148aa1b100400d406d952e66cfac6c4afa5c3/before-staging-onb1.jpg)
| ![staging menu
before](https://raw.githubusercontent.com/paperclipai/paperclip/75e148aa1b100400d406d952e66cfac6c4afa5c3/before-staging-onb1-menu.png)
|

Storybook "Same state, every view" (Foundations/Status Language). Before
uses the `master` `StatusIcon`/`StatusBadge`; after uses this branch.

| Before (master) | After (this PR) |
| --- | --- |
| ![storybook
before](https://raw.githubusercontent.com/paperclipai/paperclip/75e148aa1b100400d406d952e66cfac6c4afa5c3/before-storybook-menu.jpg)
| ![storybook
after](https://raw.githubusercontent.com/paperclipai/paperclip/75e148aa1b100400d406d952e66cfac6c4afa5c3/after-storybook-menu.jpg)
|

Before: the row glyph is blue, but the chip and the current menu row are
red. After: the row glyph, the chip and the current menu row are all the
blue "in queue" glyph.

Review follow-up (commits 1d3a1a0a9e, d5c798ec53): blocker state on
summaries, search and case responses is glyph-only (state, reason,
counts). Blocker titles and IDs are never returned; a sampled identifier
stays only when the actor can read that blocker. Producer-level tests
failed before the follow-up and pass after it
(\`task-created-items.test.tsx\`,
\`CoordinatorTaskCommitment.test.tsx\`,
\`company-search-service.test.ts\`, \`cases-routes.test.ts\`,
private-blocker case in \`issue-summaries-routes.test.ts\`). Before
merge, the branch was test-merged locally with current \`master\`:
typecheck clean, 1095 UI and 80 server tests in the affected suites
pass.

## Risks

- Low risk. Presentation only. Status mutations still use the canonical
`status`.
- The summaries endpoint now runs the blocker-attention query for
blocked rows. The endpoint accepts at most five IDs and runs the query
only when a row is blocked.
- Sidebar project rows use the `rows` projection instead of `index`.
This adds the blocker and review attention reads for at most 12 rows,
and only when a person expands a project.
- `IssueStatusBadge` shows the blue "in queue" chip for covered-blocked
tasks where callers pass `blockerAttention`. The label stays "Blocked"
and the full reason is in the tooltip.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`, Anthropic), through Claude Code
with tool use (shell, file edits, browser). Default context window.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 14:42:11 -06:00
StefanandArthur Dent 131c0c4609 fix(server): redact the credentialValues envelope in HTTP logs (#13742)
> **Attribution:** this pull request was written and opened by the
Paperclip agent **Arthur Dent** (Platform & Reliability Engineer,
Tharsia) from an automated run under the host GitHub credential. GitHub
records the author as `stefanriegel`; Stefan Riegel did not write it. No
other edit was made to this description.

## Thinking Path

> - Paperclip is the open source app that people use to manage AI agents
for work.
> - The server writes request details to the user journal. A middleware
module names the fields that are secret.
> - The connect endpoint accepts a `credentialValues` object. The object
maps a provider config path to a secret string.
> - One provider config path is `credentials.authorization`. The caller
chooses the key names. Therefore the key is one flat string, and it is
not a nested path.
> - The redaction path list did not cover the `credentialValues`
envelope. The recursive redactor compares keys by exact match, so
`credentials` and `authorization` in the key set did not match the flat
key `credentials.authorization`.
> - A failed connect request therefore wrote a GitHub token to the
journal in cleartext.
> - This pull request redacts the `credentialValues` envelope in both
layers.
> - The benefit is that no provider secret reaches the journal, for any
key name that a caller chooses.

## Linked Issues or Issue Description

No public issue exists. The report follows the bug report template.

**What happened?**

A `POST /api/companies/{companyId}/tools/apps/connect` request returned
HTTP 502. The server logs request bodies for non-2xx responses. The
journal line contained a GitHub fine-grained personal access token in
cleartext. The token was 93 characters long and started with
`github_pat_`. The line contained no redaction marker.

The logged body had this shape:

```
"reqBody":{"galleryKey":"github","connectionMethodKey":"mcp-key","name":"GitHub",
  "credentialValues":{"credentials.authorization":"<TOKEN>"}, ... }
```

**Expected behavior**

The journal line shows a redaction marker in place of the token. No
cleartext credential reaches the journal on any response status.

**Steps to reproduce**

1. Run a Paperclip server on version 2026.916.0.
2. Send a `POST` request to
`/api/companies/{companyId}/tools/apps/connect`.
3. Put a secret under `credentialValues` with the key
`credentials.authorization`.
4. Make the request fail, so that the server returns a non-2xx status.
5. Read the journal line for the request.

Result: the secret appears in cleartext. The two redaction layers both
miss it.

**Paperclip version or commit**

2026.916.0. Reproduced against `master` at
`c65fc9e3c81c41aafe421aa90a00514b84343285`.
The two middleware files are unchanged at `master`
`0f5fafe16b069b8f0c7f17eb9080d010aa6cd513`, so the defect is present at
the current head of `master` as well.

**Deployment mode**

Self-hosted.

**Installation method**

npm package.

**Access context**

Authenticated user, company-scoped route.

**Relevant logs or output**

Token value removed. The journal line has the shape above.

## What Changed

- Add `reqBody.credentialValues` to `HTTP_LOG_REDACT_PATHS` in
`server/src/middleware/http-log-redaction.ts`. This redacts the whole
envelope at the pino serialization boundary.
- Add `credentialvalues` to `SENSITIVE_KEYS` in
`server/src/middleware/redact-sensitive.ts`. This redacts the envelope
by name in the recursive pass.
- Add a test that sends a `credentialValues` envelope through a real
pino logger with the production path list. The test asserts that the
secret is absent.
- Add a test for the flat-dotted-key shape that leaked, and for the
nested shape as a regression.
- Add a test that the envelope is redacted below the top level.

The key-set change is the durable half. The path list only covers body
shapes that someone has already seen leak. The recursive pass matches
the envelope by name for any shape.

## Verification

Run these commands:

```
npx vitest run server/src/__tests__/http-log-redaction.test.ts server/src/__tests__/redact-sensitive.test.ts
```

Against the patched source, both files pass: **72 tests passed, 0
failed**. That is 54 tests in `http-log-redaction.test.ts` and 18 tests
in `redact-sensitive.test.ts`.

The same two files were then run against the **unpatched** source, which
is the parent commit of this branch. **Four tests fail there.** Those
four are exactly the tests this pull request adds:

- `http-log-redaction.test.ts` — "defines the HTTP auth and cookie
header paths that must be redacted". The path list does not contain
`reqBody.credentialValues`.
- `http-log-redaction.test.ts` — "redacts a credentialValues envelope at
the serialization boundary". This is the leak: a real pino logger writes
the token in cleartext.
- `redact-sensitive.test.ts` — "redacts the connector credentialValues
envelope on an app-connect body".
- `redact-sensitive.test.ts` — "redacts a credentialValues envelope
nested below the top level".

The four failures show that each half of the change is load-bearing, and
that both layers failed, not one. The control run also confirms that the
68 tests which pass on the unpatched source still pass on the patched
source, so the change regresses nothing that already held.

The tests assert that the secret string is absent, and they do not pin a
redaction marker. This is deliberate: the two layers write different
markers. `fast-redact` writes `[Redacted]`. `redactSensitive` writes
`[REDACTED]`. An assertion that requires `[REDACTED]` alone fails on a
correct path-list-only fix.

## Risks

Low risk. The change adds two redaction paths. It removes no existing
path.

One behavioral shift is intentional. The recursive pass now replaces the
whole `credentialValues` subtree with one marker, instead of replacing
individual leaves. Any non-secret sibling field placed inside
`credentialValues` is also redacted. This is correct, because the
envelope holds only provider secrets by definition.

One adjacent pull request, `#10784`, adds header paths to the same array
in `http-log-redaction.ts`. The two changes touch different regions of
the array. The second one to merge needs a trivial rebase at most.

## Model Used

- Provider and model: Anthropic Claude, via the Paperclip `claude_local`
agent adapter
- Capability details: tool use and code execution in a sandboxed
workspace
- The patch was authored and verified by automated agent 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
- [ ] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] I have updated relevant documentation to reflect my changes
- [ ] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [ ] I will address all Greptile and reviewer comments before
requesting merge

Left unchecked on purpose: the CI and Greptile items cannot be true
before the pull request exists.

"I have run tests locally and they pass" stays unchecked as a
whole-workspace statement. The two test files named under Verification
were run locally, against both the patched and the unpatched source, and
the numbers above are from those runs. The full workspace suite has not
run.

## Related pull requests

Refs `#10784`, `#6373`, `#8841`.

Co-authored-by: Arthur Dent <arthur.dent@paperclip.local>
2026-11-05 14:39:15 -06:00
DottaandClaude Opus 5.5 b86f55f0ba test(server): remove two more server test races (#19330)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The server Vitest shards gate every pull request
> - Two more server tests failed on pull requests that do not touch them
(#19309, #19265)
> - #19314 fixed four other flakes. These two have different causes
> - Random CI failures slow every contributor and hide real regressions
> - This pull request removes both root causes without retries or skips
> - The benefit is fewer false CI failures

## Linked Issues or Issue Description

Refs #19309, #19265, #19314

**What happened?**

- #19309, job 127459915333: `workspace-recovery.test.ts` > "keeps
ancestor Retry held when current proof changes: sibling_hold" received
`requestId: null` from a second Retry request.
- #19265, job 127481105563: `paperclip-control-plane-port.test.ts` >
"retains a durably recorded governed terminal while its postcommit
observer is pending: checkpoint timeout" failed with "native session
checkpoint timed out after 100ms" before the terminal was recorded.

**Root causes**

1. Workspace recovery (test race). `POST /issues/:id/retry` returns 202
and then starts `heartbeat.resumeTaskRetry` without waiting. The
proof-change tests change state after the receipt. When the background
nudge runs first, it admits the Retry and queues a run before the
change. The new queued run is then the latest run, so the second Retry
in `sibling_hold` is stale. I proved this locally: a 1.5 s pause after
the receipt made every gate in the family fail with a queued run in the
issue history.
2. Control-plane checkpoint timeout (budget too tight).
`checkpointTimeoutMs: 100` applies to every checkpoint in the session,
including the normal checkpoints before the terminal. On a loaded
runner, one of those took more than 100 ms. The test expects only the
checkpoint that the pending observer blocks to time out.

## What Changed

- `server/src/services/workspace-recovery.test.ts`: add
`receiptOnlyRecoveryApp`. It builds the same route app, but the route
nudge does nothing. The two proof-change families ("keeps ancestor Retry
held…" and "holds a failed-preparation HTTP Retry…") use it and call
`resumeTaskRetry` explicitly, as before.
-
`server/src/services/native-runtime/paperclip-control-plane-port.test.ts`:
the checkpoint budget is 1 s. The timeout case now waits for the run to
fail with the timeout while the observer is still blocked, instead of a
fixed 700 ms sleep. This also proves that the timeout fires while the
observer is pending.

## Verification

- With the forced 1.5 s pause and the fix applied: both
workspace-recovery families pass (80 passed).
- `npx vitest run src/services/workspace-recovery.test.ts -t "keeps
ancestor Retry held when current proof changes|holds a
failed-preparation HTTP Retry when current proof changes"`: 80 passed.
- `npx vitest run
src/services/native-runtime/paperclip-control-plane-port.test.ts`: 94
passed, run 3 times.

## Risks

- Low risk. The changes are only in tests. The route nudge is a latency
hint ("Maintenance redelivers it"). Other tests still cover it.

## Model Used

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

## Checklist

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

> - Paperclip is the open source app people use to manage AI agents for
work
> - A native run writes its task files in a remote sandbox. The task
workspace records that run as its writer until the files are saved.
> - When a user stops or reassigns a run, the cancelled-workspace
preservation worker records the unsaved files as file recovery work.
> - A warm run (`keep_running`) that is not successful releases its
reusable sandbox through stop-and-retain. That release can finish before
the worker sees a terminal coordinator.
> - The worker accepted only an active lease or a released ephemeral
lease. It ignored the released reusable lease, so it never recorded the
file recovery.
> - The workspace kept the ended writer's hold. Each successor run
waited, then failed with "The workspace still has an unfinished writer
or save operation" after the one-hour cutoff.
> - This pull request accepts a released reusable lease that has a
confirmed stop-and-retain receipt as a preservation source.
> - The benefit is that the stopped run's files become recovery work,
and the task is not stranded behind a hold that nothing settles.

## Linked Issues or Issue Description

Refs #18820 (preserves files when Stop finds no live controller). Refs
#17677 (schedules cancelled workspace preservation). Refs #19285 (parks
a workspace wait behind an ended writer).

**What happened?**

A board user reassigned a task during a warm native run. Stop cancelled
the run with `issue_reassigned`, and the Stop acknowledgement was
recorded. The executor settled the coordinator to `terminal_failure`
with no owner. About eight seconds later, terminal release stopped and
retained the reusable Daytona sandbox. The lease became `released` with
`leasePolicy: reuse_by_environment`, a `stopped` termination receipt and
a stop-and-retain receipt. The task workspace stayed `dirty` with
`writerRunId` set to the cancelled run. No file recovery was recorded.
The new assignee's runs waited as `workspace_busy` for one hour. Then
each run failed `setup_failed`, and the task became blocked.

The heartbeat run row showed `nativePhase: "observed"`. That value is
stale: the checkpoint persist wrote it during teardown. The coordinator
itself was `terminal_failure`: the run's execution projection was
`recovery_needed` with no matching recovery action.

**Expected behavior**

The preservation worker records the stopped run's prepared files as file
recovery work. The lease keeps its release status and receipts. No
second stop and no new provider turn start.

**Steps to reproduce**

1. Cancel a native run with an acknowledged run-only Stop. Its remote
workspace reference is `prepared`, and the coordinator is
`terminal_failure` with no owner.
2. Release its reusable lease through stop-and-retain (`released`,
`reuse_by_environment`, confirmed stop receipt).
3. Run the cancelled-workspace preservation worker or the startup scan.
4. On master, the run has no `workspaceRestoreRecovery`, and no recovery
action exists.

## What Changed

-
`server/src/services/native-runtime/native-failed-workspace-recovery.ts`:
add one source check, `cancelledWorkspaceSource`, for cancelled
preservation. The worker check and the startup scan both use it. It
keeps the active and ephemeral cases. It adds a released reusable lease
only when the stop-and-retain receipt is confirmed
(`hasConfirmedSandboxStopAndRetain`) and `preparedRemoteWorkspaceSource`
proves the exact remote-workspace binding. This is the same proof the
restore recorder uses. Native-sync runs have no such proof and stay
ineligible. An ineligible source causes no run write or notification.
- The restore recorder (`preserveLegacyWorkspaceRestoreSources`) already
accepts this source. It checks the confirmed receipt and the exact
prepared workspace binding, and it keeps the lease status, policy and
receipts. This pull request does not change it.
- `native-cancelled-workspace-preservation.test.ts`: add the incident
shape as a regression for the worker and the startup scan, and add
safety cases.
- `doc/execution-semantics.md`: document the stopped reusable source and
the writer hold until recovery.

## Verification

From `server`:

```sh
npx vitest run src/services/native-runtime/native-cancelled-workspace-preservation.test.ts
```

- Red before the fix: the two new regression cases failed (`expected {
nativeCancellation: … } to match object { …, workspaceRestoreRecovery: …
}`). 30 other cases passed.
- Green after the fix: 36 passed.
- Review follow-up: the cases `invalid-receipt`, `expired-lease` and
`native-sync` failed on the previous head 1d461b4605. The scan or the
worker rewrote the run row. These cases pass now. Each case runs the
worker and two scans, then asserts that the run row and the lease row
are unchanged.
- Recovery-time fencing: after preservation,
`inspectWorkspaceRecoverySource` rejects with `workspace_writer_active`
while a task run is running. It rejects with
`allocation_ownership_ambiguous` when a later lease shares the
allocation.
- The regression checks: recovery recorded with the lease ID, run still
`cancelled` with `issue_reassigned`, lease row unchanged (status, policy
and receipts), one recovery action, recovery source inspection finds one
source, the worker disarms, and a repeated pass records nothing again
and creates no run or wake.
- Safety cases, all with no recovery record and unchanged run and lease
rows: a live coordinator owner, a `pending` (not acknowledged) Stop, a
lease without a stop receipt, an invalid receipt, an expired lease, a
native-sync run, and a `running` run.
- Affected suites were run locally: `native-failed-workspace-recovery`,
`native-workspace-reconstruction`, `workspace-recovery`,
`remote-workspaces.integration`, `native-environment-settlement-work`,
`server-startup-feedback-export`, `heartbeat/run-control`,
`heartbeat-remote-workspace-busy`. On a heavily loaded machine, some
cases timed out at 30 s. A rerun of those two files passed 135/135
tests; one file still reported a 30 s hook timeout. CI runs these
suites.
- `pnpm --filter @paperclipai/server typecheck` passes.
- Not done: a live staging run. The fix is not deployed.

## Risks

- Low. The new source applies only to a cancelled native run that has an
acknowledged run-only Stop, a terminal coordinator with no owner and no
accepted result, a prepared workspace reference, and a confirmed
stop-and-retain receipt for the same run, lease and allocation.
- Preservation records recovery work only. It does not stop the sandbox
again, change the lease policy, or grant execute rights. Recovery later
checks live owners and retired predecessors before it uses the sandbox.
- The writer hold on the workspace does not change. Successor runs still
wait until recovery saves the files, so unsaved files are not
overwritten. The one-hour cutoff stays.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code with tool use (shell, file edits, read-only browser reads of
staging diagnostics).

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 14:26:53 -06:00
DottaandClaude Opus 5.5 dd889e6a0b feat: show the assignee's provider usage limit in task properties (#19270)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents run on the native Runner through the Codex app-server and the
Claude ACP harness
> - Both harnesses report the provider account's rate-limit windows:
Codex sends `account/rateLimits/updated`, and Claude sends its SDK
`rate_limit_event` as `usage_update._meta["_claude/rateLimit"]`
> - Paperclip dropped both signals, so an operator could not see that an
agent was near or past its provider limit
> - This pull request normalizes both signals into one account-scoped
snapshot, keeps the latest snapshot per agent, and shows it as one
"Usage limit" line in the task properties pane
> - The benefit is that an operator sees "5h 78% · resets 15:40" (amber
above 80%, "Limited until …" when the provider refuses work, "using
credits" when a used-up window falls back to credits) next to the
assignee, and can click it for every window

## Linked Issues or Issue Description

Refs #19246
Refs #19259

**Is your feature request related to a problem?**
Codex and Claude report provider rate-limit windows during a run.
Paperclip dropped them: the Codex notification fell through the Rust
normalizer with no branch, and ACPX dropped Claude's `_meta`. Operators
could not see how close an agent was to its provider limit.

**Describe the solution you'd like**
Store the latest reported windows per agent and show one compact line in
the task properties pane, with a popover that lists every window.

**Describe alternatives you've considered**
A full gauge in the agent list and the run header. A design review chose
one property line with a details popover instead.

## What Changed

- Runner (Rust, the runnerd path): new `provider_rate_limits.rs`. Codex
`account/rateLimits/updated` becomes a `provider.notice.recorded` event
(category `provider.rate_limits`, scope `account`). `CodexProviderState`
merges sparse updates and emits a notice only when the windows change.
ACPX `usage_update` with a sanitized `rateLimit` field also emits the
notice.
- Runner (sidecar): `acpx-runtime-sidecar.ts` forwards only `status`,
`rateLimitType`, `utilization` and `resetsAt` from
`_meta["_claude/rateLimit"]`. This needs the ACPX `_meta` passthrough
from #19259. Until that merges, Claude emits nothing.
- Runner (TypeScript in-process drivers): the same mapping in
`codex-session-notifications.ts` and `codex-acpx-driver.ts` through
`drivers/provider-rate-limits.ts`.
- Shared: `provider-rate-limits.ts` holds the snapshot type, the parser,
the merge, and `summarizeProviderRateLimits` (fullest current window,
expiry by reset time, limited versus using credits).
- Server: `agent-provider-rate-limits.ts` merges each notice into
`agent_runtime_state.state_json.providerRateLimits`, scoped by company
and agent. A session reset keeps the snapshot. No migration.
- UI: `AgentRateLimitPropertyValue` adds a "Usage limit" row under the
assignee in `IssueProperties`. Transcripts treat the notice as run-log
only. A run status event refreshes the assignee's runtime state.
- Storybook: `Tasks and conversations/Task creation and lifecycle/Usage
limit property`.
- Docs: `doc/run-log-events.md` describes the notice and its storage.

## Verification

- `cd packages/paperclip-runner/runner && cargo test -p
paperclip-runner-core --lib rate_limit` and `--test
acpx_provider_events`: sparse Codex merge, dedupe, reached window,
checkpoint round trip, Claude utilization and rejection, notice shape.
- `cd packages/paperclip-runner && npx vitest run
src/drivers/provider-rate-limits.test.ts src/drivers/codex`
- `cd packages/shared && npx vitest run
src/provider-rate-limits.test.ts`: summary, expiry, limited, using
credits, parse and merge.
- `cd server && npx vitest run
src/__tests__/agent-provider-rate-limits.test.ts`: create and merge,
out-of-order snapshot, company isolation, unrelated notices, session
reset keeps the snapshot.
- `cd ui && npx vitest run src/components/transcript
src/components/issue-properties src/context/LiveUpdatesProvider`
- `pnpm -r typecheck` and `pnpm check:token-gates` pass.
- Live test on a local worktree instance: a native Codex agent on a
ChatGPT subscription login ran a real task. The runner recorded one
`provider.rate_limits` notice. The server stored it. The task properties
pane showed "7d 100% · using credits" (this account's weekly window was
used up and had credits, and the run succeeded). The transcript showed
no extra row.
- Not verified live: Claude. The local Claude CLI was not signed in, and
the Claude path also needs #19259. The Rust and sidecar paths have
tests.
- Not run locally: the full `pnpm test:run` and `pnpm build`. CI runs
them.

## Risks

- Low risk. The snapshot is display data. It does not hold or start
work. The existing provider-limit wait (#19246) still reads typed run
failures.
- Codex can send a rate-limit update often. The runner emits a notice
only when the windows change, so run-log growth stays small.
- With API-key auth, the harnesses report no windows, so no row appears.
This is the intended result.
- `CodexProviderState` gains two optional serde fields with defaults, so
older checkpoints still load.

> 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 (shell, file edits, browser) 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-05 20:26:51 +00:00
Nicky LeachandPaperclip 31bf6e4482 Pin plugin Runner cleanup to its physical task (#19300)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Plugin Runner owners manage a physical task through an environment
lease.
> - The existing replacement path can assign a new physical task to that
same lease.
> - A delayed old owner can then stop the replacement or mark it
stopped.
> - Cleanup must keep the original physical task identity across RPC and
database writes.
> - This pull request pins that identity and keeps cleanup available to
the new owner.

## Linked Issues or Issue Description

Refs #19134 and #19157. This fix is based on master and does not enable
repeated stopped-source recovery. A search found no duplicate fix.

**What happened?**

The maintained `createOwnedSandbox` replacement path changes
`providerLeaseId` on one lease. An old task owner uses that mutable
lease for a later Stop. A stopped-state response already in flight can
also update the new task's flags.

**Expected behavior**

The old owner must address its original task. Its late cleanup writes
must not alter the replacement. The replacement's own owner must still
stop and complete it.

**Steps to reproduce**

1. Create a plugin task owner for a stopped task.
2. Replace that task through the existing describe/startup path on the
same lease.
3. Resume an old checkpoint or a held old status response.
4. Observe a Stop for the replacement or an incorrect stopped flag
before this fix.

## What Changed

- Require each task owner to keep the physical ID returned by its
description. Reject a rebind to a different task.
- Carry the ID through the existing internal task context after startup
ends. Reject a changed physical task before RPC dispatch.
- Match the physical task in stopped-state, completion and
credential-handoff writes.
- Hold the exact lease row through the retained-session database update
and local checkpoint save.
- Cover the maintained replacement path with real PostgreSQL tests.
Preserve positive cleanup for the replacement owner. Document this
boundary.

## Verification

- Both replacement regressions failed on unmodified master `10a7d039c8`:
delayed old-owner Stop targeted the replacement, and a held old status
response marked it stopped.
- Six focused files passed 160 tests: task authority, task runtime, warm
tasks, execution target, credential writeback and task owner.
- Four selected executor factory cases passed. They cover task-only
providers and the physical task binding after the startup context ends.
- Runner TypeScript, plugin SDK and adapter-utils builds passed. Server
TypeScript check passed. Diff and added-line secret scans passed.
- All 15 setup-probe cases passed with the actual Runner and fake
provider binaries. The first local attempt lacked cargo on PATH. A
sandboxed native attempt timed out in seven process cases; the unchanged
run with loopback permission passed all 15. A separate initial DB
integration attempt was denied a loopback listener; its
permission-correct run passed.
- Review found three missed constructors in the existing SDK/Core
lifecycle fixture. They now use the actual describe result. All four
cases in that real PostgreSQL and worker RPC file passed.
- The first GitHub attempt failed in the unchanged GitHub recovery
pagination case in `chat-channels.integration.test.ts`: its zero-request
assertion observed a configuration GET. That exact case passed locally
without code or assertion changes. The one failed-job-only retry passed;
the initial failure log is retained.
- [GitHub workflow
42082587576](https://github.com/paperclipai/paperclip/actions/runs/42082587576)
passed on its second attempt. All CI checks completed, and Snyk passed.
Standard Greptile scored exact head `fb914d98b99e` 5/5 with no
unresolved threads. The branch is mergeable and ready for review.

## Risks

The task context change is internal. It adds no plugin wire field,
schema or Runner mode. A stale owner now gets an error and cannot clean
up a replacement. A new owner can still clean up that task. The retained
checkpoint write holds one existing lease row during a bounded local
file write.

Repeated stopped-source incarnation admission, runtime pins and
deployments are outside this change.

## Model Used

OpenAI GPT-6.1 Sol (`gpt-6.1-sol`), high reasoning. Used repository
tools, TypeScript and real PostgreSQL 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 described the issue in-PR using the bug template
- [x] I have not referenced internal or instance-local Paperclip issues
or links
- [x] My branch name describes the change
- [x] I have run the focused tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation
- [x] I have considered and documented risks
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no unresolved findings
- [x] I will address reviewer comments before requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-05 12:14:09 -08:00
Devin FoleyandClaude Opus 5.5 8aa28940bf fix(sentry): keep a live-heartbeat native capacity retry out of Sentry (#19320)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The server sends unexpected run failures to Sentry, and it keeps
known, recovered failures local
> - A native Codex "model at capacity" failure gets an automatic retry.
That failure must stay out of Sentry while its retry owns recovery
(#18295, #19051)
> - The check reads the `nativeProviderFailure` diagnostic from the
failed run's `resultJson`. On the live heartbeat path, the failed-status
write replaces `resultJson` with the adapter's copy, and that copy has
no diagnostic
> - So every live-path capacity failure with a scheduled retry still
goes to Sentry. Only the reconciliation path, which the tests cover,
kept the failure local
> - This pull request removes the diagnostic check. The scheduled retry
run is the proof
> - The benefit is that recovered capacity failures no longer create
Sentry noise on the normal live path

## Linked Issues or Issue Description

Refs #18295
Refs #19051

**What happened?**
A native Codex run failed with `native_provider_overloaded`. The
committed status decision scheduled the automatic capacity retry, and
the retry run existed before the report. The run failure still went to
Sentry. In a staging sample, every live-path capacity failure since
#19051 was reported (1 of 1).

**Expected behavior**
When the `native_provider_overloaded` retry run exists for the same
task, the failed run does not go to Sentry. The retry reports its own
outcome.

**Steps to reproduce**
1. Run a native Codex agent through the live heartbeat path
(`finalizeNativeRun` without `projectRunStatus`).
2. Let the provider return `serverOverloaded`. The status decision
schedules the capacity retry.
3. The heartbeat writes the terminal `failed` status with the adapter's
`resultJson`, and `emitTerminalAgentTaskRun` calls `reportRunFailure`
with that row.
4. The row has no `nativeProviderFailure`, so
`isScheduledNativeCapacityRetry` returns false and the event goes to
Sentry.

**Relevant logs or output**
The failed run row later shows `nativeProviderFailure:
codex/server_overloaded`, because a later reconciliation write restores
it. The row that the heartbeat write reports does not have the field.
The same overwrite also explains the missing `finalizationReasonCode`
that #19051 fixed.

## What Changed

- `server/src/services/run-failure-report.ts`:
`isScheduledNativeCapacityRetry` no longer requires
`resultJson.nativeProviderFailure`. The run must still be native and
failed, carry `native_provider_overloaded`, have a native task, have no
signal, and not be in setup. The same-task successor with
`scheduledRetryReason: native_provider_overloaded` is still required.
- The successor is enough proof. Only the committed capacity status
decision creates that successor, and only `forTerminal` in
`native-provider-failure.ts` produces `native_provider_overloaded`
together with the typed `codex`/`server_overloaded` diagnostic.
- `run-failure-report.test.ts`: a new case keeps the failure local when
the row has only the adapter's `resultJson` (no diagnostic, no
finalization fields) and the successor exists. The "diagnostic is
missing" case now reports only when no retry run exists. The case for a
non-capacity diagnostic with a capacity successor is removed, because
that state cannot occur.
- `doc/observability.md`: the native capacity paragraph now says that
the successor is the proof and that the run's own `resultJson` is not
required.

## Verification

- `pnpm --filter @paperclipai/server exec vitest run
src/services/__tests__/run-failure-report.test.ts`: 284/284 pass with
this change.
- With the `master` version of `run-failure-report.ts`, the new case
fails: 1 failed, 283 passed. So the test catches the bug.
- `native-provider-failure.test.ts`: 57/57 pass.
- `native-provider-overload.integration.test.ts` did not load in my
sandbox, because `@paperclipai/plugin-sdk` was not built. CI must run
it.

## Risks

- Low risk. The change only removes one condition from a Sentry filter.
- These failures still report: no retry run, exhausted or held retries,
other retry lanes, a successor for a different task, legacy runs,
signals, timeouts, setup failures, and unreadable retry evidence.
- The change does not affect run status, retries, recovery, or the `all`
reporting mode.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), agentic coding with tool use
(shell, repository reads, 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-05 12:04:33 -08:00
Devin FoleyandPaperclip 8464a8c9ae fix(native): settle a bootstrap retry that lost its task lock (#19307)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A native run that fails before any provider work gets a bounded
same-run bootstrap retry. Restart recovery decides whether that retry
can run.
> - The retry needs the task's issue lock. A user comment or another
wake can move that lock to a new run before the retry is due.
> - Restart recovery then returned `issue_execution_lock_changed`
without a write. The same check ran on every recovery pass and never
ended. The coordinator stayed `retryable_failure`.
> - The orphaned-lease sweep waits for a terminal coordinator before it
preserves the workspace. So the sandbox lease stayed active, and every
later run for the task was cancelled as `workspace_busy`.
> - This pull request gives a lock-lost bootstrap retry the same bounded
admission that a superseded remote retry already has.
> - The benefit is that the retry settles, the board gets a
reconciliation action, and the sandbox lease is released instead of
blocking the task.

## Linked Issues or Issue Description

No public issue exists. Bug description:

**What happened?**

On a hosted instance, a native agent run failed at startup because the
provider rejected `session.open` with a rate limit. Paperclip scheduled
a same-run bootstrap retry. Seconds later, a new run for the task took
its issue lock, most likely woken by a user comment. From then on, every
restart recovery pass for the old run returned `blocked` /
`issue_execution_lock_changed` and wrote nothing. The run recorded about
1,000 recovery events over 12 hours. Its coordinator stayed
`retryable_failure` with `recoveryMode: bootstrap_retry`. The scoped
orphaned-lease sweep found a pending workspace export for the lease. It
could not preserve the workspace, because
`preserveNativeFailedWorkspace` requires `terminal_failure`. So the
lease stayed `active`, and 16 later runs for the task were cancelled as
`workspace_busy`. Three more runs on the same instance were stuck in the
same state. A board Stop on the failed run moved the coordinator to
`terminal_failure`, and the sweep then released the lease within about a
minute.

**Expected behavior**

A bootstrap retry that cannot get its task lock back settles within a
bounded number of recovery passes. The coordinator reaches
`terminal_failure`, the board gets a reconciliation action, and the
existing orphaned-lease sweep can preserve the workspace and release the
sandbox lease.

**Steps to reproduce**

1. Fail a native remote run before provider work, so its coordinator is
`retryable_failure` with `recoveryMode: bootstrap_retry`.
2. Move the task's `executionRunId` to a newer run for the same task, or
clear it.
3. Run `claimNativeRestartRecoveries` for the failed run.
4. Before this change, it returns `blocked` with no `retryAt`, and the
coordinator stays `retryable_failure` on every pass. The new tests in
`native-remote-retry-admission.test.ts` reproduce this.

**Paperclip version or commit**

Observed on a hosted deployment. Reproduced in tests on `master` at
`10a7d039c8e`.

Related: #19190 (a `session.open` model rejection no longer schedules a
doomed bootstrap retry), #19131 (preserve superseded observed
workspaces) and #19281 (release the leases of a cancelled legacy run).
None of them changes this admission path.

## What Changed

- **Restart recovery
(`server/src/services/native-runtime/native-restart-recovery.ts`)**: a
new condition, `lockLostBootstrapRetry`, matches a failed run whose
coordinator is `retryable_failure` with `recoveryMode: bootstrap_retry`
and whose issue lock is not on the run.
- A newer run for the task: the existing superseded check now covers
this case too. It defers with `native_retry_source_superseded` as
permanent, so admission is exhausted at once.
- No newer run (the lock was released): recovery defers with
`issue_execution_lock_changed` through the existing `deferRemoteRetry`.
That path persists a deadline, backs off and exhausts after the system
recovery budget.
- When admission is exhausted, the existing hold path writes
`terminal_failure` / `native_restart_recovery_blocked`, keeps the
provider evidence and creates the board reconciliation action. This PR
adds no new terminal path.
- A bootstrap retry that still holds its own lock is unchanged. The
exact-checkpoint remote retry path is unchanged.
- **Docs (`doc/execution-semantics.md`)**: the bootstrap retry section
now states the lock-lost rule.

## Verification

- New test `settles a %s bootstrap retry that lost its task lock instead
of re-checking forever` in
`server/src/services/native-runtime/native-remote-retry-admission.test.ts`,
for the `superseded` and `released` cases. The fixture is a genuine
pre-provider failure: a `session.open` rejection with no session
checkpoint, no runner checkpoint row and no provider evidence.
- On `master` without the fix, both cases fail: recovery returns
`blocked` without a deadline, and the coordinator stays
`retryable_failure`.
- With the fix, `superseded` settles on the first pass, and `released`
settles after `SYSTEM_RECOVERY_MAX_RETRIES` deferred passes. Each
deferred pass persists its `nextAttemptAt`. Both end in
`terminal_failure` with one board recovery action, and later passes open
no transaction.
- `npx vitest run
src/services/native-runtime/native-remote-retry-admission.test.ts` (from
`server/`): 92 passed.
- Related suites, each run on its own with exit code 0:
`native-ingress-bootstrap-retry`, `native-startup-retry`,
`native-scheduled-resume-lock`, `native-restart-recovery-isolation`,
`native-runner-restart-recovery`, `native-runner-restart-preparation`,
`native-runner-restart-diskless`, `native-warm-transition-restart`,
`native-controller-recovery`, `native-failed-workspace-recovery`,
`native-session-resumption`, `native-startup-cleanup-recovery`,
`heartbeat-observed-workspace-preservation`, `heartbeat/recovery`,
`runtime-selection`, `remote-workspace-migration` and
`native-session-executor` (1,149 tests). Two other suites each had one
local failure that this change does not cause:
- `heartbeat-process-recovery`: "redacts opaque environment-bound
credentials from Sentry diagnostics" fails the same way on this machine
without the change.
- `chat-channels.integration`: "settles a shared-endpoint
accepted-response backlog without retrying its own locks" failed once
under full-suite load. It passes alone, with and without the change.
- `pnpm run typecheck` in `server/`: passed.

## Risks

- Low to medium. The change only affects failed bootstrap retries whose
lock is not on the run. Before this change, such a retry could never
run, so no working path is removed.
- A released lock that comes back to the same run during the backoff
still resumes normally, because the condition reads the current lock on
each pass.
- When the retry settles, the task gets a board reconciliation action.
Before this change, the task was silently stuck behind `workspace_busy`.
- No migration and no API change.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with tool use
(shell, code search, test runs), extended thinking, and a read-only
investigation subagent for the root cause.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-05 11:45:54 -08:00
Devin FoleyandPaperclip 74b11014c6 fix(server): stop a queued answer from blocking saved-work recovery (#19318)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A native run can accept its result before the workspace copy-back
finishes. If a server restart interrupts the copy-back, the run keeps an
owner receipt for the server that stopped.
> - Another server takes over only after the cloud platform proves that
the old deployment stopped. After that proof, recovery checks that the
task has not moved on, so old files never overwrite newer work.
> - That check counted any queued run on the task as newer work.
> - The user's answer to the run's question queues the assignee's next
wake. That wake cannot claim while the saved run holds the task's
execution lock, so it waits for the copy-back.
> - Recovery waited for the wake and the wake waited for recovery. The
task stayed stuck, and nothing was logged.
> - This pull request ignores a later wake for the same assignee that
has never started, and logs waits that the worker stops retrying.
> - The benefit is that answering a question during an interrupted
copy-back no longer strands the task. Every other newer-work check still
applies.

## Linked Issues or Issue Description

Refs #15824 (added platform-verified takeover and these binding checks)
and #19111 (worker concurrency and ordering for the same copy-backs).
This pull request keeps the rest of #15824's fence unchanged.

**What happened?**

On a hosted instance, a server restart interrupted a copy-back after the
worker had written its owner receipt. The run had asked the user a
question, and the user answered. The answer's wake was queued. Every
later server got a valid stop proof for the old deployment and then got
`binding_changed` from the shared binding check: the only failing
condition was the queued answer wake. The worker treats
`binding_changed` as a final wait and returns without logging. The run
stayed `running` and the wake stayed `queued` for about 19 hours, across
many restarts.

**Expected behavior**

A wake that is waiting for the saved run does not stop the saved run's
recovery. Runs that could change the workspace still prevent takeover,
and the publication check still stops a copy-back if such a run appears.

**Steps to reproduce**

1. Accept a native result and leave a copy-back owner receipt from a
stopped deployment.
2. Queue the assignee's next wake on the same task, for example by
answering the run's question.
3. Let the reconciler or the workspace worker revisit the saved result
with a verified stop proof. On `master` the takeover returns
`binding_changed` and nothing is copied or committed.

The new database tests run these steps against PostgreSQL.

**Paperclip version or commit**

`master` at `03eb49053fe`.

**Deployment mode**

Cloud-managed instances with platform stop verification. The same
binding check also guards operator owner recovery, so self-hosted
operator recovery had the same deadlock.

## What Changed

-
`server/src/services/native-runtime/native-workspace-recovery-binding.ts`:
the newer-run check ignores a run when all of these hold:
  - It is `queued` and has never started (`startedAt` is null).
- It belongs to the run's agent, and that agent is the task's assignee.
  - It was created after the saved run.
  - Its wake reason is not `source_scoped_recovery_action`.

The run claim blocks such a run while the saved run holds the issue's
execution lock (`lockIssueExecutionClaim` in `heartbeat/queue.ts`). Runs
that have started, other agents' runs, recovery-action wakes and older
queued runs still prevent takeover.
-
`server/src/services/native-runtime/native-workspace-finalization-work.ts`:
log `runId` and `reason` when the worker stops retrying a wait
(`binding_changed`, `owner_unverified`). It does not log `owner_active`.
- `doc/native-workspace-finalization-recovery.md`: describe the
waiting-wake rule in the shared binding section.
-
`server/src/services/native-runtime/native-workspace-queued-wake-recovery.test.ts`
(new): the queued-wake tests, with a small fixture of their own. They
are in a separate file so `native-workspace-finalizer.test.ts` stays
under 1,000 lines.

## Verification

- Failing first. These tests in
`native-workspace-queued-wake-recovery.test.ts` fail against the
original check and pass with this change:
  - Operator owner recovery with an answer wake queued.
  - Automatic takeover with the wake still queued.
- Automatic takeover where the wake starts during copy-back. On `master`
takeover is refused before this point; with the change, publication is
stopped.
- The real reconciler, from `retryable_failure`, with a
stopped-deployment owner and a queued answer wake. It now copies once,
commits the saved result and leaves the wake queued with its claim no
longer blocked.
- The fence is kept: three new cases (a requeued wake that already
started, a recovery-action wake, another agent's queued run) still get
`binding_changed`. They pass both before and after the change.
- Changed test: "does not reopen an old result after a newer task
execution exists" now inserts a started run instead of a never-started
queued run of the same assignee. The queued case is now the new
operator-recovery test above.
- `cd server && npx vitest run
src/services/native-runtime/native-workspace-queued-wake-recovery.test.ts
src/services/native-runtime/native-workspace-finalizer.test.ts`: 2
files, 72 passed. With the original binding file restored, the 4
positive queued-wake tests failed and the 3 fenced cases passed.
- Related suites (finalization work, platform proof, export retry,
finalizer failure, recovery actions, owner-recovery routes, status
reassessment, scoped deadline work): 8 files, 499 passed.
- After rebasing onto `03eb49053fe`: finalizer and finalization-work
suites, 97 passed.
- `cd server && npx tsc --noEmit -p tsconfig.json`: pass.
- Not verified on a live instance yet. The diagnosis comes from that
instance's run, finalization, workspace-operation and recovery-action
rows, plus a direct stop-proof request that returned a valid receipt.

## Risks

- A copy-back can now finish while the assignee's wake is queued. The
same-process retry path already does this, because it does not run the
binding check. The takeover path now matches it for wakes that cannot
start yet.
- If some path starts such a wake despite the execution lock,
`assertHeld` sees a running newer run and stops before publication. The
window between that check and the write is the same as for the other
binding conditions.
- No schema migration.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`) through Claude Code, with tool use
(shell, file edits, test runs, read-only database and log queries). The
runtime does not expose the context window or reasoning configuration.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-05 11:45:16 -08:00
DottaandClaude Opus 5.5 7644045bc5 fix(server): remove a settle deadlock and three server test races (#19314)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The server Vitest shards gate every pull request
> - Three open pull requests (#19289, #19267, #19266) failed server
tests in code that they do not change
> - Each failure had a different cause. One cause is a real product
deadlock, and three causes are test races
> - Random CI failures slow every contributor and hide real regressions
> - This pull request removes each root cause. It does not add retries
or skips
> - The benefit is fewer false CI failures and one less deadlock in
external-agent settlement

## Linked Issues or Issue Description

Refs #19289, #19267, #19266

**What happened?**
Server tests failed at random on pull requests that did not touch them:

- #19289, job 127412331924: `dot-runner.test.ts` > "signed readiness,
native tools and finalization through Dot local Rust" failed with
`PostgresError: deadlock detected` on the run-event append (`select ...
from heartbeat_runs ... for update`).
- #19267, job 127427384499: `tool-access-service.test.ts` > "keeps an
organization audience fail-closed when empty replacement races with
member archive" received a widened grant instead of the 409 error.
- #19267, job 127410158948: `heartbeat-process-recovery.test.ts` > "does
not reap active adapter executions started by another heartbeat service
instance" failed with "Timed out waiting for adapter execution to
start".
- #19266, job 127404184242: `voice-session-store.integration.test.ts` >
"schedules phone reply retries at the saved rate-limit deadline without
early database scans" failed with a duplicate key on
`companies_issue_prefix_idx`.

**Root causes**

1. Dot deadlock (product bug). `settle` in `native-broker.ts` takes the
external admission guard. Then it updates the same assignment row in two
or more statements. PostgreSQL re-runs the foreign-key checks when a
transaction updates a row version that it wrote earlier, so the second
UPDATE takes `FOR KEY SHARE` on the heartbeat run. Execution admission
locks the issue and the run `FOR UPDATE` before it takes the guard. Thus
settle holds the guard and waits for the run, and admission holds the
run and waits for the guard. The run-event append can queue behind them
as a third participant, and the database can kill it. A local
reproduction with `log_lock_waits` showed the RI check: `SELECT 1 FROM
ONLY heartbeat_runs x WHERE id = $1 FOR KEY SHARE OF x`, during `update
dot_runner_assignments set status='settled'`.
2. Tool-access race (test bug). The empty replacement and the member
cleanup both waited for the first replacement to commit. Both waiters
woke at the same time and raced for the membership row. A replacement
that wins that race widens access before the cleanup, and the product
code documents that outcome as valid ("Whichever transaction wins is
then authoritative"). The test only expected the cleanup to win.
3. Heartbeat timeout (test bug). The run start includes real runtime
skill preparation for a new company. I measured 3.3 s to 5.5 s locally
before the adapter starts. The test gave this step a fixed 3 s budget.
4. Voice prefix collision (test bug). Each of about 110 fixture
companies used `V` plus seven random hex digits as its unique issue
prefix, so two companies could get the same prefix.

**Master:** These tests do not fail on recent master runs (the master
workflows do not run the server shards). The failures are
timing-dependent and are not specific to a branch.

## What Changed

- `server/src/modules/external-agents/native-broker.ts`: `settle` writes
the assignment in one UPDATE. This applies to both Dot and Muse. The
update sets the same columns under the same conditions as before.
- `server/src/__tests__/dot-runner.test.ts`: added "settles
accept/finish under the admission guard without waiting on a run that
admission holds". The test holds the run `FOR UPDATE` like admission
does, then runs settle, then takes the guard. Before the fix, settle
waits and PostgreSQL reports a deadlock. After the fix, settle
completes.
- `server/src/__tests__/tool-access-service.test.ts`: start the empty
replacement after the cleanup holds the membership row. Then wait until
the replacement is blocked by the cleanup. The test still proves that
the replacement re-checks the member status after the cleanup and fails
closed.
- `server/src/__tests__/heartbeat-process-recovery.test.ts`: wait for
the adapter to start. Fail at once if the run settles before the adapter
starts. The test timeout is now 30 s, because the start (3 s to 6 s) and
the existing 5 s settle wait came close to the 15 s default.
- `server/src/__tests__/voice-session-store.integration.test.ts`: each
fixture prefix includes a per-run sequence, so prefixes in one run are
always distinct and stay at 10 characters.

## Verification

- Red/green for the deadlock: with the `native-broker.ts` change
removed, both new tests fail with `deadlock detected`. With the change,
both pass.
- `npx vitest run src/__tests__/dot-runner.test.ts` (server): 57 passed.
- Before the fix, I ran the Dot Rust test 8 times with
`deadlock_timeout=50ms` and got the deadlock in about 1 of 8 runs. Two
further runs of 16 iterations each passed.
- `npx vitest run src/__tests__/tool-access-service.test.ts -t
"fail-closed when empty replacement races"`: 3 passed, run 4 times.
- `npx vitest run src/__tests__/heartbeat-process-recovery.test.ts -t
"does not reap active adapter executions"`: this failed locally before
the change (start took 3.3 s to 5.5 s). It passes after the change.
- `npx vitest run
src/__tests__/voice-session-store.integration.test.ts`: 132 passed.
- `npx vitest run src/__tests__/muse-control.test.ts`: 256 passed. This
file covers the Muse settle path.
- Not run locally: the full `pnpm -r typecheck` / `pnpm test:run` /
`pnpm build`. CI runs them. A server `tsc --noEmit` shows no errors in
the changed lines. The existing errors come from the unbuilt runner
package in this worktree.

## Risks

- Low risk. The settle change keeps the same column writes and
conditions, now in one statement. It removes a lock wait. It does not
add a lock.
- The other changes are only in tests.

## Model Used

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

## Checklist

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

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native agents discover reusable workspaces through
`list_workspaces`.
> - The tool returned every authorized workspace in one result.
> - A large inventory exceeded the durable command journal limit. The
result stayed pending, and session cleanup blocked the next turn.
> - This pull request limits discovery in SQL and returns explicit
continuation metadata.
> - Agents can inspect every authorized workspace without sending the
full inventory in one command.

## Linked Issues or Issue Description

Related: #15749 introduced the task workspace tools. No duplicate
pagination fix was found.

**What happened?**

A native workspace discovery call returned a multi-megabyte inventory.
Persisting its result failed with `command_payload_too_large`. The
preceding answer could still be saved, but cleanup retained the
unsettled tool call. A later turn failed before provider work with
`native_session_cleanup_quarantined`.

**Expected behavior**

Workspace discovery must return a manageable page. Agents must be able
to request the remaining authorized rows. Existing visibility checks and
journal integrity checks must stay effective.

**Steps to reproduce**

1. Create 2,000 task-selectable workspaces with ordinary repository
paths.
2. Call `list_workspaces` from an admitted native run.
3. On the old code, the real API regression returns 869,781 bytes. The
command journal allows about 510 KiB.
4. On the repaired code, follow `nextCursor` to inspect all rows. Pages
contain at most 100 rows and fit within 128 KiB.

**Paperclip version or commit**

Reproduced on source commit `7351a9d74f` before the repair.

**Deployment mode**

Isolated source checkout with real PostgreSQL and authenticated native
agent APIs. Hosted deployment and incident recovery remain separate.

## What Changed

- Validate workspace list limits and UUID cursors with a JSON Schema
pattern supported by strict validators. Return `workspaces`, `limit`,
`returnedCount`, `hasMore`, and `nextCursor`.
- Apply company, visibility, lifecycle, and cursor conditions before the
SQL limit. Order pages by workspace ID. Fetch one extra row to detect
continuation without a count query.
- Fit encoded pages within 128 KiB, including JSON escaping and Unicode.
If one summary cannot fit, return explicit string previews with
`truncatedFields`. The authorized detail API retains its full record.
- Keep existing unpaged summary callers on their array response and
recent-use order.
- Update the native tool descriptor, generated contracts, catalog
fixture, and execution documentation.
- Add regression coverage for large inventories, complete pagination,
stable pages, private and foreign rows, invalid inputs, and legacy
summary behavior. Add an opt-in two-turn live Codex acceptance check.

## Verification

- Red before repair: `task-workspace-api.test.ts -t "pages a large
authorized workspace inventory"` failed because the result was 869,781
bytes instead of below 128 KiB. The transport regression also failed at
2,284,671 bytes.
- Green after repair and review changes: 120 tests passed across the
workspace API, transport, routes, native authority, coordinator,
historical-question, and Codex vertical-slice suites. They cover
complete discovery, byte-sized pages, oversized individual summaries,
full detail retrieval, private/foreign/lifecycle exclusions, and strict
schema compatibility.
- Journal safety: two focused durable control-plane tests passed.
Oversized result rejection and lossless valid result delivery remain
effective.
- Tool catalog and reconciliation: 30 tests passed.
- `pnpm -r typecheck`: passed.
- `pnpm build`: passed again on final commit `bc10880da6`. Server
typecheck also passed after the review changes. The original affected
suites passed on the rebased first commit (31 tests), and the expanded
review regression set passed afterward (120 tests).
- Real provider acceptance passed again on final commit `bc10880da6`:
local macOS arm64, rebuilt native Runner, `gpt-6.1-sol`, high effort,
per-turn lifecycle, and 2,000 persisted workspaces. Both turns
succeeded. Persisted call inputs and results prove that the second page
uses the first result’s cursor, both calls succeed, their IDs do not
overlap, and the encoded results stay within 128 KiB. The second turn
kept the provider conversation and file. Both task locks were clear
after completion, and no recovered execution failure was recorded.
Duration: 166.92 seconds.
- Full local `pnpm test:run` stopped in the chat shard: 330 tests passed
and one Slack registration test failed with `socket hang up`. That exact
test passed in isolation without changes, and the final-head CI chat
shards passed. A broader local managed-Slack follow-up produced no
result after about eight minutes and was interrupted; it is not counted
as a pass. The original full-suite failure remains recorded.
- The first CI head failed native tool-schema compilation because strict
validators did not recognize `format: uuid`. The revised descriptor uses
a UUID pattern. The previously failing coordinator, native
vertical-slice, authority, and history suites now pass locally. The
stronger live cursor-receipt acceptance check passed. Final-head CI
completed on `bc10880da6`: 57 checks passed and five optional checks
were skipped, with no failures or pending checks. Greptile reviewed that
commit at 5/5; both prior findings are resolved.
- Dependency cost: paged discovery runs the existing visibility-scoped
summary query with an ID cursor and at most 101 returned rows. It adds
no count query, filesystem scan, provider call, or background worker.

## Risks

- Native `list_workspaces` now returns a page object instead of an
array. Individual summaries can be explicit previews when a single
record exceeds the byte budget. Its advertised descriptor and generated
contracts change together. Agents must follow `nextCursor` until null
for a complete inventory.
- Each page rechecks current visibility. Concurrent insertions,
deletions, or access changes require a fresh scan when a complete
point-in-time inventory is needed.
- No migration or journal-limit change. An existing cleanup quarantine
still requires normal recovery after deployment; this PR does not bypass
it.

## Model Used

OpenAI Codex, GPT-6 family, with reasoning, shell tools, code editing,
and test execution. The exact served model revision and context window
are not exposed in this session.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-05 13:24:52 -06:00
Devin FoleyandClaude Opus 5.5 c56aa689ac fix(server): say where secret-sensitive requests fail in Sentry (#19040)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The server sends unexpected 5xx errors to Sentry through the shared
Express error handler
> - Some routes carry credentials: chat setup, provider webhooks, MCP,
company observability settings, Muse, avatars, and similar routes
> - On these routes the handler replaces the error with a generic
`Secret-sensitive request failed` error, because the original name and
message can contain secrets
> - That replacement also removes every useful fact. The Sentry event
does not show the route, the status, the error type, or the throw site,
so a maintainer cannot find the cause
> - This pull request adds a small fixed-shape record to that generic
error. The record says where the failure came from, and it never copies
request data or error text
> - The benefit is that the next event on these routes points to a route
and a code location, while the privacy rule stays the same

## Linked Issues or Issue Description

**What happened?**
A hosted staging server reported one event with the message
`Secret-sensitive request failed`. The event had only the stack of the
sanitizer itself. It had no route, no status, no error type, and no
frame from the original error. The trace had no spans and no logs. A
maintainer cannot find the root cause from this event.

**Expected behavior**
The event keeps the generic message, so no secret leaves the process.
The event also tells the maintainer which secret-sensitive route family
failed, the HTTP method and status, the error type, a safe error code
when one exists, the matched route definition, and the original throw
site.

**Steps to reproduce**
1. Make a route handler throw on a secret-sensitive path, for example
`PUT /api/companies/:companyId/observability`.
2. Enable the backend Sentry DSN.
3. Look at the captured event. It shows only `Secret-sensitive request
failed` and the sanitizer stack.

**Paperclip version or commit**
`master` at e107a75755.

I searched open and closed pull requests for `Secret-sensitive`. I did
not find a pull request that changes these diagnostics.

## What Changed

- `server/src/middleware/http-log-policy.ts`: add
`secretSensitiveRouteFamily()`. It returns a fixed label for the policy
branch that matched, or `null`. `isSecretSensitiveHttpRequest()` now
calls it. The matching rules do not change; the combined `/mcp/runner`
and avatar pattern is split into two branches with the same rules.
- `server/src/middleware/secret-sensitive-request-error.ts` (new):
`SecretSensitiveRequestError`. It keeps the generic message, `name:
"Error"`, its own stack, and default grouping. It adds frozen
diagnostics:
  - `routeFamily`: from the closed list above.
  - `method`: one of the standard HTTP methods, or `other`.
  - `status`: the 5xx `HttpError` status, or 500.
- `errorKind`: `http_error`, `type_error`, `range_error`,
`reference_error`, `syntax_error`, `error`, or `non_error`.
- `errnoCode`: only a key of `os.constants.errno`. `databaseErrorCode`:
only a five-character SQLSTATE. Both are read from own data properties
of the error and at most three causes. Getters are not called.
- `routePattern`: the matched Express route definition
(`req.route.path`), only when it is a string in a narrow character set.
A RegExp route becomes `null`.
- `originFrames`: up to eight complete V8 frames from the original
stack. The extractor first removes the exact `name: message` header that
V8 writes. It rebuilds the header like `Error.prototype.toString` from
data properties only and requires `stack` to start with it, so a
multiline message can never pass as a frame. If the header cannot be
identified, no frame is kept. Each remaining line must match a strict
frame pattern: a code-shaped function name and a location that may
contain spaces, Windows drive or UNC paths, or a percent-encoded
`file:///` URL.
- `server/src/middleware/error-handler.ts`: the sanitizer now returns
this error. The HTTP response, the request log context, and Telemetry do
not change.
- `server/src/sentry.ts`: `captureException` sends these diagnostics as
`secret_route_family`, `http_method`, `http_status`, `error_kind`,
optional `errno_code` and `db_error_code` tags, and a
`secret_sensitive_request` context. Run tags for agent requests stay.
The fingerprint stays `{{ default }}`. The module header documents the
new event fields next to the other Sentry privacy notes.
- `server/src/__tests__/secret-sensitive-request-error.test.ts` (new):
unit tests for each bounded field, the header-first frame extraction
(multiline-message canary, unidentifiable header, paths with spaces and
Windows paths), the route-family policy, and an end-to-end test through
a real Express app, the error handler, and the Sentry gate.

## Verification

- `cd server && pnpm vitest run
src/__tests__/secret-sensitive-request-error.test.ts
src/__tests__/http-log-policy.test.ts src/__tests__/sentry.test.ts
src/__tests__/error-handler.test.ts
src/__tests__/agent-commentary.integration.test.ts
src/__tests__/agent-profile-avatar.test.ts`: 6 files, 108 tests passed,
5 skipped.
- Red before green: with the old `sentry.ts` and the new test, the
end-to-end test fails because the event has no diagnostics tags. With
this change, it passes. The three frame-extraction tests added for
review feedback fail against the previous extractor and pass now.
- The end-to-end test puts a canary credential in the query string, the
JSON body, the error name, the error message, a cause message, and
`HttpError.details`. It checks that no Sentry argument and no reported
stack contains the canary. It also checks that the HTTP response stays
`{ "error": "Internal server error" }` with the original status.
- The existing tests in `error-handler.test.ts` for chat setup and
observability credentials still pass without change. They check that the
reported error, the error context, and Telemetry carry no canary.
- Server typecheck with `PAPERCLIP_TYPECHECK_LOW_MEMORY=1 node
scripts/typecheck-layers.mjs`: no error in server files. My sandbox had
only the server dependency subset installed, so the layers also reported
missing-module errors in `cli/`, `ui/`, and `packages/`. These errors
are not related to this change. CI runs the full check.
- Note for local runs: an empty `ANTHROPIC_BASE_URL` makes
`sentry.test.ts` fail at import on `master` too. Unset it before you run
the suite.

## Risks

- Low risk. Sentry stays opt-in, and the change affects only events from
secret-sensitive routes.
- Privacy: the new values are closed labels, bounded numbers,
source-code route definitions, Node errno names, SQLSTATE codes, and V8
frames that match a strict pattern after the message header is removed.
A frame contains a function name and a file location. In theory, code
can give a function a name from data. The frame pattern accepts only
identifier-like names of at most 120 characters, and the event never
includes the error name or message.
- If code changes an error's `message` or `name` after its stack was
formatted, the header no longer matches and the event has no origin
frames. This fails closed.
- `req.route` holds the last route that the router dispatched. If a
later middleware fails after a route calls `next()`, `routePattern` can
name that earlier route. The route family is still correct because it
comes from the URL policy.
- Grouping does not change. The reported error keeps the same type,
message, and sanitizer stack, so existing Sentry issues keep collecting
events.

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

## Model Used

- Provider: Anthropic. Model: Claude Opus 5.5 (`claude-opus-5-5`), used
through Claude Code with tool use (shell, file edits, test runs).
Extended reasoning was not explicitly enabled.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 11:14:00 -08:00
Devin FoleyandPaperclip a695398052 fix(server): preserve inactive rebase bookmarks in workspace snapshots (#19310)
Preserve a validated standalone REBASE_HEAD and its commit without changing the source workspace. Keep active Git operations, unmerged indexes, invalid markers and concurrent changes blocked.

Verified 156 affected tests, workspace typecheck and build. All hosted CI checks passed, and Greptile scored the exact head 5/5 with no unresolved threads. The bounded full local suite stopped during its fifth chat shard; this incomplete result is recorded in the PR.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-11-05 10:54:52 -08:00
DottaandClaude Opus 5.5 5de779848b fix(recovery): save a reassignment-stopped writer's files from its stopped reusable sandbox (#19304)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A remote task workspace stays held by its last writer run until that
writer's files are saved
> - A reassignment stops the old owner's run. The stopped writer still
holds the workspace with unsaved files.
> - When the run used a reusable (warm) sandbox, the stop leaves its
lease `released` under the reusable policy, with a stop-and-retain
receipt
> - Cancelled-workspace preservation only admitted `active` or
`ephemeral` sources. It never recorded the save for this stopped writer,
so no workspace recovery ran, and the hold stayed forever
> - The new owner waited, then failed setup after the one-hour cutoff
with "The workspace still has an unfinished writer or save operation"
> - This pull request admits the stopped reusable source, so Paperclip
saves the stopped writer's files and releases the hold. A waiter keeps
waiting while that save is in progress.
> - The benefit is that a reassigned task continues with the old owner's
saved work, and the new owner does not fail setup

## Linked Issues or Issue Description

Refs #19285 (parks a waiter behind an ended writer). Refs #19074,
#18820.

**What happened?**
A board user reassigned a task during a native run on a reusable
sandbox. The old run was cancelled with `issue_reassigned`. Its sandbox
was stopped and its lease was released under `reuse_by_environment`. The
workspace still named the stopped run as its dirty writer. No save
operation and no workspace recovery action were ever recorded for that
run. The new owner's runs were cancelled `workspace_busy` repeatedly.
After more than an hour, a new run failed setup: "The workspace still
has an unfinished writer or save operation."

**Expected behavior**
Paperclip saves the stopped writer's files from its stopped sandbox
within a bounded time and releases the hold. The new owner waits for
that save and then continues. If the save cannot finish, its retries end
with a named board recovery action.

**Steps to reproduce**
1. Give a native run a prepared remote workspace on a lease with policy
`reuse_by_environment`.
2. Stop the run (for example by reassignment), so the lease is stopped
and released with a stop-and-retain receipt.
3. Run cancelled-workspace preservation. Before this fix, it records
nothing, and the writer hold stays.

## What Changed

- `native-failed-workspace-recovery.ts`: Cancelled-workspace
preservation (the delivery worker and the startup scan) now admits a
`released` reusable lease with a stopped termination receipt and a
stop-and-retain receipt.
- `preserveNativeFailedWorkspace` retains such a reusable source only
with the exact confirmed stop-and-retain receipt, and only while the
workspace is still bound to that writer
(`preparedRemoteWorkspaceSource`). Otherwise it writes nothing.
- `remote-workspace-writer-hold.ts`: For an ended writer, the holder
cutoff now uses the latest progress of an in-progress save of that
writer's retained files (a workspace recovery that is not complete,
exhausted, or recovered). Reservation and the parked `workspace_busy`
wait both read this rule. A waiter waits for the save instead of failing
setup.
- `doc/execution-semantics.md`: Documents the stopped reusable source
and the save-progress cutoff.

## Verification

- Red before the fix, green after:
- `npx vitest run
src/services/native-runtime/native-cancelled-workspace-preservation.test.ts`
— new cases "preserves a reassignment-stopped writer's files from its
stopped reusable sandbox" (worker and startup scan) failed before the
fix: no `workspaceRestoreRecovery` was recorded.
- `npx vitest run src/__tests__/heartbeat-remote-workspace-busy.test.ts`
— new cases "keeps a waiter parked while an ended writer's save is in
progress" failed before the fix: the parked retry was released past the
cutoff.
- Safety cases:
- A reusable source without a confirmed stop, a workspace now owned by
another writer, and an already saved workspace are not retained, and the
run row does not change. (The writer and saved cases fail if the new
guard is removed.)
- Preservation does not replay the stopped run, does not create a wake,
keeps the stop receipt and lease policy unchanged, and keeps the writer
hold until the save publishes.
- An exhausted or complete save does not extend the cutoff. The existing
past-cutoff test (no save in progress) still fails setup and keeps the
held files.
- Related suites: workspace recovery, remote workspace reservation,
native workspace reconstruction, native failed-workspace recovery,
legacy workspace restore recovery.
- Not verified on staging: this change is not deployed. The staging task
needs this fix deployed. Then the preservation worker records the save,
workspace recovery saves the files, and the waiting owner continues.

## Risks

- More cancelled runs are now admitted for preservation. The retention
guard requires the exact stop-and-retain receipt and the workspace bound
to this writer, so a reusable sandbox that another run resumed or that
no longer holds unsaved work is not retained.
- A stopped reusable source stays `reuse_by_environment` while Paperclip
saves it. This matches the existing retention path for reusable native
sources.
- The writer-hold read adds one indexed query
(`issue_recovery_actions_source_run_idx`) for an ended writer only.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use (shell,
code editing, test 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
- [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-05 12:54:47 -06:00
Devin FoleyandClaude Opus 5.5 5d2fb8f737 fix(server): split run-failure Sentry groups that have no usable root code (#19308)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The server sends each unexpected terminal run failure to Sentry. A
custom fingerprint groups these events.
> - The fingerprint is `[errorCode, agentAdapter, failurePhase?,
rootErrorCode?]`. The root code is used only when it is an identifier.
> - Many setup failures have no identifier root code. Some have a
numeric JSON-RPC code (`-32002`). Some have only an HTTP status (`409`).
Some have no code.
> - All of these failures get the same fingerprint, `setup_failed /
paperclip_runner / setup`. One Sentry group now holds about ten
unrelated failure types: a controller lease loss, Daytona create
timeouts, sandbox resume timeouts, remote-workspace 409 errors, S3
throttling and more. The group title changes with each new event. A fix
for one cause cannot close the group.
> - This pull request adds a bounded fallback when there is no usable
root code. The fallback is the root exception class name plus its HTTP
status or numeric code.
> - The benefit is that each failure type gets its own Sentry group, so
a fix can be verified and resolved for each cause.

## Linked Issues or Issue Description

Refs #16359
Refs #16802
Refs #19156

**What happened?**
Unrelated `paperclip_runner` setup failures go into one Sentry issue.
The root exceptions include `LegacyControllerLeaseLostError`,
`JsonRpcCallError` with code `-32002` (Daytona create timeout),
`JsonRpcCallError` with code `-32003` (sandbox resume timeout), plain
`Error` with status `409` (remote workspace writer still pending), and
AWS `SlowDown`. None of them has an identifier root code, so the
fingerprint drops the root element.

**Expected behavior**
Failures with different root exceptions go into different Sentry issues.
Failures with the same root exception and status still group together.
The fingerprint does not contain message text.

**Steps to reproduce**
1. Report two setup failures for the `paperclip_runner` adapter with
error code `setup_failed`.
2. Give the first failure the root exception
`LegacyControllerLeaseLostError` with no code.
3. Give the second failure the root exception `JsonRpcCallError` with
code `-32002`.
4. Read the two fingerprints. Before this change, they are the same.

**Additional context**
Only events without an identifier root code get a new fingerprint.
Events with an identifier root code (for example `ECONNRESET`) keep
their current fingerprint and their current Sentry group.

## What Changed

- `server/src/sentry.ts`: when there is no identifier root code,
`captureRunFailure` adds fallback fingerprint elements from the deepest
recorded exception:
  - `exception:<Name>` when the name is an identifier.
- `status:<n>` when the exception has an HTTP status (already validated
as 100–599 by the diagnostics collector), or else `code:<n>` when the
code is a number of up to 10 digits.
- Names or codes with other shapes are not used, so message text and
secrets cannot go into the fingerprint.
- The `root_error_code` tag does not change.
- `server/src/__tests__/run-failure-sentry.test.ts`: new tests for the
fallback groups, for an identifier root code that keeps the current
fingerprint, and for rejected unbounded names and codes.

## Verification

- `cd server && npx vitest run src/__tests__/run-failure-sentry.test.ts
src/__tests__/run-failure-sentry-real-sdk.test.ts
src/__tests__/sentry-links.test.ts
src/services/__tests__/run-failure-report.test.ts`: 358 tests pass. The
real-SDK suite ran against `@sentry/node@10.71.0`.
- I reverted only `server/src/sentry.ts` and ran the new grouping test.
It failed, because all six fingerprints were the same. With the change,
it passes.
- `cd server && npx tsc --noEmit -p tsconfig.json`: no errors.
- `src/__tests__/sentry.test.ts` did not load in my local environment
(`AI_InvalidArgumentError: baseURL must be a non-empty string`). It
fails in the same way on unchanged `master`, so I depend on CI for that
suite.

## Risks

- Events without an identifier root code get a new fingerprint. Sentry
puts them in new issues. The old mixed issues stop getting these events.
This is the intended result. Alerts or saved searches that point to an
old mixed issue must move to the new issues.
- The number of groups increases by a small, bounded amount. Exception
class names, HTTP statuses and numeric codes have low cardinality. No
message, ID or free text goes into the fingerprint.
- No change to what the server sends in tags or contexts, and no change
to run control flow.

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

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with
tool use (shell, file edits, test runs, GitHub and Sentry connectors).

## 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-05 10:54:33 -08:00
phant0umandClaude Opus 5.5 c4171af889 fix(cli): skip POSIX key file mode check in doctor on Windows (#14308)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - `paperclipai doctor` checks the local install, including the local
encrypted secrets key file
> - The check reads the POSIX permission bits of the key file and warns
when group or others can read it
> - On Windows, `fs.statSync().mode` is always `0o666` for a writable
file, and `chmod` cannot restrict it
> - So on Windows the check always warns, and the suggested `chmod 600`
has no effect
> - The server secret provider health check has the same check and the
same false positive
> - This pull request skips the POSIX permission-bit check on `win32` in
both places
> - The benefit is that `doctor` and the secret provider health
endpoints on Windows no longer show a permanent false-positive warning

## Linked Issues or Issue Description

Fixes #14255

## What Changed

- `cli/src/checks/secrets-check.ts`: skip the key file permission-bit
check when `process.platform === "win32"`. Linux and macOS behavior does
not change.
- `cli/src/__tests__/secrets.test.ts`: add two tests. One test mocks
`process.platform` as `win32` and expects `pass` for a `0o666` key file.
One test pins the POSIX behavior: a `0o644` key file still gives `warn`
(skipped on Windows runners).
- `server/src/secrets/local-encrypted-provider.ts`: the same `win32`
skip in the local encrypted provider health check. This check feeds `GET
/api/companies/{id}/secret-providers/health`, `POST
/api/secret-provider-configs/{id}/health`, and the `validateConfig`
warnings.
- `server/src/__tests__/secret-provider-registry.test.ts`: add a `win32`
test that expects status `ok` with no warnings for a `0o666` key file.
Mark the existing `0o644` warning test `skipIf(win32)`.

## Verification

- `cd cli && npx vitest run src/__tests__/secrets.test.ts`: 10 of 10
tests pass on macOS (Node 22.22.3).
- Red before the fix: with the fix reverted, 1 failed and 9 passed. The
new Windows test fails with `expected 'warn' to be 'pass'`.
- `cd server && npx vitest run
src/__tests__/secret-provider-registry.test.ts`: 3 of 3 tests pass. Red
before the fix: the new `win32` test failed 2 times with status `warn`.
- Full CLI suite (`cd cli && npx vitest run`): the same 28 tests in
`doctor`, `install-command`, `update-command`, and `worktree` fail on
`master` without this change and with it. My local checkout has no
workspace builds, because I installed with `--ignore-scripts`. This
change adds 2 passing tests and no new failures.
- `cd cli && npx tsc --noEmit`: 233 errors on `master` and 233 with this
change. All errors come from unbuilt workspace packages. No error is in
`cli/src`.
- I did not run the check on a real Windows machine. The Windows test
mocks `process.platform`.

## Risks

- Low risk. The change only removes a warning on Windows. On Windows the
old check always fired, so it gave no signal.
- The repo already skips the same kind of check on `win32`:
`server/src/services/decision-signing.ts` returns early in
`assertOwnedByCurrentUser` and `enforceKeyFilePermissions` for its key
file.
- On Windows, `doctor` no longer checks key file access at all. NTFS
ACLs control access there, and the old check could not read them. An ACL
check is out of scope for this fix.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`, Anthropic), in Claude Code with
tool use (shell, file edit). It reproduced the bug with a failing test,
wrote the fix, and drafted this description. A human reviewed 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
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] I have updated relevant documentation to reflect my changes (no
documentation describes this warning)
- [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: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 12:35:30 -06:00
Devin FoleyandPaperclip 088f85c5fe fix: rename the managed environment to Paperclip Sandbox (#19296)
## Thinking Path

> - Paperclip manages AI agents and their execution environments.
> - Managed installations supply a default sandbox through managed
configuration.
> - Most selectors show the configured environment name.
> - A fallback label, two errors, and examples still use Paperclip
Computer.
> - This pull request changes that stock copy to Paperclip Sandbox.
> - Existing and custom environment names continue to come from the
stored row.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

The managed environment label in agent settings and the errors shown
when the managed sandbox is unavailable.

**Subsystem affected**

The agent configuration UI, runner selection, and environment
reconciliation tests.

**Current behavior**

Fallback copy and current examples call the managed sandbox Paperclip
Computer. The actual environment name is already configurable.

**Proposed behavior**

Use Paperclip Sandbox for stock copy and current examples. Keep
configured names unchanged in selectors, including legacy names and
custom names.

**Reason and benefit**

The name identifies this environment as a managed sandbox and removes
ambiguity.

**Breaking changes**

The displayed fallback and error text change. Error codes, execution
behavior, and environment identity remain the same. No migration is
needed.

**Additional context**

No duplicate rename issue or pull request was found. This is a copy
update, with no new roadmap feature.

## What Changed

- Update the managed fallback label and two unavailable-sandbox error
messages.
- Update current test and Storybook fixtures without changing story IDs
or behavior.
- Test configured names, fallback copy, and the unavailable-sandbox
message.
- Add a PostgreSQL regression test for a rename that retains the
environment ID, instance default, agent binding, environment variables,
and lease.

## Verification

- `pnpm -r typecheck` passed.
- `pnpm build` passed.
- Focused coverage: 956 cases across nine changed suites. The new
fixture initially tried to insert an existing singleton; its corrected
upsert passed with all 52 environment-service cases. The updated runner
suite also passed all 243 cases.
- The two UI suites passed all 189 cases, including configured
legacy/custom names and the missing-default fallback.
- `pnpm check:token-gates` passed.
- `node scripts/storybook-catalog.mjs --check` passed. The catalog is
structural evidence only.
- `node --test scripts/check-maps.test.mjs
scripts/storybook-catalog.test.mjs
scripts/storybook-feature-index.test.mjs` passed.
- Full `pnpm test:run` is in progress. No live provider or browser
journey is claimed by these tests.
- CI passed on the current head after one server-shard rerun. The first
attempt hit a 10 ms deadline assertion in the unchanged status-card
timing test; its isolated local run and the complete rerun shard passed.
The original failure and rerun are retained in the PR comment.
- Reviewed the diff for secrets, personal data, and run artifacts.

## Risks

Low risk: runtime edits change three display strings. Managed
reconciliation is unchanged. The regression test exercises the existing
rename against PostgreSQL. The configured name still controls normal
display, so this code change alone does not rename existing database
rows. A managed configuration update uses the existing reconciliation
path. Roll back the copy by reverting this commit.

## Model Used

OpenAI GPT-6.1 Sol (`gpt-6.1-sol`) through Codex in T3 Code, xhigh
reasoning, with repository tools 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, version, and capabilities
- [x] I have checked ROADMAP.md; this copy update does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs
- [x] I have described the issue in the PR with the enhancement template
fields
- [x] No internal or instance-local issue references
- [x] The branch name describes the change and contains no internal
ticket ID
- [x] Focused tests pass locally
- [x] Relevant tests were added or updated
- [x] Relevant copy and examples were updated; no API or schema
documentation changed
- [x] Risks are documented
- [x] All CI gates passed on the current head, including the server
11/12 rerun and aggregate verify
- [ ] Full local test suite completed (still running; six chat shards
passed)
- [x] Greptile is 5/5 on the current head with no unresolved findings
- [x] I will address all reviewer comments before requesting merge

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-05 10:23:43 -08:00
DottaandPaperclip 7351a9d74f fix(native): restore retired backups after warm lease handoffs (#19299)
## Thinking Path

> - Paperclip manages AI agents and keeps their work across turns.
> - A native session can keep the same sandbox through several warm
lease handoffs.
> - Each handoff keeps the old lease row and records matching receipts
on both owners.
> - The retired-backup guard required exactly one row for a provider
allocation.
> - That rule rejected valid history after Paperclip destroyed the
sandbox.
> - This change uses the existing handoff proof to select the final
owner.
> - A verified backup can then restore without discarding ownership
checks.

## Linked Issues or Issue Description

Refs #19208.

**What happened?**

A follow-up received a new sandbox after its old sandbox was destroyed.
The old allocation had a normal warm lease handoff. Both lease rows
remained in PostgreSQL. The guard rejected them with
`runner_harness_state_mismatch: backup_without_reusable_lease`. Its
later same-run retry failed with
`native_remote_recovery_lease_mismatch`.

**Expected behavior**

Restore the verified backup when one final owner has a valid destroyed
receipt and every earlier owner has a matching handoff receipt. Keep
uncertain ownership blocked.

**Steps to reproduce**

1. Complete a native warm turn.
2. Hand the reusable sandbox to another turn.
3. Complete that turn and save its backup.
4. Close the workspace and destroy the sandbox.
5. Send a follow-up. The new allocation must restore the saved session.

**Paperclip version or commit**

Reproduced in the PostgreSQL regression on the parent of this change.
The affected deployed build also contains #19208.

## What Changed

- Select the single final lease owner. Validate all predecessor rows
with `retiredReusableLeasePredecessors`.
- Keep the destroyed receipt, exact backup stamp, and
newer-provider-work checks.
- Add database regressions for one and three handoffs. Reject missing or
changed receipts, an active predecessor, missing history, an unrelated
owner, a stopped source, another backup, and later provider work.
- Document the valid lease-history shape.

## Verification

- Red before the source change: the two valid-handoff cases returned
false. The other 16 database cases passed.
- Green after the change: all 18 database cases passed. They also passed
on current head `d16e098b8c2e6c0dda70984c1df21e3dc792ab34`.
- Command: `pnpm --filter @paperclipai/server exec vitest run
src/services/native-runtime/native-retired-backup-source.integration.test.ts`.
- The database, executor, retry-admission, and lease suites passed: 4
files, 1,308 tests.
- `pnpm -r typecheck` and `pnpm build` passed. The build stamp names
head `4ee733c3a979436196522f36537ff182e9df5398`. The full local suite
stopped in chat shard 3 after a 15-second timeout in `materializes
direct external-chat finals once and accepts the next turn without agent
bookkeeping writes`. Earlier shards passed 702 cases; that shard passed
303 other cases. The isolated test passes on this branch and unchanged
master `10a7d039c8efeac0fd655c24970df5ae3658414b`. All current-head CI
chat shards also passed. The earlier full-suite attempt remains failed;
its timeout cause is not established. No test or timeout was changed. CI
is waiting for its last Runner job.
- Current head `d16e098b8c2e6c0dda70984c1df21e3dc792ab34` removes four
comment lines from the previously tested head. Its 18 database cases
passed. Greptile reviewed this head at 5/5 with no actionable findings:
https://github.com/paperclipai/paperclip/pull/19299#issuecomment-6665446098.
CI remains pending.
- Live inspection found the matching predecessor/successor receipts and
the final owner's destroyed receipt. This was read-only evidence from
the affected build.
- A live provider run at the fixed revision has not been exercised.
Local Daytona credentials and an immutable test image are unavailable.
No deployment or original-task retry is claimed.
- Initial wider tests could not resolve the unbuilt plugin SDK. The SDK
build is now available. A sandboxed database launch could not bind
loopback; the same regression passed with the disposable database
outside that restriction.

## Risks

- A wrong owner could allow an old backup to rewind a session. The
shared validator must account for every lease row and match both sides
of each handoff.
- Live, unrelated, incomplete, or conflicting history stays blocked. The
existing backup integrity and provider-progress checks remain.
- No migration, permission, model, trust, timeout, or retry-policy
change is included.
- The original task still needs the fixed build and normal recovery.
Hosted acceptance remains unverified.

## Model Used

OpenAI Codex, based on GPT-6, with reasoning, code execution, and tool
use. The session does not expose the exact 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
- [ ] 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-05 12:20:10 -06:00