Commit Graph
7246 Commits
Author SHA1 Message Date
DottaandClaude Opus 5.5 7a93cc6215 fix(server): redact invitation tokens from agent run logs (#20360)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - People join a company through an invitation link. The link carries a
bearer token in its URL path.
> - An agent can create an invitation in chat and give the person the
join link.
> - The server writes each agent run to a run log file (`.ndjson`). This
file is a plain diagnostic copy. Local agents run as the same OS user,
so they can read it.
> - #20343 removed invitation tokens from HTTP logs. The run log still
kept the full join link and the raw token.
> - This pull request removes invitation tokens from run-log chunks when
the server writes them.
> - The benefit is that a live join link does not stay in a file that
other local agents can read. The person still gets the working link in
the chat reply.

## Linked Issues or Issue Description

Refs #20343 (same token policy for HTTP logs).

**What happened?**

An agent created an invitation in chat and replied with a
`/invite/<token>` join link. The agent run log file under
`data/run-logs/` kept the full token. The token was in the agent message
and in the tool result.

**Expected behavior**

The run log keeps the route and drops the token:
`/invite/***REDACTED***`. A raw `pcp_invite_...` token also becomes
`***REDACTED***`. The chat reply that the person reads keeps the working
join link.

**Steps to reproduce**

1. Check out master.
2. Run `server/src/services/heartbeat/run-log.test.ts` from this PR.
3. The "run-log invitation token privacy" tests fail. The token stays in
both the legacy text chunk and the canonical native envelope.

**Paperclip version or commit**

Master at the merge base of this branch.

**Deployment mode**

Source server, local run logs (`local_file`). The same functions also
format object-store run logs.

## What Changed

- Add `redactInvitationTokens()` in
`server/src/middleware/invitation-token-redaction.ts`, next to the HTTP
log redactor from #20343. It removes the token from `/invite/<token>`
segments and from raw `pcp_invite_` tokens inside free text and JSON
text. The character classes stop at JSON, Markdown and URL delimiters,
so the record structure stays valid. `redact-sensitive.ts` stays
byte-identical, because the chat eval pins its exact bytes before it
accepts a real invitation link.
- Apply it in `compactRunLogChunk()` and
`redactCanonicalNativeRunLogChunk()`. These are the two functions that
format every agent run-log chunk before the store writes it. The run
excerpt columns use the same chunk, so they are also redacted.
- `canonicalNativeRunLogRedactionForms()` also yields the earlier,
unredacted projection. A run log written by an earlier release still
verifies after an upgrade.
- `restoreCompactedEvent()` accepts a truncated record that the earlier
compactor wrote with the token. Cold recovery of such an old log still
works.
- Add a test that proves the chat reply comes from database run events,
not from the run log. The reply keeps the full join link, and the
presentation path never reads the run log.

## Verification

- From `server`: `node ../node_modules/vitest/vitest.mjs run --config
vitest.config.ts src/services/heartbeat/run-log.test.ts
src/__tests__/invitation-token-redaction.test.ts
src/__tests__/redact-sensitive.test.ts
src/__tests__/heartbeat-run-log.test.ts
src/__tests__/logger-invitation-redaction.test.ts
src/services/native-runtime/native-run-log-watermark.test.ts
src/services/native-runtime/native-run-log-recovery.test.ts
src/__tests__/heartbeat-run-output.test.ts
src/services/native-runtime/agent-chat-response-recovery.test.ts`
passes.
- Before the `run-log.ts` change, the two new redaction tests in
`run-log.test.ts` failed (red), then passed (green).
- `native-run-log-watermark.test.ts`: a legacy-compacted record with a
token in its retained head cold-recovers. It failed with
`native_log_source_coverage_unproven` before the restore fix.
- `agent-chat-response-recovery.test.ts` (embedded Postgres): a final
reply with a join link is saved as the exact comment with the token. No
run-log read happens.
- `tsc --noEmit -p server` shows no errors in the changed files.
- A live chat eval (`access.invite-teammate`) run with this change
confirms the run-log file has no token. The run is linked in a comment.

## Risks

- Low risk. Run-log readers (run transcript view, feedback export,
support inspection) now show `***REDACTED***` in place of invitation
tokens. This removes a live credential from a diagnostic copy. It does
not change the chat reply or the stored run events.
- A path segment after `/invite/` in free text is always redacted, even
if it is not a token. This loses only diagnostic detail.
- A token that a provider streams across two separate chunks is not
joined first. Each chunk is redacted on its own. A complete token in one
chunk, such as a final message or a tool result, is removed.

> 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`), with tool use and code
execution.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 17:52:19 -06:00
DottaandPaperclip b09f17fbfd fix(agents): repair queued setup through task connection cards (#20361)
## Thinking Path

> - Paperclip manages AI agents doing work for people.
> - Agent verification must finish before an accepted task can start.
> - A missing AI account default can prevent verification before a
provider starts.
> - Lifecycle policy treated that owned configuration refusal as a
temporary failure and retried each minute.
> - The accepted turn remained Queued without explaining the
prerequisite.
> - Park account configuration failures and reuse the task's existing
inline connection card.
> - Successful repair rechecks verification and releases the original
queued turn under its original authority.

## Linked Issues or Issue Description

**What happened?** An OpenCode agent selected the responsible user's
OpenRouter default, but that default did not exist. Verification retried
unchanged configuration indefinitely and its accepted task stayed
Queued.

**Expected behavior:** Preserve accepted work, explain the setup
prerequisite, and let the appropriate person repair it using the
existing task connection UI. Completion must verify the agent before
starting the same queued turn. Genuine pause, approval and access gates
remain effective.

**Reproduction:** Create an OpenCode Runner agent with a
responsible-user OpenRouter binding and no personal default; assign a
task while verification is pending. The base retries verification and
shows an unexplained queue.

Related: #18702 handles pending task connection recovery after account
repair. This PR handles the earlier agent-verification boundary. #20330
addresses a separate Codex governed-wait timeout and is already merged.

## What Changed

- Share owned account-selection failure evidence through a pure library
contract. Lifecycle policy parks those configuration failures with
fixed, redacted guidance; temporary infrastructure errors retain their
retries.
- Batch setup-state reads for unstarted queued turns. Distinguish manual
repair from automatic setup retries and preserve Stop, started/history
state and lifecycle holds.
- Reuse the existing `connection_intent` card, account selection and
completion APIs. Its controller-only `agentSetup` marker accepts only
never-started legacy turns; runtime tokens still require a running run.
- Address the card to the lifecycle verifier's account owner, preserving
the task's execution principal and private-task visibility. Attach
missing cards on admission, verification, startup and full recovery,
including retained retries and continuations. Repeated delivery and
publication remain bounded. A new queued turn supersedes the setup card
of a stopped turn; generic connection cards retain cross-run reuse.
- Recheck lifecycle verification after usable-account completion, then
release the existing turn through the normal readiness path. Preserve
decline, Stop, reassignment and changed setup-owner gates. Fence an
in-flight verification failure from overwriting a repair; add no
provider wake or recurring worker.
- Park owned account refusals during resume as well as initial
verification. Bind setup cards to the lifecycle operation, settle
obsolete deliveries, and keep explicit Retry guidance accurate.
- Reuse the router’s existing lifecycle pin for setup repair without
rerouting the task, advancing the pool cursor, or replacing its saved
binding. Legacy direct-account cards remain readable.
- Hide the incorrect Resume banner during setup, retaining genuine
manual/budget pause controls. Document the execution and retry contract.

## Verification

- Red: eight original behavior assertions failed on the base, including
the real selector's retry decision and the real batched queue
projection. Six owned selection cases failed classification.
Temporary-error presentation, different task/verifier users, retained
retry/continuation recovery and the setup Resume banner also have
failing-before/passing-after regressions.
- Green: 430 affected backend, lifecycle, shared-contract and UI tests
passed before the final review corrections. Subsequent focused runs
cover one inline card, real account selection/completion, duplicate
delivery/publication, verification before readiness, unchanged run
authority, private tasks, no extra provider wake,
decline/Stop/reassignment, company isolation, invalid runtime tokens,
transient retries and redacted diagnostics.
- Final setup/card and generic connection-service suites: 49 tests
passed. The stopped-turn case failed before the correction, then passed
with real account completion, the stopped source remaining cancelled,
and the replacement source remaining queued.
- After rebase, all 129 focused assertions passed. The local queue
suite’s 88 execution assertions passed repeatedly, but its
database-fixture teardown exceeded both 30-second and 120-second hooks.
An isolated queue case completed, and the setup/connection suites fully
passed. The original queue test and cleanup remain unchanged; broad CI
supplies the full-suite result.
- Server/UI typechecks, module boundaries, token gates and diff checks
passed. `pnpm verify --explain` requested broad CI because the
execution-contract document changed; no redundant full local suite was
substituted.
- Real source acceptance: macOS arm64, Node 26.4.0, isolated embedded
PostgreSQL and local board identity. Created an OpenCode/OpenRouter
agent and task through the API with no default. Admission retained one
unstarted turn and produced the existing inline Connect OpenRouter card.
The browser opened its credential form and closed it without entering
credentials. The same card/run persisted after server restart and
reload, with the setup label and no incorrect Resume. The browser also
reproduced Stop followed by a new queued turn retaining the stale card
on the prior head. On `12f609c98372fd1e50644028db75cfcc8deb210d`,
startup superseded the stale card and retained the same replacement
turn, with the original turn still cancelled and no provider execution.
- The earlier CI case-search failure also reproduces on exact base
`0e3fdebea8257bdff11573081c76b306f12db230` without this PR. Its repair
is now in master via #20371; this branch is rebased onto it and contains
no API-search changes.
- Staging's actual Runtime test returned “Connect an account and choose
your personal default” before provider startup. The original turn later
started independently on deployed code, then hit a separate
workspace-export failure. Automatic file recovery subsequently marked
that run successful; its task remains Blocked and the trial’s useful
output is unverified. This PR is not deployed; provider authentication
through its card and downstream publication remain unverified. No
account, credential, default, permission or binding was changed during
diagnosis.
- Previous revision review: Greptile 5/5 on
`12f609c98372fd1e50644028db75cfcc8deb210d`, with all four inline
findings addressed and resolved. [Full CI
passed](https://github.com/paperclipai/paperclip/actions/runs/42713494508)
on that revision: 59 successful checks, one neutral check and three
intentional skips, with no pending or failed checks.

- Latest revision `976bcae993805a8a2222395bbff9394f6791b424`: resume,
router and Retry-copy regressions failed before repair. Green: 92
lifecycle/setup/shared tests, 83 setup/generic-connection/router tests,
and 137 UI/shared tests. Server/UI typechecks, module boundaries and
token gates passed. [Full CI
passed](https://github.com/paperclipai/paperclip/actions/runs/42720731137)
on this exact revision: 59 successful checks and three intentional
skips, with no pending or failed checks. Greptile reviewed this head at
5/5; all six review findings are addressed and resolved. Real Stop/retry
acceptance retained the same replacement turn and current-operation card
through two actual server restarts, with no provider execution.

## Risks

- Known account configuration failures now require repair or explicit
Retry. Temporary failures keep automatic retries; accepted work and
authority remain intact.
- The projection adds one company-scoped batch read only for unstarted
queued turns. Card attachment examines at most 100 active candidates and
adds no timer, worker, schema or migration.
- The optional stored setup-card marker requires an aware reader during
rollback. No run/session/checkpoint identity format changes. Setup
presentation grants no access and cannot bypass verification or other
gates.

## Model Used

- OpenAI GPT-6 (Codex), with reasoning, tool use, code editing and test
execution. The exact serving model identifier and context-window size
are not exposed in this session.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-09 17:49:34 -06:00
DottaandClaude Opus 5.5 8004ad069e perf(ui): skip socket reconcile for reads that have no live events (#20398)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The board UI keeps company data fresh through `LiveUpdatesProvider`,
which listens on a WebSocket and invalidates queries for each event
> - The provider closes the socket while the tab is hidden, and events
are not replayed, so it invalidates every active query when the socket
opens and when the tab returns
> - Some of those queries have no live events at all: instance settings,
plugin UI contributions, and the person's workspace and sidebar
preferences
> - Reconciling them adds requests to every tab return and to the first
socket open, on a server that is already saturated
> - This pull request excludes those reads from the reconcile and gives
them a 5-minute stale time
> - The benefit is four fewer requests on each tab return and on the
first socket open, with no change to company data freshness

## Linked Issues or Issue Description

**What existing behavior does this improve?**
The burst of requests when a board UI tab regains focus, and the second
wave right after page load.

**Subsystem affected**
Board UI live updates (`ui/src/context/LiveUpdatesProvider.tsx`) and
query defaults (`ui/src/main.tsx`, new `ui/src/lib/live-reconcile.ts`).

**Current behavior**
On a local instance, returning to a hidden Mine tab sends 27 API
requests. Those include `instance/settings/experimental`,
`workspace-preferences/me`, `plugins/ui-contributions` and
`sidebar-preferences/me`. Live events never touch these. The same four
re-fire when the socket first opens after load.

**Proposed behavior**
The socket reconcile skips those four reads. Their stale time is 5
minutes, so a window-focus refetch runs at most every 5 minutes. Their
own mutations still invalidate them at once.

**Reason and benefit**
Each tab return costs four fewer requests. On staging, about 25 requests
re-fire together on focus, and every one delays the visible rows.

## What Changed

- New `ui/src/lib/live-reconcile.ts`: `LIVE_EVENT_FREE_QUERY_KEYS`
(instance settings, general and experimental settings, plugin UI
contributions, workspace preferences, sidebar preferences),
`isLiveReconciledQuery`, and `applyLiveEventFreeQueryDefaults`.
- `LiveUpdatesProvider` passes `isLiveReconciledQuery` as the predicate
on its invalidate-all paths: tab return, disconnected polling, socket
open, `live.resync`, and the transcript recovery that runs after a
resync. The first healthy open still also skips session and health.
- `main.tsx` applies the 5-minute stale time to those keys with
`setQueryDefaults`. No observer sets its own stale time for them.
- Not changed, and why:
- `auth/get-session` and `health` keep focus refetch and reconnect
handling for sign-out and reconnect detection.
- `cli-auth/me` (board access) keeps the reconcile because it detects
revoked access.
- The company list keeps the reconcile because live events invalidate
it.
- Company data (agents, chats, projects, approvals, labels, join
requests, live runs and others) keeps the reconcile. The socket is
closed while hidden, so the reconcile is how missed events arrive.
Keeping the socket open while hidden is a larger change and is not in
this pull request.
- Tests: new `live-reconcile.test.ts` covers the scope and the
stale-time defaults. The hidden-tab test in
`LiveUpdatesProvider.hook.test.tsx` expects the predicate.

## Verification

- `ui`: `npx vitest run src/lib/live-reconcile.test.ts
src/context/LiveUpdatesProvider src/lib/query-client.test.ts
src/lib/persisted-query-cache.test.ts` passes (283 tests).
- `pnpm verify` asks for broader CI coverage because `main.tsx` is
outside the resolved test graphs. Required CI provides it.
- Manual, on a local instance with the Mine page: hide the tab for 45 s,
then show it, and count `/api/` resource entries. `master` sends 27
requests and this branch sends 23. The four reads above are absent;
every company read is still present.

## Risks

- The reconcile was also how other tabs picked up changes to these
reads. A change made in another tab or browser now reaches this tab on
its next focus after 5 minutes, or on reload; before, it was the next
focus after 30 s. This covers an administrator changing an instance or
experimental setting or installing a plugin, and the same person
changing their workspace or sidebar preferences in another tab (for
example the coordinator or classic shell, or sidebar order). Changes
made in the same tab still apply at once.
- Low risk otherwise. Only the reconcile predicate and the stale time
change.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with
tool use (shell, file edits, browser). 1M-token context window. Extended
thinking was off.

## 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-09 17:42:54 -06:00
DottaandClaude Opus 5.5 b184e40108 perf(ui): serve every project list from one includeArchived read (#20395)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The board UI reads the company project list in many places: the
sidebar, starred projects, the command palette, the new-task dialog, and
archive-aware pages such as the Mine inbox
> - Active-only consumers read `GET /projects`, and archive-aware ones
read `GET /projects?includeArchived=true`, under two React Query keys
> - A Mine load therefore fetched the same large payload twice (580–590
KB each on our dev instance)
> - The two responses differ only by archived rows; the server's active
list is exactly the rows where `archivedAt` is null
> - This pull request makes every consumer share the
`includeArchived=true` query, and active-only consumers filter it with
`select`
> - The benefit is one project read per page instead of two

## Linked Issues or Issue Description

**What existing behavior does this improve?**
Duplicate project list reads on page load.

**Subsystem affected**
Board UI project queries (`ui/src/lib/company-projects-query.ts` and its
consumers).

**Current behavior**
A cold Mine load sends both `/companies/:id/projects` (sidebar) and
`/companies/:id/projects?includeArchived=true` (inbox).

**Proposed behavior**
One `includeArchived=true` read serves both. Active-only consumers apply
`selectActiveProjects`.

**Reason and benefit**
The project list is one of the largest shell payloads. Reading it once
saves a large response on every page that has both kinds of consumer.

## What Changed

- New `companyProjectsQuery(companyId, { includeArchived? })` in
`ui/src/lib/company-projects-query.ts`. It always uses the
`includeArchived: true` key and fetch. For active-only use it adds a
stable, module-level `select` that keeps rows without `archivedAt`.
- 22 files that read the active list now spread
`companyProjectsQuery(...)`, and so do the plugin SDK project picker and
16 archive-aware pages that wrote the shared key and fetcher inline.
Their `enabled` and other options are unchanged. `OnboardingWizard`'s
one imperative read is unchanged. `pages/Inbox.tsx` already reads the
same key and fetcher, so it shares the entry without an edit.
- Invalidation: project mutations and live events invalidate
`queryKeys.projects.all(companyId)`, which matches the shared key. The
onboarding wizard invalidated the old active-only key; it now
invalidates `projects.all`.
- Tests: new `company-projects-query.test.tsx` (one request for both
consumers, correct filtering, and one refetch after `projects.all`
invalidation). Three existing tests (`CommandPalette`, `CompanyExport`,
`GitHubBotManagement`) now seed or expect the shared key.
- New browser check
`tests/perf/collections/shared-project-list.spec.ts`: after the Mine
inbox read completes, the page has sent no active-only project request.

## Verification

- `pnpm verify` passes.
- `ui`: the consumer suites (`CoordinatorSidebar`, `SidebarProjects`,
`SidebarStarredProjects`, `CommandPalette`, `NewIssueDialog`,
`AgentWorkPanels`, `CoordinatorNeedsYou`, `IssueDetail`,
`ProjectDetail`, `Projects`, `CompanyExport`, `ChatExecutionDefaults`,
`GatewaysList`, `Search`, `IssueProperties`, `persisted-query-cache`)
pass.
- Browser check:
`PAPERCLIP_COLLECTION_PERF_BASE_URL=http://127.0.0.1:<port> pnpm exec
playwright test --config tests/perf/collections/playwright.config.ts
shared-project-list.spec.ts`. It passes on this branch and fails on
`master`.

## Risks

- A page that needs only active projects now downloads archived rows
too. For most companies that is a small difference; the duplicate read
it removes is a full copy.
- The server does not order either list, and consumers sort for display,
so filtering on the client keeps behavior.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with
tool use (shell, file edits, browser). 1M-token context window. Extended
thinking was off.

## 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-09 17:42:47 -06:00
DottaandClaude Opus 5.5 c7eb35809e perf(ui): read skills and routines only when an editor mounts (#20392)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The board UI wraps every page in an `EditorAutocompleteProvider`
that supplies slash commands to Markdown editors
> - The provider read the full company skills and routines lists as soon
as a company was selected, on every page
> - The Mine inbox has no editor, but it still paid for both reads:
259–284 KB for skills and 225–333 KB for routines on our dev and staging
instances, with skills taking 8.5–13.7 s on a saturated staging server
> - Those reads compete with the inbox rows for the same server
> - This pull request starts the two reads only when the first Markdown
editor mounts
> - The benefit is two fewer large reads on every page without an
editor, and no change for pages that have one

## Linked Issues or Issue Description

**What existing behavior does this improve?**
The requests that every board UI page sends on load.

**Subsystem affected**
Board UI context (`ui/src/context/EditorAutocompleteContext.tsx`).

**Current behavior**
A cold load of `/:prefix/issues?view=mine` sends `GET
/companies/:id/skills` and `GET /companies/:id/routines` although
nothing on the page renders them.

**Proposed behavior**
The provider keeps both queries disabled until a component calls
`useEditorAutocomplete()`. `MarkdownEditor` is the only caller, so the
reads start when an editor mounts: a task composer, the new-task dialog
when it opens, a comment box.

**Reason and benefit**
Each page without an editor saves two large reads. Pages with an editor
read the same data at mount, before a user can type `/`.

## What Changed

- `EditorAutocompleteProvider` holds a `requested` flag. Both queries
are `enabled` only after it is set.
- `useEditorAutocomplete()` sets the flag in an effect on first use. The
flag stays set, so the lists stay current through the existing
live-update invalidations for skills and routines.
- New unit test `EditorAutocompleteContext.test.tsx`: no reads without
an editor; one read of each list when two editors mount.
- New browser check `tests/perf/collections/shell-editor-reads.spec.ts`:
a Mine load sends no `skills` or `routines` request.

## Verification

- `pnpm verify` passes.
- `ui`: `npx vitest run src/context/EditorAutocompleteContext.test.tsx
src/components/task-chat/TaskChatComposer.test.tsx` passes.
- Browser check against a disposable local instance:
`PAPERCLIP_COLLECTION_PERF_BASE_URL=http://127.0.0.1:<port> pnpm exec
playwright test --config tests/perf/collections/playwright.config.ts
shell-editor-reads.spec.ts`. It passes on this branch and fails on
`master`.
- Manual: on a local instance the Mine load dropped from 34 to 32 API
requests. Opening a task shows the composer; `/` lists skills and
routines as before.

## Risks

- Low risk. On the first editor mount the slash menu is empty until the
two reads return. On `master` the same reads ran at app start, so a user
who typed `/` within the first moments of a page with an editor saw the
same gap.
- `attention/summary` is not changed. The navigation's "Needs you" count
renders it on the Mine page.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with
tool use (shell, file edits, browser). 1M-token context window. Extended
thinking was off.

## 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-09 17:42:39 -06:00
DottaandClaude Opus 5.5 7e93ccb4b3 perf: fetch recent task summaries in one request (#20389)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The board UI sidebar shows the person's recent tasks on every page,
including the Mine inbox
> - The sidebar refreshes those rows through `GET
/companies/:id/issues/summaries`, which accepted at most five IDs
> - `useRecentTasks` sent the chunks one after another, so a 25-task
history cost five sequential requests
> - On staging each request took 3–6 s, so the recent-task rows settled
about 20 s after load and every request competed with the inbox rows
> - This pull request raises the per-request cap to 50 and fetches any
remaining chunks in parallel
> - The benefit is one summaries request for a typical history instead
of a serial chain

## Linked Issues or Issue Description

**What existing behavior does this improve?**
The refresh of the sidebar's recent tasks on page load.

**Subsystem affected**
Board UI (`ui/src/hooks/useRecentTasks.ts`) and the `/issues/summaries`
route bound (`packages/shared`, `server/src/routes/issues.ts`).

**Current behavior**
A history of N recent tasks sends ceil(N/5) summaries requests in
series. Measured on staging on 2026-11-09 for a cold Mine load: five
requests of five IDs, started at 1.0 s, 4.3 s, 8.1 s, 12.3 s and 18.4 s,
each taking 3.3–6.1 s.

**Proposed behavior**
Up to 50 recent tasks are one request. Longer histories send their
chunks at the same time.

**Reason and benefit**
The staging server is saturated, so each extra serial round trip delays
the sidebar and the inbox rows. One request removes four round trips
from a typical load.

## What Changed

- `ISSUE_SUMMARIES_MAX_IDS` is now 50 (was 5). The shared validator, the
route's 400 message, the service guard and the OpenAPI description use
the new bound.
- `useRecentTasks` builds all chunks first and fetches them at most
three at a time (`RECENT_TASK_SUMMARY_CONCURRENCY`). The recent list has
no size cap, so this keeps a long history from becoming a burst. The
cache key, omission handling and pruning are unchanged. A failed chunk
still fails the batch, so nothing is pruned on a partial failure.
- Tests: the route test accepts exactly `MAX` IDs and rejects `MAX + 1`;
a hook test checks that at most three chunks are in flight; the API test
uses the constant; `useRecentTasks` tests cover a 25-task history in one
request and a 103-task history with three chunks in flight together,
with and without a failed chunk.
- New browser check
`tests/perf/collections/recent-task-summaries.spec.ts`: a Mine load with
a 12-task history sends summaries requests that each carry all 12 IDs,
and the server answers 200 with all 12 tasks.

## Verification

- `pnpm verify` (selected Vitest files) passes.
- `server`: `npx vitest run src/__tests__/issue-summaries-routes.test.ts
src/__tests__/issue-display-status.test.ts` passes (embedded
PostgreSQL).
- `ui`: `npx vitest run src/hooks/useRecentTasks.test.tsx
src/api/issue-summaries.test.ts
src/components/task-chat/TaskChatBlockerLinks.test.tsx
src/components/CoordinatorSidebar.test.tsx
src/components/SidebarRecentTasks.test.tsx` passes.
- Browser check against a disposable local instance:
`PAPERCLIP_COLLECTION_PERF_BASE_URL=http://127.0.0.1:<port> pnpm exec
playwright test --config tests/perf/collections/playwright.config.ts
recent-task-summaries.spec.ts`. It passes on this branch. With
`master`'s hook and constant it fails: three requests of 5, 5 and 2 IDs.

## Risks

- Low risk. The route still validates UUIDs, deduplicates, and applies
the same read authorization per row. Blocker attention is computed with
set-based queries, so 50 IDs is the same number of queries as 5.
- During a rolling deploy, a new UI against an old server would get a
400 for a history above five tasks. The server serves the UI bundle, so
both change together on a normal deploy. The sidebar keeps its stored
snapshot when the refresh fails.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with
tool use (shell, file edits, browser). 1M-token context window. Extended
thinking was off.

## 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-09 17:42:32 -06:00
DottaandPaperclip 2f47d0fadb perf(workspace): run portable archives in bounded CPU workers (#20404)
## Thinking Path

> - Paperclip preserves agent work when its control plane changes.
> - Workspace imports capture and verify portable filesystem archives.
> - The archive function uses asynchronous I/O but hashes and handles
manifests on the caller's JavaScript thread.
> - Concurrent imports therefore share one CPU execution context for
that work.
> - This pull request runs the same archive function in bounded native
Node workers.
> - The parent keeps all database, storage, source locks and publication
decisions.

## Linked Issues or Issue Description

**What happened?**

Concurrent administrative workspace imports can spend many minutes
capturing archives while the owning Node process uses about one CPU
core. Promise concurrency does not move hashing and manifest work off
that process's JavaScript thread.

**Expected behavior**

Independent archive calls use separate CPU execution contexts. At most
eight archive workers run in one process. Cancellation must wait for
file cleanup and actual worker exit before the caller releases its
locks.

**Steps to reproduce**

Run several workspace captures in one process. The existing wrappers
call the same archive function on the parent thread. The regression
holds eight real worker threads at a CPU barrier while the parent
remains responsive, then cancels a queued ninth call without spawning
it.

**Deployment mode**

All server callers of the portable capture and restore wrappers. Related
import work: https://github.com/paperclipai/paperclip/pull/20311 and
https://github.com/paperclipai/paperclip/pull/20394.

## What Changed

- Run the existing self-contained archive function in native workers.
Its algorithm and format stay byte-identical.
- Limit active workers to eight per process, with a 1 GiB old-generation
heap limit each. Workers stop when their owned call finishes.
- Forward cancellation to the worker's AbortController. Wait for actual
exit before returning, throwing or releasing an active slot. Cancel
queued calls before they spawn.
- Own capture scratch in a private parent-created worker temporary root.
Remove it after actual exit, including abnormal exit.
- Keep direct archive algorithm fault tests on the canonical
implementation. Add real-worker byte, manifest, restore, CPU barrier and
cancellation tests.

## Verification

- Four real-worker regressions passed: forced-exit capture scratch
cleanup; exact capture bytes/manifest and restored files/modes/links;
eight CPU barriers plus queued abort; abort cleanup joined through
actual worker exit.
- Canonical workspaceArchive declaration/body hash is unchanged from the
base:
`d6cffeab112b817fab8f65a3c0d8ecdc8d23589d37bf44ae30fe9e53213d54f4`.
- The combined archive run passed 114 cases. Three Unix-socket cases
failed because the sandbox denied listen with EPERM; a focused retry
with socket permission passed all three. The initial separate worker run
passed all three cases; the new head passed all four, including
forced-exit cleanup. All server layered typechecks and module boundary
checks passed.
- The initial ac4bc5760d source passed a synthetic smoke on staging’s
Linux Node v24.22.2 through the installed tsx loader: direct/worker byte
and digest parity, worker restore of three files plus a symlink, and
private fixture cleanup. It made no database, object-storage or provider
calls.
- Greptile reviewed final head
`3a51fe67ff17a56b2ddd7877c89734ba60afd18e` at 5/5 with no actionable
issues. All applicable final-head CI passed, including aggregate verify,
typecheck, build, workspace consumer suites and all server shards. The
second review remains queued. No measured migration speedup claim is
made yet.

## Risks

Each active worker adds a bounded V8 heap and native overhead. Eight
workers can use more memory and I/O bandwidth than the former single
JavaScript thread. The existing 64 MiB manifest limit and streaming file
buffers stay in place. Worker failures refuse the call and cannot
publish authority. The canonical restore staging path stays unchanged;
this PR does not claim cleanup after every arbitrary process crash.
Cooperative cancellation retains the existing cleanup path; the parent
waits for actual worker exit. No database, key, object-storage or
provider behavior changes.

## Model Used

OpenAI Codex, based on GPT-6, with tool use and code execution. The
exact deployment identifier and context-window size are not exposed in
this session.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-09 17:42:11 -06:00
Nicky LeachandClaude Opus 5.5 5858cef8f9 perf(runner): poll the provider right after an ACK releases it (#20374)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The Runner (`paperclip-runnerd`) sends provider events to the
controller over one authenticated socket
> - Its main loop reads one control frame, then polls the provider only
after a read finds no frame
> - After the controller ACKs the last sent event, the next read waits
the full 250 ms `RUNTIME_READ_TIMEOUT` before the Runner polls the
provider again
> - A Codex turn start needs one or two such rounds before the Runner
reads `turn/started`, so every Codex turn waits 250 ms or 500 ms for no
reason
> - This pull request reads only the frames that are already queued
after a consumed control frame, and then polls at once, when this
connection produced the unacknowledged output
> - The benefit is 250 ms less time to `turn/started` for each ACK
round, and faster event delivery after each ACK

## Linked Issues or Issue Description

This is an enhancement. The fields follow
`.github/ISSUE_TEMPLATE/enhancement.yml`.

**What existing behavior does this improve?**
Codex turns wait for the Runner read timeout before the Runner sends
`turn/started`.

**Subsystem affected**
The Runner (`paperclip-runnerd`) durable loop in
`packages/paperclip-runner/runner/crates/runner-core/src/durable/`, for
the local Codex provider.

**Current behavior**
On a hosted test deployment (48 h, 114 Codex turns),
`runner.transport.started_event` for Codex has two modes:

| Codex turn | n | `started_event` p50 |
| --- | --- | --- |
| no provider cold start in the trace | 57 | 468 ms (all < 700 ms) |
| provider cold start in the trace | 57 | 918 ms (51 of 57 ≥ 700 ms) |

For Claude (ACPX) turns, the p50 is 90 ms. ACPX returns `turn.started`
with the command result. Codex reads `turn/started` from the provider in
a later poll.

**Proposed behavior**
The Runner polls the provider as soon as the controller ACK releases the
poll gate. The time to `turn/started` then follows the controller round
trip, not the read timeout.

**Reason and benefit**
- Real Codex (`codex app-server` 0.157, local) sends `turn/started` 2–16
ms after its `turn/start` response. The provider is not the cause.
- `runner.transport.started_event` starts when the `turn.start` command
result arrives. It ends when the controller commits `turn.started`. In
this time the Runner sends `turn.accepted`, waits for its ACK, waits 250
ms, polls, and sends `turn.started`.
- On the first turn after a provider launch, the first poll returns only
the launch diagnostic that the executor retained. A second ACK round and
a second 250 ms wait come before the poll that reads `turn/started`.
This is the difference between the two modes above (about 450 ms: 250 ms
wait and one event round).
- So 250 ms (warm turn) or 500 ms (first turn) of each Codex
`started_event` is the read timeout. The rest is one or two controller
event rounds (about 200 ms each on that deployment), less than 20 ms of
Codex time, and the 10 ms pump interval of the TypeScript transport.
- The expected benefit is 250 ms less per warm Codex turn start and 500
ms less for the first turn after a provider launch, plus faster Codex
event delivery after each ACK.

**Breaking changes**
None. No protocol, schema, or controller change.

## What Changed

- `AuthenticatedTransport::receive_queued_json` reads with a 1 ms
timeout and then restores `RUNTIME_READ_TIMEOUT`.
- New `CommandExecutor::provider_poll_is_live` hook (default `false`).
The local Codex executor returns `true` only when it owns a running
provider with no open startup attempt, so a poll cannot restore or
launch a provider. The native executor forwards it for the local Codex
facade only.
- In the steady-state loop, after a consumed control frame, the Runner
uses `receive_queued_json` when it has sent output that this connection
produced (a command or a provider poll) and the executor reports a live
provider. If no frame is queued, it polls the provider at once.
- When a provider poll returns no output, or when no frame was consumed,
the Runner uses the 250 ms read as before. A 1 ms read follows only a
consumed frame, so an idle loop does not spin.
- The replay boundary is the highest source sequence before the
welcome's pending commands run. A fresh command delivered in the welcome
counts as this connection's output. Replayed commands only resend their
stored results.
- Replayed output from an earlier connection or process, ACPX, and the
other providers keep the 250 ms read. The controller can still send its
next command before a poll restores a provider. (Without the
live-provider gate, six ACPX source-recovery tests in
`runnerd-acpx-source-reconciliation.test.ts` failed: the fast poll
restored the ACPX sidecar before the next recovery command.)
- Control frames that are already queued behind the ACK are still read
and executed before the provider poll. The ACK gate in
`poll_executor_events_after_controller_ack` is unchanged.

## Verification

- `cargo test -p paperclip-runner-core --lib durable::` (121 passed).
Five new tests in `src/durable/tests/ack_release_poll.rs`, included by
the transport test module like `hosted_retention.rs`:
-
`acknowledged_output_polls_the_provider_without_waiting_out_the_read_timeout`:
the provider poll follows the ACK read in less than
`RUNTIME_READ_TIMEOUT`. Without the change it fails (the poll waited
280–356 ms).
- `control_queued_behind_an_ack_runs_before_the_next_provider_poll`: a
command sent in the same write as the ACK executes before the provider
poll.
-
`welcome_command_output_polls_the_provider_without_waiting_out_the_read_timeout`:
the output of a fresh command in the welcome also releases the poll at
once. With the replay boundary taken after the welcome commands, it
fails.
- `replayed_output_ack_keeps_the_read_timeout_before_the_provider_poll`:
after a reconnect, a command sent 50 ms after the ACK of replayed output
executes before the provider poll. Without the replay condition it
fails.
-
`restorable_provider_ack_keeps_the_read_timeout_before_the_provider_poll`:
when the executor does not report a live provider, a command sent 50 ms
after the ACK executes before the provider poll.
- `npx vitest run src/live/runnerd-` in `packages/paperclip-runner` with
the new debug `paperclip-runnerd`: run with `--no-file-parallelism`.
Before the welcome-boundary fix: 576 passed, 3 skipped. After it: 575
passed, 3 skipped, and 1 failure once (`stops a lost runnerd's provider
and escaped tool job from its lineage journal (journal=true)`, a SIGKILL
timing test) while `cargo test` ran at the same time.
`runnerd-codex-transport.test.ts` then passed 293 of 293 three times
with this build, and three times with the base build.
- Local before and after measurement with the real Runner, the
TypeScript transport, and the fake Codex app-server (8 turns, 2 runs
each, debug build):

  | span | before p50 | after p50 |
  | --- | --- | --- |
| `runner.transport.started_event` | 377 ms (first turn 682–695 ms) |
119–122 ms (first turn 117–119 ms) |
| `runner.transport.turn_start` | 462–471 ms (first turn 775–780 ms) |
204–212 ms (first turn 247–251 ms) |

- The saving on the hosted deployment is not measured. From the analysis
above, the expected saving is about 250 ms for a warm Codex turn and
about 500 ms for the first turn after a provider launch.

## Risks

- Only local Codex gets the fast path. ACPX streaming keeps the old 250
ms wait after each ACK. A later change can opt ACPX in when its poll can
prove that it does not restore the sidecar.
- More provider polls during Codex streaming: a poll can now follow each
ACK at once instead of after 250 ms. Batches can become smaller, so the
provider state save for each batch can happen more often. Each event
already has its own durable save.
- A command that the controller sends more than 1 ms after its ACK can
now arrive after a provider poll. This was already possible after 250
ms. The change applies only when this connection produced the output, so
the provider is already active.
- If the Runner cannot restore the 250 ms read timeout, it treats the
socket as failed and reconnects. It does not continue with a 1 ms
timeout.
- No protocol, schema, or controller change.

> 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`) in Claude Code, with tool use
(shell, code search, Honeycomb queries) 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
- [ ] 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-09 15:38:58 -08:00
DottaandClaude Opus 5.5 c985541a57 perf(db): index retry-exhaustion receipts for the attention scan (#20386)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The attention feed shows runs that failed after automatic retries
stopped
> - To find these runs, the feed reads every `heartbeat_run_events` row
of the company and filters the rows by a message prefix
> - Run output makes this table very large, so each feed request does a
large scan
> - On a hosted test database, this one statement used the most total
database time: 923 calls at a mean of 5 s (4,709 s in total)
> - Request authentication waits behind this background work, so all
users feel the load
> - This pull request adds a small partial index that holds only the
retry-exhaustion receipts
> - The benefit is that the same query reads a few index entries instead
of all company events

## Linked Issues or Issue Description

**What existing behavior does this improve?**
The attention feed query that finds retry-exhausted runs
(`listAttentionExhaustedRuns`).

**Subsystem affected**
Server attention feed and the `heartbeat_run_events` table schema.

**Current behavior**
The query reads all company rows in `heartbeat_run_events`, filters them
by `event_type = 'lifecycle'` and the `Bounded retry exhausted` message
prefix, and then joins the few matches to failed or timed-out runs. The
cost grows with all run output. It does not grow with the number of
exhausted runs.

**Proposed behavior**
A partial index holds only the matching receipts. The query plan reads
the index and joins a few rows. The query text and the result do not
change.

**Reason and benefit**
The scan was the most expensive statement by total time on a loaded
hosted database. A local reproduction shows a 54x faster query and about
116x fewer buffer reads.

**Breaking changes**
None. The API, the feed rows, and their order do not change.

## What Changed

- Add the partial index `heartbeat_run_events_retry_exhausted_idx` on
`(company_id, run_id, id DESC)`. The index covers only rows where
`event_type = 'lifecycle'` and `message like 'Bounded retry
exhausted%'`.
- Add migration `0454`. It uses `CREATE INDEX CONCURRENTLY IF NOT
EXISTS`. The migration runner runs it as a top-level statement, the same
as migrations 0432, 0436 and 0450.
- Add a regression test. It pins the row order (newest run first, then
the newest receipt on a tie) and the latest receipt per run. It also
checks that the query plan can use the new index. If the query predicate
changes so that the index no longer applies, the test fails.
- Add `scripts/benchmark-exhausted-run-attention.ts`. It measures the
same query with and without the index, and the time to build the index.
- Document the index in `doc/DATABASE.md`.

## Verification

Cause and thesis. The scan cost comes from reading all company events,
not from the few exhausted runs. Thesis: an index that holds only the
receipts makes the query cost depend on the number of receipts, and
keeps the result identical. The benchmark below confirms the thesis.

Rejected alternative without a migration. I first tried a LATERAL lookup
that starts from the failed and timed-out runs. On the same fixture it
was about 2x slower (median 379 ms against 194 ms). It reads the full
event history of each failed run with random I/O, and that costs more
than one sequential scan.

Local SQL benchmark (the SQL that the service generates; same SQL with
and without the index):

```sh
pnpm --filter @paperclipai/server exec tsx ../scripts/benchmark-exhausted-run-attention.ts --runs=30000 --events-per-run=100 --samples=9
```

Fixture (synthetic): one company with 30,000 runs (3,819 failed or timed
out) and 3.0 M events, plus a decoy company with 0.5 M events. The table
is 1.4 GB. About 5% of events are `lifecycle` events. Events of 20
concurrent runs are interleaved. 1 in 23 runs has a receipt, some runs
have two receipts, and output decoys contain the prefix. Two agents are
terminated. Embedded PostgreSQL 18.1 on an Apple laptop. The hosted
database is Neon with different compute, so the absolute times are local
SQL measurements only.

The script runs two cycles (without index, then with index; then with
index, then without index) to expose cache bias. Each measurement has 2
warmups and 9 samples.

| Query | Without index | With index | Samples and spread | Output check
|
| --- | --- | --- | --- | --- |
| `listAttentionExhaustedRuns` | median 157 ms (96–1,271 ms) | median
3.3 ms (2.2–5.7 ms) | 18 each | 133 rows, positional rows equal in all 4
measurements |
| Buffers (EXPLAIN ANALYZE) | 127,743 blocks | 5,580 blocks | | plan
uses the new index (asserted) |

Speedup: 48x (median), on head `ae6037b2a0` (the same SQL and index as
the final head). The machine was under heavy load from other work, so
the spread without the index is wide. An earlier run on the first head
gave 248 ms against 4.6 ms (54x). Index build (`CREATE INDEX
CONCURRENTLY` from the migration file): 1.7 s and 2.5 s for the 1.4 GB
table. Index size: 152 KiB.

Review fix: the query now writes `event_type = 'lifecycle'` as a SQL
literal, not a bound parameter. With prepared statements, PostgreSQL can
reuse a generic plan. A generic plan cannot prove that a bound parameter
matches the index predicate, so it would not use the index.

Tests:

- `cd server && npx vitest run src/__tests__/attention-service.test.ts`:
44 passed. The new test checks a fresh plan and a forced generic plan
(`plan_cache_mode = force_generic_plan` with `PREPARE`). It failed with
the bound `event_type` parameter and with a changed message prefix
(confirmed red), and it passes with the fix. Locally, the file's
`afterAll` database cleanup hook timed out on a heavily loaded machine
(load average about 28). The timeout also occurs when the new test is
excluded.
- `pnpm --filter @paperclipai/db check:migrations`: passed.
- `pnpm --filter @paperclipai/db typecheck` and `pnpm --filter
@paperclipai/server typecheck`: passed.
- `pnpm verify`: see the CI results for this head.

## Risks

- Migration time. A concurrent index build reads the full
`heartbeat_run_events` table and does not block writes. Locally, the
build took 1.2–2.5 s for 1.4 GB. The time grows linearly with the table
size. I did not time the build on a production-size clone. On a database
with a 5 s full company scan, I estimate the build at tens of seconds,
which is in the 1–2 minute budget. This estimate is not proven.
- If the build stops, PostgreSQL can leave an invalid index. The
migration runner repairs invalid concurrent indexes when it runs again,
and `IF NOT EXISTS` makes a rerun safe.
- The index predicate must stay textually equal to the query predicate.
If someone changes the message prefix or binds a predicate value as a
parameter, the planner cannot use the index. The new test catches both
cases, for fresh and generic plans.
- Each new receipt writes one more small index entry. Receipts are rare,
so the write cost is very small.
- The hosted improvement is not verified until deployment. After
deployment, compare a new query-statistics window for this statement.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code with tool use (shell, file edits, local PostgreSQL). Standard
context window and reasoning mode.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 17:37:20 -06:00
DottaandClaude Opus 5.5 c57cd1bdb9 perf(server): read plugin lifecycle inbox heads in one pass (#20405)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Plugins get agent, project and agent runtime changes from the
`resource_lifecycle_events` journal. Each plugin acknowledges events one
resource at a time.
> - The inbox page query checked each unacknowledged event against all
older events of the same resource. Each check walked the acknowledged
history of that resource.
> - Agent runtime streams get a new event for each configuration change,
so one resource can have thousands of acknowledged events. The cost grew
as pending events times acknowledged history.
> - On a hosted instance, three copies of this read ran for 601 seconds
each, and one earlier copy ran for 19 minutes. The delivery worker runs
it again after each acknowledgment.
> - This pull request selects the head of each resource in one pass and
keeps the same result.
> - The benefit is that the page read is linear in the company journal
size. In local tests it is 250x to 660x faster, and the result rows are
equal.

## Linked Issues or Issue Description

**What happened?**
The plugin lifecycle inbox page query (`select "id", "company_id",
"resource_type", "resource_id", "action", "created_at" from
"resource_lifecycle_events" where ...`) ran for 601 seconds on a hosted
database. Three copies were active at the same time.

**Expected behavior**
A lifecycle page read must stay fast when a resource has a long
acknowledged history and a pending backlog.

**Steps to reproduce**
1. Give a company 40 agents with 1,500 `agent_runtime` events each.
2. Acknowledge the first 1,200 events of each agent for one plugin.
3. Call `pluginLifecycleInbox(db, pluginId).list(companyId, 100,
undefined, ["agent", "project", "agent_runtime"])`.
4. The read takes about 10 seconds for 60,000 events of that company. At
200,000 events it takes about 6 minutes.

**Paperclip version or commit**
`02bfe71a6b` (master).

**Deployment mode**
Hosted (PostgreSQL). Local repro uses embedded PostgreSQL 18.

## What Changed

- `server/src/services/plugin-lifecycle-inbox.ts`: `list` now selects
one head per resource with `DISTINCT ON (resource_type, resource_id)`.
The head is the unacknowledged creation, else the oldest unacknowledged
event. Then the query applies `afterId`, id order and the limit. This is
the same set that the old `firstForResource` predicate admits.
`acknowledge` keeps the old predicate for its single-event check.
- `plugin-lifecycle-inbox.ts`: new `hasPending(companyId, types)`. It
returns whether any unacknowledged event exists. Each resource with an
unacknowledged event has a head, so this gives the same answer as
`list(companyId, 1).length > 0`.
- `server/src/services/plugin-lifecycle-delivery.ts`: the idle-sleep
journal check uses `hasPending`. This keeps its early exit. A one-row
`list` page would read the full journal.
- `server/src/__tests__/resource-lifecycle-events.test.ts`: new database
test for heads across acknowledged history, a creation recorded after
transitions, acknowledged gaps, shared ids across resource types,
per-plugin progress, another company, `afterId` pages, limits,
`hasPending`, and the next head after an acknowledgment. The test also
passes on the old query, so it locks the contract.
- `scripts/benchmark-plugin-lifecycle-inbox.ts`: new benchmark. It loads
the baseline service from git, runs both on the same fixture, alternates
order, asserts equal rows, and prints plans.
- `doc/DATABASE.md`: describes the read cost and the benchmark command.
- No schema change, index or migration.

## Verification

Thesis: the old read cost is (unacknowledged candidates) x (acknowledged
older events per resource), because each candidate walks the
acknowledged prefix of its resource. One pass that keeps the
per-resource head removes that product and returns equal rows.

Baseline plan at 220,080 events: 40,020 candidates each walked about
3,999 older rows, which gave 160,039,980 acknowledgment probes and
562,140,833 buffer hits. The treatment plan reads the journal once with
a hash anti join and sorts the 40,020 unacknowledged rows (3,925 buffer
hits).

Local SQL timings (embedded PostgreSQL 18, Apple Silicon; event counts
include a 20,000-event second company, synthetic data, warm cache, one
warmup, order alternates):

| Fixture (events / acknowledged) | Case | Before median | After median
| Samples (after) | Speedup |
| --- | --- | --- | --- | --- | --- |
| 40,060 / 16,040 | page of 100, all types | 3,263 ms | 13.1 ms | 3
(12.0-14.1) | 250x |
| 80,080 / 48,060 | page of 100, all types | 10,137 ms | 23.9 ms | 5
(22.7-32.6) | 424x |
| 220,080 / 160,060 | page of 100, all types | 355,718 ms | 538 ms | 3
(72-785) | 662x |
| 1,020,080 / 960,060 | page of 100, all types | not run (est. > 30 min)
| 1,682 ms | 7 (1,203-2,384) | - |
| 80,080 / 48,060 | page of 100, agent and project only | 1.5 ms | 1.6
ms | 5 | neutral |
| 80,080 / 48,060 | idle-sleep check | 14.5 ms | 13.8 ms | 5 | neutral |
| 1,020,080 / 960,060 | idle-sleep check | not run | 406 ms | 7 | - |

- Result rows are equal (`assert.deepEqual` on all mapped rows, in
order) for each case where both versions ran.
- The agent runtime revision read in `plugin-agent-runtime-state.ts` and
`agent-file-store.ts` (`select id ... order by id desc limit 1`) uses a
backward index-only scan and reads 5 buffers at 1M events. It is not
changed.

Commands:

```sh
pnpm --filter @paperclipai/server exec tsx ../scripts/benchmark-plugin-lifecycle-inbox.ts --baseline-ref=02bfe71a6b --agents=40 --per-agent=1500 --acked=1200 --samples=5
pnpm --filter @paperclipai/server exec tsx ../scripts/benchmark-plugin-lifecycle-inbox.ts --baseline-ref=02bfe71a6b --agents=40 --per-agent=25000 --acked=24000 --samples=7 --treatment-only
cd server && npx vitest run src/__tests__/resource-lifecycle-events.test.ts src/__tests__/plugin-lifecycle-delivery.test.ts src/__tests__/agent-runtime-hooks.test.ts
```

- `resource-lifecycle-events.test.ts`: 28 passed.
`plugin-lifecycle-delivery.test.ts`: 40 passed (includes idle-sleep
"none" and "present"). `agent-runtime-hooks.test.ts`: 22 passed.
- `tsc --noEmit -p server`: pass.

## Risks

- The page read is still linear in the company journal size, because
acknowledgments are per plugin and the journal has no retention yet. At
1M events a page takes about 1.7 s locally. Each acknowledgment wakes
another full pass. A per-plugin cursor or retention can remove this
later.
- The planner can choose a different plan on the hosted database. The
local plans use a hash anti join and one sort. The hosted improvement is
not yet measured.
- `acknowledge` still checks one event against older events of its
resource. That check is linear in that resource's history per call, as
before.
- Low behavior risk: the new test passes on both the old and new
queries.

## Model Used

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

## Checklist

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

> - Paperclip is the open source app people use to manage AI agents for
work
> - Each finished agent run gets a liveness classification. The task
page shows it, and recovery reads it.
> - Run finalization writes the classification. A backfill repairs runs
that do not have it, for example older runs or runs whose finalization
failed.
> - The task page runs read (`GET /issues/:id/runs`) starts this
backfill on each read, for each task.
> - The candidate lookup reads every unclassified finished run in the
company. For each row, it reads the run context and probes
`activity_log`. Rows of other tasks never match, so each read repeats
the same work.
> - On a hosted test instance, this lookup used approximately 1 s per
call, 2,636 calls, and 3,500 s in total.
> - This pull request makes the lookup read only the rows of the task,
and skips a task after a pass finds nothing left to repair.
> - The benefit is less database work per task page read. The
classification still gets written.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

The run liveness backfill that the task runs read starts
(`scheduleRunLivenessBackfill` in `server/src/services/activity.ts`).

**Subsystem affected**

Server: activity service, task runs read, run liveness backfill.

**Current behavior**

Each task runs read runs this lookup:

```sql
select ... from heartbeat_runs
where company_id = $1 and liveness_state is null and status not in ('queued', 'running')
  and (context_snapshot ->> 'issueId' = $2
       or exists (select 1 from activity_log where company_id = $3 and entity_type = 'issue'
                  and entity_id = $4 and run_id = heartbeat_runs.id))
limit 20
```

PostgreSQL reads all unclassified finished runs of the company through
`heartbeat_runs_company_liveness_idx`. It then reads the context of each
row, which can be large, and probes `activity_log` for each row.
Unclassified runs of other tasks, and runs without a task, stay
unclassified, because no read of their own task repairs them. Thus the
cost grows with the company, not with the task. The lookup also runs
again on each read when the task has nothing to repair.

**Proposed behavior**

- The lookup reads only the runs of this task. One branch uses
`heartbeat_runs_company_ctx_issue_created_idx` (context `issueId`). The
other branch uses `activity_log_company_entity_created_idx` (issue
activity rows with a run id). The result keeps the same filter: same
company, liveness missing, status not `queued` or `running`, at most 20
rows.
- When a pass finds fewer than 20 rows, it has repaired all rows of the
task. The service then skips the task for 10 minutes. A full batch of 20
does not start the skip window, so the next read repairs the next batch.

**Reason and benefit**

The lookup is now proportional to the history of one task. Most reads
find nothing, and those reads now do no database work for 10 minutes.

**Breaking changes**

None. No migration, no API change, no new background work.

**Additional context**

The chat bootstrap read already skips the backfill
(`skipLivenessBackfill`). That option still works, and a test covers it.

## What Changed

- `server/src/services/run-liveness-backfill-query.ts`: new
`missingRunLivenessCandidatesQuery`. It replaces the `OR` and the
correlated `exists` with `id in (context branch union all activity
branch)`. Both branches are filtered by company and task.
- `server/src/services/activity.ts`: the backfill uses the new query and
returns the number of rows it found. `scheduleRunLivenessBackfill` keeps
a bounded in-memory map (at most 10,000 tasks, oldest entry is removed
first) of tasks with nothing left to repair. It skips these tasks for 10
minutes.
- `server/src/__tests__/activity-service.test.ts`: four new
real-database tests.
- The backfill repairs runs found by context and runs found only by
issue activity. It does not change a running run, an unclassified run of
another task, or a run of another company that names the same task id.
- After a pass leaves nothing to repair, more reads do not repair a new
unclassified run. After the skip window ends, the next read repairs it.
- A task with 25 unclassified runs (more than one batch) gets all of
them repaired over later reads.
- A read with `skipLivenessBackfill` does not repair. The next normal
read does.
- `scripts/benchmark-run-liveness-backfill.ts`: a local SQL benchmark
that compares the pre-fix query with the new query.

## Verification

Local SQL benchmark (embedded PostgreSQL 18.1, macOS, synthetic data):

```sh
cd server
pnpm exec tsx ../scripts/benchmark-run-liveness-backfill.ts --baseline-ref=02bfe71a6bbd7c5676cb7e72778b361c5bc72765
```

- Baseline: `02bfe71a6b` (pre-fix master). Treatment: `80fcfcf7f1`,
clean tree.
- Fixture assumptions (staging row counts were not available): one
company with 30,000 classified runs, 6,000 unclassified finished runs of
other tasks or of no task, each with an approximately 20 KiB hex
context, 400 other tasks, and 300,000 `activity_log` rows (half with a
run id). The target task has 40 classified runs, 200 activity rows, and
the missing rows of each case. Decoys: one `running` run of the task,
and one run in another company that names the task id. A small control
uses 500 classified and 50 unclassified runs.
- Each arm: 2 warm-up runs and 11 measured runs, in alternating order.

| Case | Candidates | Baseline median (min to max) | New median (min to
max) | Speedup | Shared buffers (baseline to new) |
| --- | --- | --- | --- | --- | --- |
| Task with nothing to repair | 0 | 37.2 ms (31.1 to 44.1) | 1.21 ms
(0.42 to 1.79) | 30.8x | 34,247 to 138 |
| Task with 3 missing | 3 | 37.8 ms (27.2 to 39.6) | 1.16 ms (0.77 to
2.05) | 32.6x | 34,265 to 148 |
| Task with 25 missing | 25 | 37.5 ms (34.6 to 69.8) | 1.63 ms (1.06 to
3.47) | 23.0x | 34,361 to 154 |
| Small control | 3 | 0.79 ms (0.70 to 1.06) | 0.70 ms (0.48 to 1.70) |
1.1x (neutral) | 314 to 18 |

The `EXPLAIN ANALYZE` execution time for the large cases goes from
approximately 35 ms to approximately 0.15 ms. These are local SQL
measurements. The hosted latency improvement is not yet measured.

The benchmark also checks result equality. For each case, it runs both
queries without a limit and compares every column of every row (as
sorted positional rows). It also checks that the limited batch of 20
comes from the baseline candidate set. All cases match.

Beyond the query, the skip window removes the lookup for most reads of a
task with nothing to repair.

Tests:

```sh
cd server
pnpm exec vitest run src/__tests__/activity-service.test.ts   # 24 passed
pnpm exec vitest run src/__tests__/activity-routes.test.ts src/__tests__/task-cold-open.test.ts src/__tests__/liveness.test.ts
pnpm exec tsc --noEmit -p .
```

- The skip-window test fails on the pre-fix `activity.ts` (red), and
passes with this change (green).
- `pnpm verify` asks for broader coverage, because the change adds a
script. Required CI supplies that coverage.

## Risks

- A run that misses its classification after a pass finds nothing for
its task gets repaired up to 10 minutes later. Normal runs get their
classification at finalization, so only the repair path is late.
- The skip map is in memory for each server process. A restart clears
it. That only causes one more lookup for each task.
- The new query keeps the same rows, filter, and limit. The query has no
order, as before. Thus the 20 rows that a pass picks can differ when a
task has more than 20 candidates. Later reads repair the rest in both
versions.
- Low risk. No migration and no API change.

## Model Used

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

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 17:37:08 -06:00
DottaandPaperclip 199670ba99 fix(accounting): prevent usage receipt stalls during streamed run updates (#20385)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native Runner commits usage receipts before it acknowledges retained
events.
> - Usage receipts and streamed run logs update the same run row.
> - A repeatable-read snapshot can become stale before the receipt
writer locks that row.
> - Continuous log writes can exhaust all accounting retries and stall
the retained event.
> - This pull request uses the existing run lock with READ COMMITTED for
standalone receipt writes.
> - Other accounting operations keep their consistent snapshot. Stall
diagnostics retain safe nested error codes.

## Linked Issues or Issue Description

Related: #20235.

**What happened?**

A hosted native run stopped on a retained usage event. Its diagnostic
reported `Error at source seq N`. The saved outbox and accounting
records locate the failing delivery boundary. Traces show repeated
accounting transactions while streamed events still updated the run row.
The historical inner exception is unavailable.

A controlled PostgreSQL reproduction proves a matching defect: a run-log
append after company validation makes the receipt writer's
repeatable-read snapshot stale. Repeated appends exhaust all four
retries with SQLSTATE `40001`. This is a demonstrated failure mechanism;
attribution of the historical incident remains an inference.

**Expected behavior**

Commit the receipt under its existing run-row lock despite concurrent
run-log writes. Observe concurrent finalization and preserve
late-receipt debt. Keep duplicate receipts idempotent and reject
conflicting or cross-company receipts.

**Steps to reproduce**

Run `server/src/services/usage-receipts-read.test.ts`. The concurrency
regression uses real PostgreSQL and the production run-log append
function. It commits a log append after company validation and before
the receipt locks the run. Before the repair, the receipt rejects with a
wrapped `40001` after four attempts. After the repair, it commits once.
The finalization variant verifies that a newly settled run receives
late-receipt debt without replacing settled state.

## What Changed

- Allow aggregate-lock-protected accounting writers to select READ
COMMITTED. Keep repeatable read as the default.
- Use READ COMMITTED only for standalone usage receipt writes. Keep the
existing run lock, immutable receipt journal, replay checks and
transactional budget-work publication.
- Add PostgreSQL regressions for concurrent log appends, concurrent
finalization, duplicate delivery and the unchanged default snapshot.
- Read the outer event-commit error and at most three nested causes for
an existing safe error code. Exclude exception messages and SQL.
- Document receipt isolation and bounded stall diagnostics.

## Verification

- Red: the production receipt writer exhausted four retries with
PostgreSQL `40001` under controlled concurrent run-log appends.
- Red: wrapped database and transport stall diagnostics returned `Error`
instead of `40001` and `ECONNRESET`.
- Green: Runner control-plane tests passed all 207 cases. All 68 server
assertions passed across receipt reads, native usage delivery and cost
accounting. Cost-accounting teardown exceeded its 30-second limit twice
on this loaded host, after all 28 assertions passed. The workspace
project retained its configured 30-second hook limit despite the CLI
override. This is a local suite cleanup failure, not a passing suite;
the same revision passed the complete server suites in CI.
- Server layered TypeScript checking passed, including source and test
layers. Runner TypeScript checking passed. Server checks cover
`ccfe3bdeb1ab88537a03fd6af9044f66863939a5`; the unchanged Runner checks
cover its parent.
- Local environment: macOS arm64, Node 26.4.0, disposable embedded
PostgreSQL. Native usage-delivery tests use the real Rust receiver.
- CI at `ccfe3bdeb1ab88537a03fd6af9044f66863939a5`:
[build](https://github.com/paperclipai/paperclip/actions/runs/42714760702/job/129493568428),
[typecheck](https://github.com/paperclipai/paperclip/actions/runs/42714760702/job/129493568367),
native Runner integration, browser shards and all nine serialized server
groups passed. All 12 general server shards passed in the [full
run](https://github.com/paperclipai/paperclip/actions/runs/42714760702).
Final rollup: 59 successful checks/statuses, three intentionally skipped
checks, no pending or failed checks.
- [Greptile reviewed this revision at 5/5 with no actionable
findings](https://github.com/paperclipai/paperclip/pull/20385#issuecomment-6753163397).
- Hosted evidence came from the task, run, saved sandbox outbox,
accounting receipts and traces. The run started fresh and did not cross
a deployment. No original agent turn was replayed.
- The hosted repair has not been deployed. The original task has not
been resumed. A fixed-revision remote-provider user journey remains
unverified.

## Risks

- READ COMMITTED changes snapshot timing for standalone receipt writes.
The run-row lock protects all mutable receipt/accounting reads and
serializes competing receipts and finalization. Other accounting
operations retain repeatable read.
- Receipt identity, replay conflict checks, company scoping, budget
publication, retry bounds and acknowledgement rules remain intact.
- No persisted format, provider identity or database schema changes.
Existing saved state needs no migration.
- Diagnostics inspect at most four objects and retain only the existing
identifier format, with an 80-character code limit.
- The reproduced race matches the incident boundary and timing, but the
original inner exception was not recorded. Rollout and live acceptance
remain necessary before incident resolution.

## Model Used

- OpenAI Codex, GPT-6 family, with code and shell tools. The exact model
ID and context window are not exposed in this session.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-09 17:35:13 -06:00
DottaandPaperclip bf818280f2 perf(workspaces): reserve cohort members concurrently (#20401)
## Thinking Path

> - Paperclip controls agent work and preserves its workspaces.
> - The online importer moves up to eight shared-source leaves under one
source fence.
> - Each member must have a durable reservation before capture starts.
> - Serial reservations repeat the same validation cost before any
archive work.
> - This pull request reserves members in parallel through their
existing independent transactions.
> - Every reservation finishes before an error can release the fence or
start capture.

## Linked Issues or Issue Description

Refs: #19873, #20387, #20394.

**Current behavior**

A measured six-member cohort spent about 110 seconds in serial
reservations. Each reservation keeps its existing source, task, recovery
and journal checks.

**Proposed behavior**

Start the bounded member reservations together. Join all outcomes,
retain member order, and refuse capture if any member fails. Keep the
exact partial-journal resume contract.

**Reason and benefit**

This removes serial metadata work before capture. Hosted speedup has not
been measured for this change.

## What Changed

- Replace only the cohort reservation caller loop with
`Promise.allSettled`.
- Preserve the existing per-member transaction, preassigned operation
ID, source fingerprint and all source fences.
- Extend the real PostgreSQL failure fixture to require overlapping
reservations and complete peer closure before fence release.
- Document the unchanged partial-journal and atomic publication
contract.

## Verification

- Canonical cached worktree preflight and plugin SDK prerequisites
passed on Node 24.22.2.
- The unchanged production baseline failed the new reservation overlap
assertion: one reader entered instead of two. The canonical PostgreSQL
wrapper cleaned up. The other 21 tests were intentionally not selected.
- The first baseline command failed before PostgreSQL because root-level
`tsx` resolution was absent. The corrected command uses the checkout's
explicit `cli/node_modules/tsx/dist/loader.mjs`.
- The four focused PostgreSQL controls passed before the final rebase:
overlap/join failure, exact partial-journal resume, parallel capture
with both cold restores, and atomic outer-commit rollback. Eighteen
tests were intentionally not selected. Server TypeScript passed.
- The final branch is based on merged #20394 at
`302ada90dfda0e8fbcd8a514d506d310c9843bcb`. The two directly affected
PostgreSQL reservation/resume controls passed again on final head
`06d436e45705da3b616a2b91725960f6e0ea52c0` (2 passed, 20 intentionally
not selected; 100.56 seconds). Server TypeScript passed again. The
canonical wrapper completed cleanup.

- The separate Ponytail review remains 4/5. Its nonblocking fan-out
warning was resolved as the documented phase contract; no source change
was required. Optional multi-error aggregation is outside this change.
- Fresh Greptile review is 5/5 on
`06d436e45705da3b616a2b91725960f6e0ea52c0`, with no actionable findings
or unresolved threads:
https://github.com/paperclipai/paperclip/pull/20401#issuecomment-6754025480.

- Exact-head CI is complete: 56 success, 4 skipped and 1 neutral, with
no failed or pending checks. Aggregate `ci / verify` passed:
https://github.com/paperclipai/paperclip/actions/runs/42718439355.
- Reproduce the final focused cases on Node 24: `node --import
./cli/node_modules/tsx/dist/loader.mjs
packages/db/src/test-postgres-command.ts pnpm exec vitest run
server/src/services/remote-workspace-cohort-migration.test.ts -t "joins
concurrent reservation validation readers|resumes the exact membership
after reservation stops halfway"`. Run `pnpm --filter
@paperclipai/server exec tsc --noEmit` for TypeScript.

## Risks

- API phase contract: `concurrency` limits capture workers. Preflight,
reservation and post-finalizer validation fan out over the exact
selected member set, with the canonical maximum of eight. The original
DB pool bounds connections. `concurrency: 1` does not promise one
metadata query or transaction at a time.
- Up to eight reservations can use independent pool connections at once.
Members that pass can leave durable journals when a peer fails. The
existing exact-member resume path retains those journals; this change
does not retry automatically.
- Capture and publication remain blocked until every reservation
settles. Publication stays serial in one transaction. Saved journal
schemas, singleton behavior and recovery proof checks do not change.
- This does not change concurrency inside the publication transaction.
One PostgreSQL connection serializes that SQL, and changing its row-lock
order has no measured benefit here.

## Model Used

OpenAI GPT-6 (Codex), with tool use and code execution. The runtime does
not expose a more specific model identifier or context-window size.

## Checklist

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

> - Paperclip is the open source app people use to manage AI agents for
work
> - Before a task reaches the agent, the server prepares the run: it
reads the task, the workspace and the configuration, and then hands the
run to the native controller
> - With an OTLP endpoint, each preparation step emits a
`heartbeat.prepare.<phase>` or `native.controller.<phase>` span
> - On a hosted test deployment, 0.3 to 0.5 s of each `task.prepare`
sits between those spans, and about 0.5 s more sits between
`execution.dispatch_admission` and `native.coordinator.claim`
> - Nobody can say which step is slow in that time, so latency work in
this area is guesswork
> - This pull request adds spans around the existing awaited work in
those gaps, with the existing timing helpers and naming conventions
> - The benefit is that a trace of a slow preparation now names the step
that used the time

## Linked Issues or Issue Description

**What existing behavior does this improve?**
The OpenTelemetry trace of task preparation. Today it has unnamed gaps
between the named preparation spans.

**Subsystem affected**
Server: heartbeat run preparation and the native controller
(`server/src/services/heartbeat.ts`,
`server/src/services/native-runtime/native-session-executor.ts`).

**Current behavior**
On a hosted test deployment (8 runs per group), `task.prepare` takes 2.6
to 3.4 s. Between `heartbeat.prepare.runtime_state` and
`heartbeat.prepare.run_identity`, about 0.25 s has no span. Between
`execution.dispatch_admission` and `native.coordinator.claim`, about 0.5
s has no span, and in one pair of runs it was 1.4 s. Nobody can tell
which step used that time.

**Proposed behavior**
Spans name the existing awaited reads and writes in those gaps. The
change adds no reads and records no content. The operator endpoint gate
and the no-operation behaviour stay.

**Reason and benefit**
Latency work in this area needs a measurement. With these spans, a trace
of a slow preparation names the step that used the time, so a fix can be
confirmed or rejected after deployment.

**Breaking changes**
None. The change adds span names to the closed list. It changes no API,
schema or behaviour.

**Additional context**
The server uses postgres.js, which the auto-instrumentation does not
cover, so database spans do not fill these gaps.

## What Changed

- `server/src/services/execution-preparation-timing.ts`: add the new
closed phase names.
- `server/src/services/heartbeat.ts`: add `heartbeat.prepare.*` spans
for `chat_retry_admission`, `workspace_selection`,
`dependency_readiness`, `issue_checkout`, `instance_settings`,
`workspace_context`, `project_context`, `plan_wake_routing`,
`routine_environment`, `responsible_user` and `execution_start`. The
re-reads of the issue after a checkout use the existing `issue_context`
name.
- `server/src/services/native-runtime/native-session-executor.ts`: add
`native.controller.*` spans for `attachment_staging`, `warm_restore`,
`span_persistence` (persistence of the preparation spans to the run log)
and `state_migration` before the coordinator claim.
- `doc/observability.md`: list the new spans.
- `server/src/__tests__/heartbeat-preparation-gap-spans.test.ts`: run a
real heartbeat through native preparation with an in-memory span
exporter. Check that each new span is emitted and that the new
preparation spans are children of `task.execution_attempt`.

## Verification

- `pnpm --filter @paperclipai/server typecheck`
- `npx vitest run src/__tests__/heartbeat-preparation-gap-spans.test.ts`
(fails without the source change, passes with it)
- `npx vitest run
src/services/heartbeat/preparation-timing-context.test.ts
src/services/execution-preparation-timing.test.ts
src/services/native-runtime/native-session-executor.test.ts
src/__tests__/heartbeat-context-compaction-execution.test.ts`

## Risks

- Low risk. The change only wraps existing awaited calls. Values and
errors pass through the timing helper unchanged, and a checkout conflict
still reaches the same `catch`.
- Phases of 50 ms or more inside `heartbeat.prepare.*` are also written
to the run log, as for the existing phases. This adds a few run-log rows
on slow runs.
- Without an OTLP endpoint, the helpers keep their current no-operation
behaviour.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 15:27:58 -08:00
DottaandClaude Opus 5.5 09241442ea fix(server): let a native reviewer approve its review stage (#20373)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Execution policies can require an independent review stage before a
task is done
> - A native (Runner) agent that is the active reviewer had no way to
record its decision
> - `PATCH status: "done"` is refused for a native run that owns the
task (#18411), and `paperclip_finish` accepts the report but leaves the
stage pending
> - The reviewer then searches the API until a deadline, and the task
stays in review with nobody able to decide it
> - This pull request records the final-stage approval on `PATCH`
without releasing the run's lock, and lets the run's own finish report
set the final status
> - The benefit is that an agent reviewer can actually approve its
review stage, in one short turn

## Linked Issues or Issue Description

Refs #18411 (added the native status guard). Related: #20264 (explicit
stage-decision operation, still open; this change is narrower and
compatible with it).

**What happened?**

A native reviewer agent was woken as the active `review` participant. It
read the deliverable and wrote its review in about 100 seconds. It then
called `paperclip_finish` with `done`. The tool answered "Completion
report accepted; the task's configured review stage is still pending".
It then tried `PATCH /api/issues/{id}` with `status: "done"` and a
comment. The route refused it with
`native_run_status_reported_by_finish`. The agent searched the API for 2
more minutes and found no other path. The stage stayed pending. When the
run ends with `done`, the status arbiter sees the pending stage and
keeps the task in review (`governed_gate_pending`), so the stage can
never resolve.

**Expected behavior**

The active review/approval participant can approve its stage from a
native run. The task reaches `done` after the approval. The implementer
still cannot approve. A run that is not the active participant still
cannot change the task status.

**Steps to reproduce**

1. Create a task with an execution policy that has one `review` stage
with a native agent participant.
2. Move the task to `in_review` so the stage is pending and the reviewer
agent wakes.
3. In the reviewer run, call `PATCH /api/issues/{id}` with `{ "status":
"done", "comment": "Approved: ..." }`.
4. Observe `409 native_run_status_reported_by_finish`. Call
`paperclip_finish` with `done` and observe that the stage stays pending.

## What Changed

- `PATCH /api/issues/:id`: when the requester is the native run that
holds the task lock and is the active participant, a `status: "done"`
request on the final stage now records the approval decision
(`executionState` completed, decision row, activity). The route removes
`status` from the write, so the task stays `in_review` and the run keeps
its execution lock. The run's `paperclip_finish` report then sets `done`
through the normal status arbiter.
- An intermediate-stage approval (which would hand the task to the next
participant) and every other status change from such a run keep the
existing `native_run_status_reported_by_finish` refusal.
- `paperclip_finish` from the active participant (`done` or
`needs_review`) while its stage is still pending is now rejected with
the exact decision path, instead of being accepted with "stage still
pending".
- The native finalizer now treats the final-stage approval that the run
itself recorded (read from the `issue_execution_decisions` row, bound to
this run and agent) as the completion authority for its clean `done`
report. Before this, a reviewer that cited free-text evidence had its
report graded as incomplete, and the task was reopened as `in_progress`
with a new wake for the reviewer. Failed verification, blocking
remaining work, attention requests, other dispositions, and a
still-pending stage keep the old outcome.
- The reviewer/approver wake text names the decision path (`PATCH` with
`status` and a required decision comment) and says a completion report
does not record the decision. A Runner reviewer that needs changes is
told to report them with `paperclip_block`, because a changes request
from the run that owns the task is a reassignment that stops that run.
- The locked review-policy recheck treats the deferred approval as a
verdict, so a policy changed to `human_only` before the locked write
still refuses it.

## Verification

- `pnpm exec vitest run
server/src/__tests__/issue-agent-mutation-ownership-routes.test.ts
server/src/__tests__/issue-execution-policy-routes.test.ts
server/src/__tests__/issue-comment-reopen-routes.test.ts
server/src/__tests__/issue-execution-policy.test.ts
packages/adapter-utils/src/server-utils.test.ts`: 5 files, 595 tests
pass.
- `pnpm exec vitest run
server/src/services/native-runtime/native-completion-feedback.test.ts`:
29 tests pass (embedded PostgreSQL), including the decision-row binding
for `executionStageApprovedByRun`.
- `pnpm exec vitest run
server/src/services/native-runtime/status-arbiter.test.ts
server/src/services/native-runtime/native-run-finalizer-telemetry.test.ts
server/src/services/native-runtime/native-stale-owner-completion.test.ts`:
72 tests pass.
- New route tests fail without the route change (409 instead of 200/422)
and pass with it. The new feedback test fails without the feedback
change ("Completion report accepted" instead of a rejection).
- `npx tsc --noEmit -p server` and `-p packages/adapter-utils`: pass.
- A route test changes the review policy to `human_only` between the
first check and the locked write; the deferred approval is refused
(403).
- Live full-stack chat eval `task-coordination.independent-review`
(Runner, OpenCode) with this change: passed in GitHub Actions run
42705964209 (eval branch with the first two commits cherry-picked). The
reviewer run recorded its approval 24 s after its review brief was
released, reported `done` 14 s later, and the finalizer set the task to
`done` with reason `execution_stage_approved`; total reviewer run 73 s.
The earlier run 42702224713 exposed the finalizer evidence issue that
the second commit fixes.

## Risks

- Low to medium. The change is limited to one request shape: a native
run, holding the lock, that is the active participant of a pending final
stage, sending `status: "done"`. All other requests keep the old guard.
- Between the approval and the finish report, the task shows `in_review`
with a completed `executionState`. If the run fails before it reports,
the existing native recovery owns the task, as for any other run.
- Previous-release story: no persisted shape changes. A reviewer run
that started before the deploy could not have recorded a deferred
approval (the old route refused it), so the new finalizer input is false
for it and the old outcome applies.
- Intermediate-stage approvals and changes requests from native runs are
still refused by the route. A Runner reviewer reports required changes
with `paperclip_block`, which keeps the stage pending for a person. A
self-service changes request needs the deferred handoff semantics in
#20264.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
canary/v2026.1109.0-canary.18
2026-11-09 17:11:42 -06:00
DottaandPaperclip 302ada90df fix(workspace): refresh source admission proofs on import resume (#20394)
## Thinking Path

> - Paperclip preserves agent work and recovery obligations across
server replacement.
> - Online workspace imports keep durable operation and snapshot
journals for interrupted work.
> - A recovery admission fingerprint hashes complete run, finalizer, and
lease rows.
> - Accounting updates can change that hash while every saved recovery
reference remains identical.
> - Resume currently treats that expired admission observation as
immutable source identity and refuses the existing journal.
> - This pull request refreshes only that observation after exact
identity checks and finalization fencing, preserving the original
archive and journal.

## Linked Issues or Issue Description

**What happened?**

An interrupted import whose saved recovery sources have no changing
online recovery hold can fail on its next attempt with
`remote_workspace_migration_source_changed`. A change to a run's
accounting fields changes `onlineProof.fingerprint`, even when all eight
saved recovery records have the same typed references and ownership
fields.

**Expected behavior**

For source-only admission drift, resume must compare every saved
recovery source field exactly, except the supported ephemeral admission
fingerprint. It must inspect the current source again, acquire the
existing finalization fence, and retain strict full-proof checks through
publication. It must keep the original operation, snapshot, archive,
nested-child scope, and cohort identity.

**Steps to reproduce**

1. Save a journal for an online import with eight recovery sources and
no active online recovery holds. Interrupt after real authenticated
restore of its uploaded snapshot.
2. Change only accounting fields on source 6's saved run.
3. Resume the same journal. The unchanged importer refuses it. The fixed
importer preserves the original identities and commits after fresh
guarded inspection.

**Paperclip version or commit**

Reproduced on master `9c634e42e449e91f3935cf59036981852f94a65e`.

**Deployment mode**

Online administrative imports with PostgreSQL. Related preservation
work: https://github.com/paperclipai/paperclip/pull/20311 and
https://github.com/paperclipai/paperclip/pull/20370. The independent
readonly parallelism work in
https://github.com/paperclipai/paperclip/pull/20387 changes different
loops.

## What Changed

- Compare the complete `legacyRecoverySources` records at resume,
neutralizing only a valid fingerprint under the supported proof schema.
Preserve every other field and refuse unsupported proof versions by
name.
- Reuse the current canonical full proofs. Persist them with an
operation metadata compare-and-swap only after the existing finalization
lock and strict reinspection succeed.
- Keep strict full-proof checks within each attempt and before
publication. Preserve all journal, snapshot, archive, nested-child, and
cohort identities.
- Add focused real PostgreSQL regressions in the existing online-source
fixture, including exactly eight saved sources and repeat-apply
idempotence.

## Verification

- Genuine baseline RED: singleton and cohort resume both fail after
accounting-only changes on the unchanged product.
- `pnpm exec vitest run
server/src/services/remote-workspace-online-source.test.ts -t 'eight
saved recovery|changed saved recovery|recovery accounting changes|saved
recovery proof'`: 8 passed.
- Controls refuse changed valid references, a matched run/lease
allocation change, competing ownership, and unsupported proof versions.
They also prove that bookkeeping changes within an admitted attempt
still refuse and a busy finalization fence prevents proof refresh.
- Successful resumes preserve the operation/snapshot IDs, cipher SHA,
manifest digest, byte size, nested scope, and cohort. Repeat apply
creates no duplicate operation or snapshot.
- Server layered typecheck, module boundaries, and whitespace checks
passed. All applicable exact-head CI passed (including typecheck, build,
all test shards and aggregate verify); hosted acceptance remains owned
by the deployment workstream. Greptile re-reviewed the narrowed scope at
5/5. Ponytail scored 4/5, recommending a regression for the unchanged
hold-bearing refusal; the maintainer explicitly kept that separate
behavior outside this source-only fix.

## Risks

This changes only source-proof resume admission. `onlineRecoveryHolds`
and their action/intent fingerprints remain compared exactly, as on the
baseline: a hold-bearing journal whose hold proof changes still refuses.
Supporting that separate case would require preserving and validating
the hold owner’s complete intent facts; this PR deliberately retains
that safety check. Bookkeeping that changes during an active attempt
still causes the existing strict publication checks to refuse that
attempt. Older unsupported proof shapes remain refused. The metadata
update uses the existing company/workspace/operation identity,
running/unfinished status, and metadata compare-and-swap. No schema,
provider, or storage format changes are included.

## Model Used

OpenAI Codex, based on GPT-6, with tool use and code execution. The
exact deployment identifier and context-window size are not exposed in
this session.

## Checklist

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

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-09 17:05:17 -06:00
Nicky LeachandPaperclip 1838de8c6c Pause agents automatically when moving to a plugin environment (#20375)
## Thinking Path

> - Paperclip manages AI agents and their execution environments.
> - Core owns agent lifecycle state and controls when work can run.
> - A plugin environment can need a computer that belongs to the agent.
> - Moving a ready agent to that environment currently requires manual
pause and resume actions.
> - Saving the environment is sufficient intent to perform those routine
steps.
> - This change records the pause with the environment update and uses
the existing lifecycle worker.
> - Work can resume only after execution stops, the plugin starts its
resources, and verification passes.

## Linked Issues or Issue Description

Related: #19173 introduced environment-scoped plugin selection and the
manual-pause requirement. I searched for related pull requests and found
no duplicate fix. No separate issue is linked.

**What happened?**

Saving a ready agent with an environment whose lifecycle plugin is not
selected returns a conflict. The error tells the user to pause the
agent, change its environment, and resume it.

**Expected behavior**

Saving the environment change starts the required pause, resource setup,
and verification automatically. Existing manual, budget, and company
holds still block resume.

**Steps to reproduce**

1. Use an agent in `ready` on an environment without an agent-owned
plugin.
2. Select an agent-owned plugin environment in the agent configuration.
3. Save without first pausing the agent.

Before this fix, the save fails. With this fix, the agent moves through
`pausing`, `resuming`, and `verifying` before `ready`.

## What Changed

- Save the new environment and automatic pause in the same transaction.
Keep the existing plugin selection during stop.
- Use the existing completion of a pause without holds to resume
automatically. Set the saved resume target to `verifying` so the new
environment must pass its test.
- Preserve independent pause holds, lifecycle version checks,
notifications, resource status events, and worker recovery.
- Add migration tests for idle and running agents, real process-stop
proof across a worker restart, added holds, failed verification, and
route authorization.
- Update the lifecycle and development docs. Keep the feature maps
unchanged, as required for a small behavior fix.

## Verification

- Current revision: `300f57539bce9b198406c3793ab3c2de46492659`.
- Lifecycle and case API discovery suites passed after updating the
branch to master. The final environment-plugin suite passed all 22
tests, including four real-route authorization cases for users and
agents. Coverage includes real process-stop proof across worker
replacement, independent holds, and failed migration verification.
- Server typecheck: passed on the current base. All workspace typechecks
and `pnpm build` passed before the base update; fresh CI covers the
updated base.
- `node scripts/check-module-boundaries.mjs`, `node
scripts/check-maps.mjs`, and `git diff --check`: passed.
- Reviewed the diff for secrets, personal data, private addresses, and
run artifacts before each push.
- [Current
CI](https://github.com/paperclipai/paperclip/actions/runs/42714733533):
passed on `300f57539bce9b198406c3793ab3c2de46492659` (52 successful
jobs; the optional test-selection optimization was skipped). Standard
Greptile: 5/5 on the current revision, with no unresolved findings. The
security scanner accepted the documented configuration-authority policy.
- Earlier CI failed the `case-history-api-discovery` assertion for the
query `case for task`. It also failed locally. The branch now includes
master's fix #20382, and that test passes. The migration implementation
did not change. The superseded CI attempt was cancelled after recording
the failure.
- Full local `pnpm test:run`: incomplete; stopped after two passing chat
partitions. The focused suites passed, and required CI supplies broad
coverage, as specified in the repository instructions.
- Local target: isolated source checkout on macOS ARM64, Node 26,
disposable PostgreSQL, fixture lifecycle plugin. Initial database
startup failed because the bundled macOS PostgreSQL package lacked dylib
aliases. Temporary dependency aliases repaired startup; the focused
database tests then passed. The installed Rust toolchain also needed to
be added to PATH.
- The pending-stop test retains a live process ID in the cancelled run.
The real host stop check blocks plugin setup. After stop acknowledgment
and worker replacement, the saved migration reaches readiness.

## Risks

- Saving this environment change can stop current execution. The system
performs that pause as part of the requested migration.
- A failed stop, plugin call, or verification keeps the agent blocked
and uses existing retry behavior.
- Editing a shared environment or the instance default still uses its
existing conflict checks. This change covers the individual agent
environment update.
- Configuration permission authorizes the temporary system pause needed
for the resource move. Separate manual-pause authority is not required
for this configuration operation. Real route tests cover authorized and
denied users and agents.
- No schema migration or deployment is included. A fixture plugin does
not prove live computer provisioning or model execution.

## Model Used

OpenAI Codex, based on GPT-6, with code execution and tool use. The
exact runtime model ID and context limit 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-09 15:02:54 -08:00
DottaandPaperclip ec75547a2e perf(workspaces): validate cohort members concurrently (#20387)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Online workspace migration preserves each member under shared source
locks.
> - A cohort must validate every member before it reserves or captures
state.
> - Two read phases checked each member in sequence.
> - An eight-member operation spent about eight minutes in those phases
before capture.
> - This pull request runs the bounded readers together and joins all
results.
> - Each source check, lock, and write order stays in place.

## Linked Issues or Issue Description

**What happened?**

An eight-member cohort spent 4 minutes 13 seconds in preflight, then 2
minutes 20 seconds in serial reservation, then 4 minutes 10 seconds in
the read checks after finalizer locks. Capture started only after these
phases. The two read phases invoked the same checks in sequence for each
member.

**Expected behavior**

Run these read checks together within the existing eight-member limit.
Wait for every started reader before throwing or releasing the source or
finalizer fence. Keep reservation and publication serial.

**Steps to reproduce**

1. Import a shared-source cohort with several members.
2. Delay one member's preflight or recovery inspection.
3. Observe that the old loop does not start the next reader until that
inspection ends.
4. Fail one reader while another is still running. The fence must remain
held until both readers finish.

**Paperclip version or commit**

Reproduced baseline: `facd409073c3c149421fd8f5af86c571fc28f294`. The
branch now includes `9c634e42e449e91f3935cf59036981852f94a65e` and its
merged queue-owner change. The queue hunks are unchanged.

**Deployment mode**

Server with PostgreSQL and online workspace migration.

Related: [cohort migration in
#19873](https://github.com/paperclipai/paperclip/pull/19873), [indexed
workspace lookups in
#20372](https://github.com/paperclipai/paperclip/pull/20372), and
[queued source-lock admission in
#20370](https://github.com/paperclipai/paperclip/pull/20370). The
duplicate search found no open change for these two read phases.

## What Changed

- Run cohort preflight member checks with `Promise.allSettled`. The
existing input limit bounds this phase to eight readers.
- Run the member checks after finalizer lock acquisition the same way.
- Throw the first rejected result in the original member order after
every reader has joined.
- Add one fault fixture for both phases. It checks real source/finalizer
locks, peer joining, retained local data, and the absence of capture or
publication.

## Verification

- Against the unchanged baseline, the two new phase cases failed because
only one reader started instead of two. These were behavioral failures,
not setup failures.
- Focused real-PostgreSQL cohort checks selected 13 cases. The first run
passed 10 and failed three existing controls. The two new reader-join
cases passed. Eight unrelated cases were not selected.
- The first run's existing capture overlap check observed one active
uploader instead of two. Two existing fence/commit controls reached
their 15-second test limits. Server typechecking ran at the same time.
This does not establish the cause of those failures.
- One retry ran only those same three controls after typechecking
finished. All three passed with unchanged source, assertions, and
deadlines. Eighteen cases were not selected. The earlier failures remain
accounted for; they are not classified as infrastructure faults or
flakes.
- `pnpm --filter @paperclipai/server typecheck` and `git diff --check`
passed.
- These focused counts were recorded before the non-overlapping
queue-owner rebase. On final head
`06158d905c4716bf44b1d028f96c706ac60e0e11`, both new reader-fence cases
passed again. Nineteen cases were not selected in this recheck.
- Focused commands:
- `pnpm --filter @paperclipai/server exec vitest run
src/services/remote-workspace-cohort-migration.test.ts -t
'joins|captures two shared-Git|before reservation|source
fingerprints|member journal|outer commit|fence connection'
--maxWorkers=1`
- Retry only: `pnpm --filter @paperclipai/server exec vitest run
src/services/remote-workspace-cohort-migration.test.ts -t 'captures two
shared-Git|actual outer commit|actual fence connection' --maxWorkers=1`
- Checks used Node 24.22.2, pnpm 9.15.4, and disposable PostgreSQL 18.1.
No hosted operation used this patch. End-to-end improvement is
unverified.
- Public commands, input limits, journal state, and recovery contracts
do not change. No command documentation update is needed.

## Risks

- Concurrent reads increase database and filesystem load while source
locks are held. The existing cohort input limit bounds each phase to
eight readers. Initial source discovery already uses the same bound.
- A failed phase waits for started peers. This retains the fence until
their work ends. Existing query and caller limits remain in place.
- Serial reservation and publication remain. Every freshness,
fingerprint, recovery, and publication check still runs.
- The local tests prove overlap and joining. They do not establish a
hosted speedup. Database contention can limit the benefit.

## Model Used

OpenAI Codex, GPT-6 family. The exact runtime model ID and context
window are not exposed in this session. Used reasoning, code editing,
shell tools, 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 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-09 16:49:25 -06:00
Nicky LeachandPaperclip 2550f399d9 fix(lifecycle): align stop proof for pre-dispatch resource waits (#20384)
## Thinking Path

> - Paperclip manages AI agents and controls when their work can run.
> - An agent pause waits until every earlier run has stopped.
> - Recovery already treats verified workspace and AI-capacity waits as
pre-dispatch outcomes.
> - The stop proof did not accept those same receipts. Historical waits
could keep an agent pausing forever.
> - This pull request shares the resource-wait classification and the
existing execution-event check.
> - The benefit is consistent stop evidence without a new lifecycle
command, database backfill, or host-boot assumption.

## Linked Issues or Issue Description

Refs #18147 and #17718.

This fixes the verified resource-wait case of #18147. It does not
certify ambiguous historical runs that have no positive non-execution
evidence.

An agent can have an old cancelled legacy run with `errorCode:
workspace_busy`, `executionStage: preparing`, and `executionRecovery: {
kind: workspace_wait, providerWorkStarted: false }`. No process, native
coordinator, or retained resource exists. Recovery recognizes this as a
resource wait, but pause previously required a stop acknowledgment from
an executor that never started.

### Credit and related work

Thanks to @br1an-c0des (#17692) and @crownlayerapp (#15383) for the
focused preparation and shared-admission analysis, @lucasweismann-ai
(#18072) for the durable dispatch-evidence and race analysis, and
@Pdesengrini (#18905) for the cancellation-receipt and process-identity
guard cases. We reviewed these alternatives before implementing this
smaller change. No contributor commits were cherry-picked.

The design uses receipts already written by core. It does not adopt a
reboot-time bypass, a new adapter-entry journal, or a pause-abort route.
#17236 and #20037 address the separate cost of history reads.

## What Changed

- Add one shared classifier for matching workspace and AI-capacity wait
receipts. Use it in legacy recovery and cancellation stop proof.
- Reuse the existing dispatch-event predicate instead of maintaining
another event list.
- Retain checks for native execution references, unresolved workspace
obligations, controller leases, and resource cleanup. A live controller
or cleanup still in flight cannot pass the proof.
- Add a focused PostgreSQL suite. It drives the real pause worker with
historical wait records and checks Retry eligibility against the same
evidence.
- Document the contract. No schema, API, UI, dependency, or scheduling
change is required.

## Verification

Source checkout on macOS arm64, Node 26.12.0, isolated local PostgreSQL
18, synthetic data only.

```sh
node node_modules/vitest/vitest.mjs run --root server \
  src/modules/agent-lifecycle/__tests__/resource-wait-stop.test.ts \
  src/services/legacy-execution-recovery.test.ts
```

- 61 tests passed across these two files.
- All eight positive regressions failed on unchanged master. The fix
restores them.
- After rebasing onto current master, the 61 focused tests passed again.
Another 53 selected tests passed in
`explicit-native-continuation.test.ts` with `-t 'pre-dispatch review
wait|unclaimed workspace retry|historical native preparation'`. The
other 361 cases in that file were not run.
- Guard cases cover wrong/missing receipts, dispatch events, process
identity, native ownership, live controller leases, workspace recovery,
cleanup in flight, unreceipted remote resources, and an unknown
historical HTTP dispatch.
- A remote lease passes only after its exact termination receipt is
stored.
- Secret/PII diff scan and `git diff --check` passed.
- Standard Greptile reviewed commit
`201a61d2bb63290ca9b9b9d524e6c6715a872310`: [5/5 with no actionable
findings](https://github.com/paperclipai/paperclip/pull/20384#issuecomment-6753165178).
No review threads are unresolved.
- Local testing is limited to affected cases. GitHub CI owns broad
typecheck, build, and test coverage.

Verification setup failures remain accounted for: worktree preflight
could not complete because Cargo is unavailable; the first bundled
PostgreSQL run failed because its macOS binary lacks a shared library.
The focused tests passed on a separate, temporary local PostgreSQL 18
cluster. An early local server typecheck could not resolve the unbuilt
Runner package; it is not claimed as a pass.

## Risks

- Stop proof now accepts existing positive resource-wait receipts.
Missing PIDs, terminal status, elapsed time, and a different boot ID
remain insufficient.
- The receipt must match its error code. The runtime must remain
unresolved, with a preparation or settled stage and no conflicting
native or workspace references.
- The shared cancellation proof is also used by explicit continuation
and Retry. Those paths keep their existing user-intent, ownership,
pause, budget, and authorization gates. A stop proof does not itself
start another run.
- Runs that lack usable evidence remain held. This PR does not claim to
repair every run shape reported on #18147.
- No migration or live data edit is needed. No hosted deployment is
included.

## Model Used

OpenAI GPT-6 through Codex, with repository inspection, code execution,
PostgreSQL regression tests, and GitHub tools. The exact serving
revision, context window, and internal thinking configuration 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 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-09 14:40:14 -08:00
DottaandPaperclip 9c634e42e4 fix(workspace): bound queued source-lock admission for imports (#20370)
## Thinking Path

> - Paperclip manages AI agents and their work.
> - Existing local workspaces need durable imports before a volume-free
deployment.
> - Imports must acquire the same source locks as workspace writers.
> - Try-only admission can repeatedly lose to background maintenance,
even when each writer finishes.
> - This pull request adds an optional bounded wait to the canonical
migration APIs.
> - Operators can join the source-lock queue while all existing
preservation checks remain active.

## Linked Issues or Issue Description

**What happened?**

An online import can repeatedly return `workspace_source_busy` while
background workspace maintenance reacquires its project source lock. The
import does not join the PostgreSQL lock queue.

**Expected behavior**

An administrative import can wait for a finite writer to finish. One
wait budget covers the complete source scope. Timeout or cancellation
must release acquired locks before the import reserves a journal.

**Steps to reproduce**

1. Hold the source lock for an eligible local workspace.
2. Start an online import while the writer holds that lock.
3. Release the writer. Before this change, the import has already
refused admission. With `sourceLockWaitMs`, the import acquires the
source scope and commits normally.

**Paperclip version or commit**

Reproduced on master `0e3fdebea8257bdff11573081c76b306f12db230`.

**Deployment mode**

External PostgreSQL with an online workspace import. Related
preservation work: https://github.com/paperclipai/paperclip/pull/20311.

## What Changed

- Add optional `sourceLockWaitMs` to singleton and cohort migration
APIs. Omitted options keep current admission behavior.
- Queue the sorted source keys with one decreasing wait budget. Abort
cancels only the dedicated backend identified by PID and backend start
time.
- Use an independent source transaction for queued singleton imports.
Keep lifecycle checks, fresh source checks, journal ownership, and
publication after commit.
- Document the canonical API option. Add real PostgreSQL importer
regressions for release, timeout, abort, and partial-key cleanup.
- Correct the inherited task-case lookup summary so the existing
search-discovery regression passes without changing its assertion or
search algorithm.

## Verification

- Baseline RED: the new singleton regression fails on the unchanged
product because the import returns busy without queueing.
- `pnpm exec vitest run
server/src/services/remote-workspace-migration.test.ts -t 'queued|bounds
the whole source queue'`: 20 passed, including five new cases.
- `pnpm exec vitest run
server/src/services/isolated-remote-workspace-source-lock.test.ts`: 4
passed.
- Server layer typecheck, `node scripts/check-module-boundaries.mjs`,
and `git diff --check` passed.
- Tests used Node 24.22.2 and the existing embedded PostgreSQL fixture.
Hosted rollout verification remains pending.
- CI build, typecheck, browser suites, all serialized suites, and the
other general shards passed. The final server shard failed only the
inherited `case-history-api-discovery.test.ts:14` search-ranking
assertion. Its unchanged local regression reproduces on the base and
passes 3/3 with a separate one-line route-summary correction. The shared
one-line summary correction is included in follow-up commit
`8163ffa9d4728134676b84c8c242a5bf62c802b5`; the unchanged case-discovery
suite passes 3/3 locally. The original CI attempt remains failed:
https://github.com/paperclipai/paperclip/actions/runs/42704666536/job/129458605783.

## Risks

Queued admission can hold earlier source keys while it waits for a later
key. The caller controls the finite budget. The abort path opens a
short-lived dedicated cancellation connection. If cancellation cannot
connect, PostgreSQL's remaining lock timeout still bounds the wait. This
option must be called at the root of administrative work; an external
scope that already holds the same source locks can cause
self-contention. The existing pre-held journal-resume path must keep its
omitted-option behavior.

## Model Used

OpenAI Codex, based on GPT-6, with tool use and code execution. The
exact deployment identifier 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
- [ ] 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-09 16:29:53 -06:00
DottaandClaude Opus 5.5 46ca7d4377 fix(api-search): stop function words from pushing case lookup off page one (#20382)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Chat and task agents find API operations with `search_api`, which
ranks the runner API catalog by word matches in each operation's method,
path and summary
> - The ranker scored function words such as "for", "the" and "of" like
resource words, so any summary that says "for" got 5 points for that
word alone
> - #20355 added two chat-bootstrap summaries that say "for up to ten
tasks" and "for one task". After that, the query "case for task" put
`GET /api/issues/{issueId}/cases` at rank 6, off the first page of 5
> - #20354 promised that agents find a task's case on the first search
page, and its test `case-history-api-discovery.test.ts` now fails on
master
> - This pull request stops the ranker from scoring function words, so
the resource words in the query decide the rank
> - The benefit is that master is green again and agents find the case
lookup (and other "X for an issue" reads) on the first page, whatever
new summaries say "for"

## Linked Issues or Issue Description

Refs #20354, #20355. Supersedes #19388 (the same ranker change, opened
earlier and not merged).

**What happened**
On a clean checkout of master at 0e3fdebea8 and later,
`server/src/__tests__/case-history-api-discovery.test.ts` fails:
"expected [ …(5) ] to include 'GET /api/issues/{issueId}/cases'".
`search_api` for "case for task" returns case handoffs, both
chat-bootstrap reads, task-drain, pending questions and privacy
constraints first. Most of these rank high only because their summaries
contain "for".

**Expected behavior**
An agent that searches "case for task" finds `GET
/api/issues/{issueId}/cases` on the first page. Words such as "for",
"an" and "the" do not change the rank.

**Steps to reproduce**
1. Check out master.
2. Run `cd server && npx vitest run
src/__tests__/case-history-api-discovery.test.ts`.
3. See the assertion failure for the "case for task" query.

## What Changed

- `server/src/services/native-runtime/runner-api-catalog.ts`: the search
tokenizer drops a small set of function words (`a`, `an`, `the`, `for`,
`of`, `to`, `in`, `on`, `at`, `by`, `with`, `and`, `or`, `from`) before
scoring. This applies to the query and to the catalog text.
- `server/src/services/native-runtime/runner-api.test.ts`: a regression
test. "list comments for an issue" returns the comments route first.
"queued comments", "activity", "runs" and "case for task" return their
routes on the first page. A query of only function words returns no
results.

## Verification

- Red on master before the change: `cd server && npx vitest run
src/__tests__/case-history-api-discovery.test.ts` fails (1 failed, 2
passed). Before the fix, the top 10 for "case for task" put the case
lookup at rank 6.
- Green after the change: `npx vitest run
src/__tests__/case-history-api-discovery.test.ts
src/services/native-runtime/runner-api.test.ts
src/__tests__/openapi-routes.test.ts
src/__tests__/agent-conversations.test.ts
src/__tests__/heartbeat-list.test.ts` passes.

## Risks

- Low risk. The change affects only `search_api` ranking. Exact
operationId lookup is unchanged. A query that has only function words
now returns no results instead of a list of arbitrary matches.
- Rank order can change for queries that used function words. The
existing ranking tests still pass.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), agentic coding with
tool use (shell, file edits) 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-09 16:28:42 -06:00
DottaandPaperclip 3cc1032f6a fix: deliver saved answers to dependency-blocked tasks (#20363)
## Thinking Path

> - Paperclip lets people direct agent work through tasks and structured
answers.
> - Dependencies stop task execution until prerequisite work finishes.
> - A person must still be able to answer or redirect the assignee of a
blocked task.
> - Ordinary comments already get a bounded answer/triage turn. A saved
question response has no comment ID, so the same gates reject it.
> - Later dispatch also requires an active task and tries to check it
out. That conflicts with bounded interaction on a blocked task.
> - This pull request verifies the stored human response at each gate
and keeps the turn in bounded interaction mode.
> - The assignee can receive the answer without starting dependency work
or removing its blockers.

## Linked Issues or Issue Description

Refs #19923. That change covers dependency waits and comment retries.
This change covers stored interaction responses that have no comment ID.

**What happened?**

A parent task waits on a parked child. The assignee asked a setup
question. After a person answers, the response stays queued. Sending
that saved response is refused as `issue_dependencies_blocked`. Ordinary
new messages can reach the same assignee.

Passing admission alone is insufficient. The dispatch status check
rejects a blocked task, and automatic checkout then tries to start work
whose prerequisites are unfinished.

**Expected behavior**

Deliver the stored human answer or confirmation to its requesting
assignee as a bounded answer/triage turn. Keep the task blocked and keep
its dependency relationships. Preserve current-owner, pause, approval,
budget, recovery, and execution gates.

**Steps to reproduce**

1. Assign a parent task and make a backlog child block it.
2. Save a question on the parent and answer it as a person.
3. Dispatch its resolved-interaction wake, or send the saved answer from
the task queue.
4. Observe that the answer does not reach the provider while an ordinary
comment can run in answer/triage mode.

**Paperclip version or commit**

Reproduced on master at `653ab8b18f76`. This branch is rebased onto
master at `0e3fdebea8`.

## What Changed

- Verify the stored response by company, task, requesting agent, status,
kind, human resolver, and resolution time. A context flag alone is
insufficient.
- Allow a verified response through dependency admission and claim in
the existing bounded interaction mode.
- Recheck that response during scheduled retry promotion and final
provider dispatch.
- Keep bounded responses from checking out the blocked task or setting
it to `in_progress`, including when the last blocker finishes during
preparation.
- Preserve continuation classification through workspace and AI-account
resource waits, so the final response and owner checks still run.
- Correct the task-case API summary so the existing `case for task`
discovery regression passes on the first search page. This fixes a
pre-existing failure exposed by full CI, without changing search ranking
or assertions.
- Ignore obsolete Retry notices after newer conversation work, without
acknowledging saved input or changing execution authority.
- Document the response contract. Add real PostgreSQL and
assembled-heartbeat regressions.

## Verification

- Final commit `ba4b337cf1bb0236b7c0a742665b222ee894ff51`: [required CI
passed, attempt
2](https://github.com/paperclipai/paperclip/actions/runs/42706363120/attempts/2),
including typecheck, production build, server/chat tests, Runner checks,
and all 8 browser shards. PR status: 59 passing checks and 3 normal
skips.
- Red: answered questions and accepted/rejected confirmations return no
admitted run before the fix. All three new positive cases fail at the
dependency gate. A resource-wait retry also reproduces as refused before
its gate is fixed.
- Green: at delivery revision `9375a603e4`, focused heartbeat
integration and classification passed 14 tests; retry scheduling and
dispatch safety passed 241 tests. These use real PostgreSQL and
controlled adapter and resource-availability boundaries. The wider
initial dependency, native-continuation, and queue suites also passed
before the final refinements.
- Negative cases keep pending, missing, wrong-task, and agent-resolved
responses blocked. Changing the response proof immediately before
dispatch cancels the run without calling the provider. Policy controls
retain the current assignee, task status, and pending approval checks.
- Positive cases execute once, leave the parent blocked, preserve the
dependency edge, and leave the child in backlog with no child execution.
The same saved response survives a resource wait; removing its human
proof refuses promotion.
- The resource-wait provider-boundary and blocker-completion regressions
also failed before their fixes. All 14 focused integration and
classifier cases passed at delivery revision `9375a603e4`, including a
changed response during retry preparation.
- Red: a retained Retry for an older run makes a newer successful
conversation display as failed. Green: all 78 database-backed query and
interruption-presentation tests pass, including a later real failure and
independent queued input controls.
- Server layered typecheck and module-boundary checks passed at
`11992aad0c7fbaf13e7548d71ee633560bd8bb6f`, before the one-line API
summary correction. That CI passed build, typecheck and every browser
shard, but exposed the pre-existing case-search assertion and a
report-assets test timeout. Final-head CI passed as linked above.
- Fresh Greptile review is 5/5 on final commit
`ba4b337cf1bb0236b7c0a742665b222ee894ff51`, with no unresolved threads.
- The existing API discovery suite reproduced its first-page assertion
before the summary correction; all 3 cases pass afterward. The timed-out
report-assets privacy test passed locally in isolation and the full
support job passed on the final-head rerun. Runner shutdowns interrupted
the first attempt; affected jobs were rerun once without code changes. A
separate onboarding timeout also passed on that rerun.
- `pnpm verify --explain` requests broad CI because the execution
contract is a global input. The final CI run supplied that coverage.
- Separately recovered the reported staging conversation through an
ordinary message and the existing steering control. The agent
acknowledged the message and the saved-answer queue drained. The fresh
Notebook action subsequently succeeded. This was on the previously
deployed revision; it does not claim this code is deployed.

## Risks

- This changes admission and dispatch. Each gate checks the persisted
response; no caller-supplied marker grants the exception.
- The bounded turn does not check out the task, resolve a dependency, or
approve another action. Existing pause, ownership, budget, approval, and
stopped-execution controls still apply.
- Each additional lookup is one primary-key read and runs only for a
response on a dependency-blocked path. There is no new polling worker.
- No migration is required. Older queued contexts get the exception only
when their stored human response still verifies. No prior run or action
is replayed by this change.
- This PR does not roll out the code to staging.

## Model Used

OpenAI Codex, GPT-6 family, with repository tools, code execution, and
browser inspection. The runtime does not expose the exact model ID,
context-window limit, or reasoning-mode label; those values are not
asserted.

## 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-09 16:27:34 -06:00
DottaandPaperclip eebdfe71b1 fix(ui): show submitted screenshots and Markdown in steering messages (#20367)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - People send follow-up messages while an agent works.
> - The queue above the composer holds those messages until delivery.
> - The queue showed raw Markdown and had no screenshot previews.
> - Submitted screenshots stayed in the composer until the save response
arrived.
> - This pull request shows formatted text and previews in the queue at
submission.
> - It keeps upload receipts so failed saves can restore the draft and
its files.

## Linked Issues or Issue Description

**What happened?**

Send a message with Markdown and screenshots while an agent works. The
queue shows Markdown source. It has no screenshot previews. The composer
keeps the submitted screenshots until the save response arrives.

**Expected behavior**

Show formatted text and screenshot previews in the queue. Clear the
submitted previews with the text. Restore them if the save fails. Keep
files added for the next message.

**Steps to reproduce**

1. Open the Screenshot Submission Storybook case under Steering and
queued messages.
2. Add a screenshot and formatted text. Select Send.
3. Leave the simulated save pending. Inspect the queue and composer.
4. Reject the save. Confirm the draft and screenshots return. Retry and
complete the save.

Related work: #19321 bounds the queued-message list. This fix keeps that
scrolling behavior.

## What Changed

- Hide submitted attachment previews as soon as Send clears the text.
Keep their upload receipts until the request settles.
- Use the shared Markdown renderer in queued rows. Show up to three
compact screenshots with a count for more images. Strip standalone image
syntax from the text preview.
- Keep paragraph boundaries. Wrap thumbnails and controls within narrow
rows.
- Load queue previews only from local attachment and asset content
paths. Add an opt-in image omission mode to the shared Markdown renderer
for compact queue text.
- Correct the case lookup API summary so the existing search test passes
with the full catalog.
- Add regressions for successful, rejected, and uncertain saves. Check
files added to the next draft.
- Add a production-component Storybook case with controlled save
responses and a desktop/mobile browser test.
- Document the queue display and draft recovery behavior in DESIGN.md.

## Verification

- Red: the original components fail five new unit cases. The desktop and
mobile browser cases also fail on raw Markdown. Screenshots show the
previews still in the composer.
- Green: 256 composer, queue, and shared Markdown unit tests pass.
- Related coverage: 466 thread, draft, and attachment tests pass. One
existing test is skipped.
- Three browser cases pass. Desktop and mobile cases check pending save,
rejection, retry, successful save, and steering. A 320px case checks
separate paragraphs with no margins, three loaded thumbnails, the
additional-image count, and auto-send controls. It checks that all
controls and images fit within the queue.
- UI typecheck and design token gates pass after the review fixes. The
initial implementation also passed the UI production build, Storybook
catalog checks, and 30 map/catalog tests. The final PR workflow passed
typecheck, build, all required test suites, and browser E2E checks.
- Browser command: `pnpm exec playwright test
queued-message-screenshots.spec.ts
--config=tests/storybook-visual/playwright.config.ts --project=stories`.
Use `STORYBOOK_TEST_BASE_URL` to target an owned Storybook server.
- Review regressions first failed with two unit failures and an
inline-paragraph browser failure. They pass after the fixes.
- Initial CI failed the case lookup search test. It reproduced locally
and now passes all three cases after the API summary correction.
- Initial CI also failed process identity inspection during a server
shutdown test. All 22 focused shutdown tests pass locally without source
changes. The cause remains unconfirmed; the final CI Runner support
suite also passed without changes to process cleanup.
- The browser cases mount production components with simulated
upload/save and steer responses. They do not prove backend persistence
or live provider delivery.
- `pnpm verify` returns exit 2 because DESIGN.md is a global CI input.
The required PR workflow supplies the broad CI coverage.
- Native preflight cannot finish because Cargo is absent in this
environment. UI dependencies were installed. The missing plugin SDK
build was prepared before the successful UI checks.

- Final head: `2219de45721360d36341ea1ae46641b6bcb20360`. [CI
run](https://github.com/paperclipai/paperclip/actions/runs/42708839687)
passed. No checks are pending or failing. Greptile and Paperclip Review
each gave 5/5. All five prior review threads are resolved.

## Risks

- Narrow rows can grow to show two text lines and thumbnails. The
existing queue height limit still applies.
- Screenshot previews use the existing image-reference parser and a
local attachment path filter. Image omission is opt-in; thread and
decision media keep their existing modes.
- Pending uploads remain in draft state while their previews are hidden.
Rejection and uncertain-response tests check that the same receipts
return without losing later attachments.

## Model Used

- OpenAI Codex, based on GPT-6. The runtime does not expose a more
specific serving model ID or context limit. Used reasoning, tools, code
execution, and browser checks.

## Checklist

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

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Online workspace migration must retain prepared sources and recovery
holds.
> - Companion discovery reads saved workspace references from run
profiles.
> - The selector can scan large saved profiles even when it returns few
rows.
> - This pull request adds one company and workspace expression index.
> - Companion discovery uses the same expression as existing migration
lookups.
> - The result preserves each source check and reduces history scan
work.

## Linked Issues or Issue Description

**What happened?**

The companion selector used a JSON path expression with no matching
index. A read-only plan on about 22,000 run rows showed a sequential
scan over a table with about 1.3 GB of table and TOAST storage. A
repeated inspection reached its statement timeout. Existing migration
lookups use a different JSON operator form, so one expression index must
match both callers.

**Expected behavior**

Use a company-scoped indexed lookup. Keep prepared and finalized
references, the exact text comparison, native issue references, and run
order. Keep all source and recovery validation.

**Steps to reproduce**

1. Retain many run profiles with saved workspace references and wide
JSON fields.
2. Inspect a workspace with few matching companion runs.
3. Explain the company and saved-workspace selector. Without the new
index, it can scan run history and read unrelated JSON values.

**Paperclip version or commit**

Baseline: `0e3fdebea8257bdff11573081c76b306f12db230`.

**Deployment mode**

Server with PostgreSQL. The local proof uses disposable PostgreSQL 18.1.
The observed server plan used PostgreSQL 17.11.

Related: [batched companion discovery in
#19735](https://github.com/paperclipai/paperclip/pull/19735). The
existing batching remains in place. The duplicate search found no open
workspace index change. This branch also carries the isolated API
route-summary correction shared with
[#20370](https://github.com/paperclipai/paperclip/pull/20370).

## What Changed

- Add one concurrent index on company ID and the saved native workspace
ID expression. The index has no state, runtime, or status predicate.
- Use nested JSON operators in companion discovery. Existing migration
lookups already use this expression. Keep both accepted states and the
same ordering.
- Register the migration on the existing top-level migration path.
Retain its advisory lock, interrupted index repair, and completion
journal behavior.
- Add a real PostgreSQL migration and ordered-result test. Update the
existing companion query counter for the equivalent SQL form.
- Generate the schema snapshot and retain the five newest snapshots.
- Describe the task case-list route as “Find the cases for a task.” This
keeps the route on the first discovery search page. Search logic and
test assertions are unchanged.

## Verification

- `pnpm --filter @paperclipai/db exec vitest run
src/heartbeat-workspace-companion-index.test.ts --maxWorkers=1`: **1
passed**. It builds the concurrent index through the canonical migrator,
checks index validity, preserves prepared/finalized and
cross-issue/null-issue results, excludes foreign and malformed
references, and checks one journal entry after a second migration call.
- `pnpm --filter @paperclipai/server exec vitest run
src/services/remote-workspace-online-source.test.ts -t 'discovers all
Git companions' --maxWorkers=1`: **1 passed; 86 not selected**. This
uses the real source owner and retains finalized recovery holds, company
isolation, and a fresh obligation between inspections.
- `pnpm --filter @paperclipai/db typecheck`, migration numbering/safety
checks, and `git diff --check`: **passed**.
- One small local SQL comparison used the captured baseline selector,
the changed selector, 3,012 synthetic rows, about 37.8 MB of table
storage, and the new expression index. Ordered results were identical.
One client timing changed from **8.70 ms to 0.86 ms**. This is one local
warm-cache comparison, not a server latency claim. Hosted improvement is
**unverified**.
- Initial setup attempts failed before the affected test ran: registry
access, local tsx IPC, and a missing SDK build. The canonical Node
24.22.2/pnpm 9.15.4 setup and SDK prerequisite resolved these setup
failures. The local SQL timing client used Node 26.4.0; the focused
application tests used Node 24.22.2.
- [The first CI
run](https://github.com/paperclipai/paperclip/actions/runs/42705129724/job/129460702073)
failed the inherited case-history API first-page search assertion. The
same failure was reproduced on the unchanged base: the route ranked
sixth for “case for task.” The isolated route-summary correction from
#20370 passed the same three assertions. The search algorithm and
assertions were not changed.
- No public command changes require a documentation update. The schema
and migration record the added index. No hosted DDL or deployment was
performed for this PR.

- Final head `c50101dabf46ade42b545d2051d724d93f413ccd`: [full
CI](https://github.com/paperclipai/paperclip/actions/runs/42707623920)
passed with 60 successful checks and three configured skips. [Fresh
Greptile
review](https://github.com/paperclipai/paperclip/pull/20372#issuecomment-6752305627)
is 5/5 on this head with no open threads.

## Risks

- The index adds storage and write cost when a run profile changes.
- A concurrent build reads the table and can wait for old transactions.
The existing migration path runs it outside a transaction and can repair
an interrupted invalid index.
- Existing nested-operator lookups can use the index. The companion
selector needs this code change to use it. Other JSON path queries are
unchanged.
- The small local result does not establish end-to-end import speed or
PostgreSQL 17 hosted latency. Fresh deployed measurement remains
necessary.

## Model Used

OpenAI Codex, GPT-6 family. The exact runtime model ID and context
window are not exposed in this session. Used reasoning, code editing,
shell tools, and local 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 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-09 16:17:13 -06:00
DottaandPaperclip 6583ffa3b4 fix(workspaces): migrate task directories with legacy shared mode (#20371)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A control-plane replacement must restore saved work without its old
disk.
> - Online workspace migration saves a guarded physical source to object
storage.
> - Some older task directories have shared workspace mode in their
saved metadata.
> - Their physical paths still identify one company and one source
issue.
> - The selected-source check rejects that mode even though the alias
check accepts it.
> - This pull request admits the old mode under the same physical-source
and writer guards.

## Linked Issues or Issue Description

Refs #18835 and #19873 for workspace preservation and source-fence
ownership. Related #11914 addresses mode selection during workspace
reuse. This branch also carries the one-line API discovery correction
shared with #20370. The search found no duplicate fix for this migration
admission path.

**What happened?**

Online migration rejects an ordinary local task directory when its row
has legacy `shared_workspace` mode. The directory can have the exact
managed company and source-issue path, with no repository or branch. The
selected-source check requires `isolated_workspace`, while its existing
alias check permits both modes.

**Expected behavior**

Admit either saved mode only when the same physical path, company,
source issue, aliases, lifecycle state and writer checks prove a safe
source. Preserve the saved mode. Do not require global maintenance.

**Steps to reproduce**

1. Create a `local_fs`, `project_primary` workspace with
`shared_workspace` mode, a source issue, and a null provider reference.
2. Put its files at the managed
`isolated-workspaces/<company>/<sourceIssue>` path.
3. Inventory online migration. The previous code reports that the source
is unproven.
4. With this fix, migrate through the existing owner and restore the
saved archive.

**Paperclip version or commit**

Base: `0e3fdebea8257bdff11573081c76b306f12db230` from freshly fetched
master.

**Deployment mode**

Online local-filesystem workspace migration with durable snapshots.

## What Changed

- Permit `shared_workspace` beside `isolated_workspace` in the existing
physical task-source admission check. All other checks remain in their
existing owners.
- Extend the existing migration and archive-restore fixture with the
legacy shared-mode task path and null provider reference.
- Use the truthful task-case lookup summary “Find the cases for a task”.
This restores its existing first-page discovery contract without
changing search scores, pagination or tests.

## Verification

- Canonical worktree preflight passed in 26.2 seconds with cached Node
24 and pnpm 9.15.4. The existing SDK dependency preparation completed
before the server tests. An initial direct test attempt could not
resolve the unbuilt SDK and executed no test cases.
- **RED:** Against the previous admission rule, the original
project-path fixture passed and the new task-path variant failed with
`eligible: false`: 1 passed, 1 failed, 86 skipped, 13.58 seconds.
- **GREEN:** All **88 tests** in
`server/src/services/remote-workspace-online-source.test.ts` passed in
**35.47 seconds** after the one-line fix. This includes migration,
archive restore, foreign and mismatched source aliases, source-issue
fencing, active execution, lifecycle, and publication-race controls.
- Command: `node node_modules/vitest/vitest.mjs run
server/src/services/remote-workspace-online-source.test.ts
--maxWorkers=1 --fsModuleCache` under Node 24.
- `git diff --check` passed. Tests used disposable PostgreSQL and a
local object-storage provider. Hosted migration acceptance remains
separate.
- Required CI [attempt
1](https://github.com/paperclipai/paperclip/actions/runs/42704874763/attempts/1)
failed on previous head `c69ea90bc0f2f18707336004598e2429f590790d`.
[Server shard
12](https://github.com/paperclipai/paperclip/actions/runs/42704874763/job/129459250886)
failed `case-history-api-discovery.test.ts:14`: the first five results
for `case for task` did not include `GET /api/issues/{issueId}/cases`.
This test source is unchanged in this PR. The dependent final verify
gate failed. The attempt remains a nonpass; no retry has been requested.
- The shared API discovery diagnosis reproduced the existing lookup
route at rank six. The summary omitted a query term. The correction from
#20370 changes only that truthful summary. On final head
`0d645370545130a8a835da8c1a77030562b29049`, the unchanged three-case API
discovery regression passed in **10.64 seconds**. The 88-case workspace
proof above remains valid for its unchanged source and fixture inputs;
it was not rerun.
- Final head `0d645370545130a8a835da8c1a77030562b29049`: [fresh required
CI](https://github.com/paperclipai/paperclip/actions/runs/42707521663)
**passed** at 22:13:53 UTC on November 9. All 51 workflow jobs are
accounted for: 49 successful and 2 skipped. The [final verify
aggregate](https://github.com/paperclipai/paperclip/actions/runs/42707521663/job/129476327386)
passed. The exact-head rollup and classic Snyk status are successful,
with no failed or pending checks.
- Fresh Greptile review is **5/5** for this final head, with no open
inline threads. Ponytail's **5/5** review covers previous head
`c69ea90bc0f2f18707336004598e2429f590790d`; it is historical evidence
for the unchanged workspace patch, not a final-head review. The old
failed CI attempt remains recorded above.

## Risks

- This admits a previously refused saved metadata shape. The physical
directory must still match the company and original source issue. The
alias check still rejects a different issue on that task path.
- Source, lifecycle, task and active-run fences are unchanged. No live
metadata is rewritten to gain admission. Saved workspace modes and
snapshot formats carry forward; no schema or command changes are needed.

## Model Used

OpenAI Codex, GPT-6.1 Sol (`gpt-6.1-sol`), with reasoning, repository
tools and code execution. Context-window size is not exposed by the
runtime.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
canary/v2026.1109.0-canary.17
2026-11-09 16:15:47 -06:00
DottaandPaperclip 02bfe71a6b fix(ui): keep new-task composer stable during submission (#20365)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - People use the New task dialog to send work to an agent.
> - The dialog uses the shared task composer.
> - A send request added an empty header with a bottom margin.
> - That margin increased the composer height by 12 pixels.
> - This pull request shows the header only when it has content.
> - The composer keeps its size and position while the request runs.

## Linked Issues or Issue Description

**What happened?**
The New task composer grew by 12 pixels when a person sent a task with a
description and no separate title. On desktop, the centered dialog moved
up by 6 pixels and the editor moved down by 6 pixels. On mobile, the
editor moved down by 12 pixels.

**Expected behavior**
The composer, editor, and dialog must keep their size and position while
the task request runs.

**Steps to reproduce**
1. Open New task.
2. Enter a task description without a separate title.
3. Send the task while the create request is still pending.
4. Compare the composer and editor bounds before and after the send.

**Paperclip version or commit**
Reproduced on `0e3fdebea8` in the production NewIssueDialog component
through its existing Storybook story.

**Deployment mode**
Local source checkout. The browser check uses fixture requests and
starts no agent runs.

No duplicate fix was found in the open PR and issue search. Related open
PR #19725 changes keyboard and pinch-zoom sizing; it does not change the
empty header during a task send.

## What Changed

- Keep the header absent during a normal send when it has no title,
subtask context, or notice.
- Keep the existing title, subtask context, connection retry notice, and
error message.
- Extend the submit test to check stable composer sections for
description-only tasks, titled tasks, and subtasks.

## Verification

- `pnpm exec vitest run ui/src/components/NewIssueDialog.test.tsx
ui/src/components/task-chat/TaskChatComposer.test.tsx`: 253 tests
passed.
- The new description-only regression failed on the original code
because an empty header appeared. It passed after the fix. The title and
subtask cases passed in both versions.
- Chromium browser check at viewport widths 1280 and 390: the original
composer grew by 12 pixels. After the fix, all measured bounds for the
composer, editor, and dialog stayed equal before and during the send.
- `pnpm check:token-gates`: passed.
- `git diff --check`: passed.
- No documentation change is needed. The task creation contract stays
the same.
- The worktree preflight installed dependencies but could not build
Runner because Cargo is absent. UI verification completed. [CI
passed](https://github.com/paperclipai/paperclip/actions/runs/42702272345)
for `53aa01c169559da1b31fd224c60ebeabd4ccf9c5`. It covers typecheck,
build, general tests, and all eight end-to-end test shards.

## Risks

- Low risk. The change only controls an empty header during a normal
send. Existing tests cover connection retry and error notices.
- No API, database, dependency, or design token changes.

## Model Used

- OpenAI GPT-6 (Codex), with reasoning, tool use, and code execution.
The session does not expose a more specific backend model ID or context
window size.

## Checklist

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

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-09 15:49:31 -06:00
Nicky LeachandClaude Opus 5.5 0e3fdebea8 perf: remove repeated reads and checks from run preparation (#20358)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Before each run starts, Core prepares the run: it opens the run log,
resolves skills, and prepares the AI runtime
> - Every run pays this preparation time before the agent can begin, for
a new task or a follow-up, on a cold or a warm computer
> - A trace on a hosted test deployment showed some preparation steps
that repeat a read or a check that a later step does again
> - This pull request removes three of these repeated steps and keeps
the check that each one duplicated
> - The benefit is that each run starts sooner, with no new mechanism
and no change to what a run can see or write

## Linked Issues or Issue Description

**What existing behavior does this improve?**
Run preparation (`task.prepare`). On a hosted test deployment it takes
about 2.8 to 3.6 s. Skills preparation has a P50 of 275 ms (1,161 ms on
a deployment with more skills and projects).
`heartbeat.prepare.run_output` takes 386 to 724 ms.

**Subsystem affected**
Run logs in object storage (`object-byte-journal`,
`object-run-log-store`) and runtime skill delivery (`company-skills`,
`skill-execution-content`).

**Current behavior**
- An object journal claim is preceded by a head read. Its only use is to
give the current owner to `claim()`. `claim()` then reads the head
again, and it rejects the claim only when the two reads differ. The run
log, provider trace capture, and run evidence migration all do this.
- Run log `begin` reads the source-prefix proof object for every claim.
For a new run log, no proof can exist, so the read always returns
"missing".
- Each delivered skill version calls `withVersion`, which authorizes the
skill again for the run's actor. The skill row was read with the same
actor's visibility condition in the same call. With an agent actor this
repeats the company source and project permission reads once per
version.

**Proposed behavior**
- `claim()` reads the head once. It still refuses a deleted journal. The
conditional head write still rejects any change after that read, and a
stale writer is still fenced.
- `begin` does not read a proof for a first claim (generation 1). A
resumed log still reads and validates its proof, so a changed or
malformed proof still fails closed.
- Runtime skill delivery authorizes each skill once, when it reads the
selected rows. Other callers of `withVersion` keep their own check.

**Reason and benefit**
Each removed step protects nothing that a later step does not protect
again:
- Head read: the claim builds on its own head read, and the conditional
write enforces it.
- Proof read: a proof is written only by a writer that holds this log's
head. The journal never removes a head (deletion leaves a tombstone that
`claim()` refuses). So a first claim cannot find a proof.
- Skill check: every caller of `materializeVersionSnapshot` passes a row
that it read through the same actor's visibility condition
(`readFullSkillRows`, `getById`, `listFull`).

Expected saving per run: two object-storage GETs on `run_output` (about
100 ms each on the trace), and the per-version visibility reads in
skills preparation (4 versions per run on the test deployment, about 388
ms per version on a heavier deployment).

**Breaking changes**
None. `claim()` no longer takes `expectedOwner`. It is an internal API
with three callers in this repository, all updated.

Refs #20284 (it made run-log tail reads parallel and cached; this change
covers the begin path).

## What Changed

- `object-byte-journal`: remove the `expectedOwner` input and comparison
from `claim()`. Remove the extra head read in its three callers (run log
`begin`, provider trace capture, run evidence migration).
- `object-run-log-store`: `begin` reads the source-prefix proof only
when the claimed head has `generation > 1`.
- `skill-execution-content`: split `withVersion` into the access check
and `withReadableVersion`. `withVersion` keeps the check for its other
callers.
- `company-skills`: `materializeVersionSnapshot` uses
`withReadableVersion`, because each of its callers already read the
skill row under the actor's visibility condition.
- Tests:
- A new run log `begin` reads `head.json` once and reads no proof. A
resumed log reads `head.json` once and reads its proof once.
- Runtime skill delivery for an agent actor does no per-version source
permission reads.

## Verification

- `cd server && npx vitest run src/services/object-byte-journal.test.ts
src/services/object-run-log-store.test.ts
src/services/object-run-log-store-tail-read.test.ts
src/services/run-log-store.test.ts
src/services/provider-trace-object-store.test.ts`
- `cd server && npx vitest run
src/__tests__/company-skills-runtime-selection.test.ts
src/__tests__/company-skills-runtime-concurrency.test.ts
src/__tests__/heartbeat-runtime-skills.test.ts
src/__tests__/company-skills-service.test.ts
src/__tests__/managed-skill-content.test.ts` (embedded Postgres)
- `cd server && pnpm run typecheck`
- Each new test fails on the base commit: the run log test sees 2 head
reads and 1 proof read; the skill test sees 1 source permission read per
delivered version.
- Existing tests still pass for: stale-writer fencing after a takeover,
a deleted journal that refuses a claim, a malformed proof that fails
closed, and a denied project-scoped skill that is not delivered.
- I did not run the full `pnpm test:run`. CI covers it.

## Risks

- Low. Two concurrent claims of the same journal: before, the second
claim failed when the first published between its two head reads. Now
the later claim takes over, and the earlier writer's next append fails
with a conflict. This is the same result as when the later claim starts
after the first publish. Writer authority (`assertAuthority`) is
unchanged.
- A skill whose visibility is revoked in the moment between its row read
and its materialization in the same call is still delivered for that
run. This was already true for a revocation just after the old
per-version 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 (`claude-opus-5-5`) in Claude Code, with tool use
(shell, file edits, test runs).

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] 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-09 13:06:12 -08:00
DottaandClaude Opus 5.5 380b3d49c0 fix(api): make case lookup and audit history discoverable to agents (#20354)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Cases are an experimental saved record with facts, audit history and
handoffs, beside the documents on their tasks
> - Agents find REST operations with `search_api`, which ranks catalog
entries by words in the method, path and summary
> - The case read routes had generated summaries such as "GET cases
events" and one generic description
> - So a chat agent that searched "list company cases" or "case facts
history" did not get any case read route on the first page
> - In a chat eval, the agent wanted to check the case for "who changed
the diagnosis", could not find it, and answered from task document
revisions only
> - This pull request gives the case read routes real summaries and
descriptions, and tells chat agents where a task's case and its
fact-change history are
> - The benefit is that agents can find and cite a case's audit history
when a person asks about it

## Linked Issues or Issue Description

Refs #20265 (case page link guidance for chat agents).

**What happened**
A person asked a chat agent "Who changed the diagnosis, and what did it
say before?" about a case. The agent called `search_api` with "cases
list facts history" and "list company cases". The case list,
task-to-case lookup and case events routes were not on the first result
page. The agent said "Not found", skipped the case audit and answered
from the task document revisions only. It did not mention the case fact
change or link the case.

**Expected behavior**
An agent that searches for a company's cases, a task's case or a case's
fact-change history finds the matching route on the first page. The
catalog says that a `fields_changed` event records who changed the facts
and when, but not the previous values.

**Steps to reproduce**
1. Enable Cases.
2. Call `searchRunnerApi({ query: "list company cases" })`.
3. See that `GET /api/companies/{companyId}/cases` is not in the five
results. The same is true for `GET /api/cases/{id}/events` with "cases
list facts history".

## What Changed

- `server/src/routes/experimental-api-paths.ts`: add
`experimentalApiDocs` with summaries and descriptions for the case list,
task-to-case lookup, case read, case events and case document revision
routes.
- `server/src/routes/openapi.ts`: use those summaries and descriptions
when present. Other experimental routes keep the generated summary. The
experimental notice stays in every description.
- `server/src/services/agent-conversations.ts`: the Cases chat directive
now names `GET /api/issues/{issueId}/cases` to find a task's case and
`GET /api/cases/{id}/events` for who changed its facts.
- `server/src/__tests__/case-history-api-discovery.test.ts`: regression
test with the queries from the failed run, and a check on the directive.

## Verification

- `cd server && pnpm exec vitest run
src/__tests__/case-history-api-discovery.test.ts` fails on master (2
failures) and passes with this change.
- `cd server && pnpm exec vitest run
src/services/heartbeat/task-markdown.test.ts
src/services/native-runtime/runner-api.test.ts
src/__tests__/openapi-routes.test.ts
src/__tests__/skill-import-discovery.test.ts
src/__tests__/primary-agent-guidance.test.ts`: 2148 tests pass.
- `npx tsc --noEmit -p server` reports no errors in the changed files.
- A live chat eval (`cases.case-history`) runs on a branch with this
commit.

## Risks

- Low risk. The change edits catalog text and chat guidance only. Route
behavior and authorization do not change.
- Search ranking changes for queries that use these words. Case routes
now rank higher for case queries.

## 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
- [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-09 15:06:02 -06:00
DottaandClaude Opus 5.5 1baac27888 feat(server): chat bootstrap reads for instant task opens (#20355)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - People open tasks from the task list to see the latest messages and
to reply to an agent
> - Before the task chat shows its first frame, it waits for about ten
separate reads: the issue, recent comments, comment input facts, recent
activity, recent runs, interactions, attachments, live runs and the
active run
> - On a busy instance each read waits for the database, and the slowest
read decides when the chat appears. A cold open takes 1.5–4 s on staging
> - This pull request adds one read that returns all of that data for up
to ten tasks, in the same JSON shapes as the existing endpoints, plus a
single-task form
> - The benefit is that the task list can preload its top tasks, and a
cold open needs one round trip instead of about ten. The UI change is in
the stacked pull request

## Linked Issues or Issue Description

**Problem or motivation**
A task open waits for about ten reads. With the database under load,
these reads queue, and a cold open takes seconds. The list cannot
preload a task cheaply, because each task costs about ten requests.

**Proposed solution**
`GET /api/companies/:companyId/issues/chat-bootstrap?ids=` returns the
first-frame data for up to 10 tasks. `GET
/api/issues/:ref/chat-bootstrap` returns it for one task. Each field
equals the matching single endpoint's JSON exactly.

**Alternatives considered**
- Prefetch the existing endpoints for each list row. For 30 rows this
sends about 210 requests to a database that is already the bottleneck.
- A new projection with different shapes. This would duplicate client
merge logic, and freshness rules could drift.

**Roadmap alignment**
This is part of the task-loading performance plan in #20276. It follows
#20277, #20284 and #20286.

## What Changed

- `packages/shared`:
  - `ISSUE_CHAT_BOOTSTRAP_MAX_IDS` (10).
- `issueChatBootstrapQuerySchema`: UUIDs or identifiers, normalized,
deduplicated and strict.
  - The `IssueChatBootstrap` and `IssueChatBootstrapItem` types.
- New `server/src/services/issue-chat-bootstrap.ts`:
- Resolves the refs inside the company and applies the `issue:read`
decision to each task.
- Reads the tasks with a concurrency of 2, and returns them in request
order.
- `server/src/routes/issues.ts`:
- The core issue read and the recent-comments read are now shared
helpers, so the bootstrap and the single endpoints use the same code.
The core view keeps its Server-Timing header.
  - Adds the batch route and the single-task route.
- Excludes the single-task route from the issue ETag middleware, so a
fetch without validators always gets a 200 with `Cache-Control: private,
no-store`.
- New `server/src/services/issue-run-presence.ts`:
- The live-runs and active-run payload builders, moved from `agents.ts`
without change.
  - The existing routes call them, and their behavior is unchanged.
- `server/src/routes/openapi.ts`: entries for both routes.

## Verification

- `cd server && npx vitest run
src/__tests__/issue-chat-bootstrap-routes.test.ts` passes 17 tests. They
run on embedded Postgres with privacy enforcement on. They cover:
- Each field deep-equals its single endpoint's JSON, for an ordinary
task and for a conversation task.
- The single-task route equals the batch item, by UUID and by
identifier. It is never answered with a 304.
  - Items keep the request order, with UUIDs and identifiers mixed.
- Private, other-company, missing and unknown tasks are omitted, and
none of their data appears. The single-task route returns 404 for them.
- Malformed, extra or repeated parameters return 400. More than 10 ids
also return 400.
  - A task-bridge key gets 403.
- A 10-task request writes nothing: no inserts, updates or deletes, and
no activity-log or comment changes.
- The issue, agent, activity, live-run, cold-open, summaries, ETag and
OpenAPI route suites pass: 56 files, 1,771 tests.
- `cd server && pnpm typecheck` passes.
- Query cost, local embedded Postgres, one 10-task request: about 49 SQL
statements per task, 55–130 ms in total. A test fails if it goes above
60 per task.

## Risks

- **Database load:** the stacked UI preloads up to 30 tasks, about 1,500
statements at idle time. This is the same work as opening those tasks,
but it is done before the person clicks. The work runs in sequential
requests of 10, with 2 tasks at a time. Most of the cost is existing run
decoration. Live runs and the active run decorate the same running run,
so that work runs twice. Sharing it is a follow-up.
- **Stricter access:** live runs and the active run now apply the
`issue:read` policy inside this endpoint. The existing routes keep their
company-only check.
- Hidden tasks are returned, as `GET /issues/:id` returns them.
- No migration and no persisted-state change.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used in Claude Code
with tool use (shell, browser automation, file editing, subagents).

## 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-09 15:05:41 -06:00
DottaandPaperclip 282503b5b3 fix(ci): prepare native binaries for Runner chaos checks
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Release verification checks that Runner preserves work through
faults and shutdown.
> - A governed-wait recovery case now runs a real native Runner and a
controlled Codex process.
> - That case needs the workspace debug binaries before the chaos suite
starts.
> - The chaos workflow builds JavaScript contracts but does not build
those native binaries.
> - A clean release worker fails before it can verify recovery.
> - This pull request builds the native prerequisites with the existing
package command before the tests.

## Linked Issues or Issue Description

Refs #20336, which added the real-Runner recovery case. Refs #20322,
whose normal delivery exposed the missing prerequisite. Existing native
test preparation and related chaos PRs were checked; no duplicate
preparation repair was found.

**What happened?**

The [Cloud readiness chaos
job](https://github.com/paperclipai/paperclip/actions/runs/42686564449/job/129397518634)
failed on Core `c67909cb0f1fa973e0991ba068d9c8b2f8f64924`. The
governed-wait recovery test tried to open
`packages/paperclip-runner/runner/target/debug/paperclip-runnerd`, which
did not exist. The job had installed dependencies and built eval
contracts. It had not built native workspace binaries.

**Expected behavior**

The chaos job builds its native prerequisites before it starts the
Runner fault tests. A build failure must fail the job. The existing
recovery assertions must still execute.

**Steps to reproduce**

1. Use a source checkout with JavaScript dependencies and contracts
prepared but no Rust debug workspace binaries.
2. Run `src/native-session-runtime.test.ts` with the test name `settles
a governed wait through real Runner shutdown after a missing Codex
terminal`.
3. Observe a missing native artifact. On the hosted job it was the
daemon. In the local reproduction, canonical JavaScript preflight had
already built a release daemon, so the missing artifact was debug
`fake-codex-app-server`.
4. Build the native workspace binaries and run the unchanged case again.

**Paperclip version or commit**

Hosted failure: `c67909cb0f1fa973e0991ba068d9c8b2f8f64924`. This branch
is based on fresh master `8a6b08227484591a30a7adea3840f7e96755ed81`.

**Deployment mode**

GitHub Actions source readiness and reusable Runner chaos verification.

## What Changed

- Run the existing `pnpm --filter @paperclipai/paperclip-runner
build:rust` command before the Runner fault suites. This builds the
locked native workspace binaries, including the daemon and controlled
Codex server.
- Extend the existing release-workflow contract tests to require
unconditional native preparation after dependency installation and
before the real-Runner suite.

## Verification

- The new workflow contract failed against the previous job setup. It
passed after adding the prerequisite.
- The unchanged governed-wait case failed with a missing debug
fake-Codex binary before native preparation: 1 failed, 251 intentionally
skipped, 2.92 seconds.
- Complete canonical worktree preflight prepared the debug binaries in
10.8 seconds. The unchanged real-Runner case then passed: 1 passed, 251
intentionally skipped, 2.03 seconds. It verifies saved work, accepted
handoff, confirmed provider exit, durable state and a single admitted
turn.
- After rebasing onto refreshed master, the exact case passed again in
**1.84 seconds**. The three affected workflow contracts also passed.
- **412 existing workflow, routing and native-cache contract tests
passed** in 26.58 seconds. Their input files did not change in the
rebase.
- `actionlint -oneline -shellcheck= -pyflakes=` and `git diff --check`
passed.
- The test uses the real local Rust Runner and a controlled local
provider. It does not call a live model or modify a hosted instance.
- The full local chaos suite was not run. Hosted execution of the
changed reusable chaos job remains a postmerge check.
- Required CI for final head `dd751732bff287dbeb05ddadd25c3370c817dd7a`:
[workflow attempt
2](https://github.com/paperclipai/paperclip/actions/runs/42689605454/attempts/2)
**passed** at 20:58:20 UTC on November 9: all 53 jobs accounted for, 52
successful and 1 skipped. The fresh [Build
job](https://github.com/paperclipai/paperclip/actions/runs/42689605454/job/129419966076),
[server shard
4](https://github.com/paperclipai/paperclip/actions/runs/42689605454/job/129419970716),
and [final verify
aggregate](https://github.com/paperclipai/paperclip/actions/runs/42689605454/job/129431023346)
all passed. The exact-head check rollup and classic Snyk status are
successful, with no failures or checks still pending.
- Attempt 1 remains a nonpass: the clean public npm install requested an
AWS SDK package version before that version was published, and server
shard 4 exceeded an existing five-second completion poll before its test
retry collided with a retained fixed-name environment fixture. The
unchanged exact server case passed locally. One bounded failed-job retry
reran both failed leaves; neither the fixture nor assertions were
changed. The initial cause of the five-second completion miss has not
been established.
- Fresh exact-head Greptile review is **5/5** with no open inline
threads. Ponytail is **4/5, Ship**, with two explicitly nonblocking
suggestions: a separate native-build step deadline and tighter
workflow-job scoping in the contract test. The native build remains
bounded by the existing job deadline; the current contract proves
prerequisite ordering in the affected workflow. Both suggestions are
retained for follow-up rather than changing this final tested revision.

## Risks

- A cold native build adds compilation time and disk use to the chaos
worker. The existing job timeout remains 40 minutes on the protected AWS
route and 45 minutes on the hosted route.
- Native build failures stop the job. The test selection, assertions,
permissions and source-proof gates remain in force.
- This change addresses the observed missing artifacts. Earlier
runner-loss waves and the separate managed-credential assertion remain
nonpasses; this change does not establish their causes or repair them.

## Model Used

OpenAI Codex, GPT-6.1 Sol (`gpt-6.1-sol`), with reasoning, repository
tools and code execution. Context-window size is not exposed by the
runtime.

## Checklist

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

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agent Chat lets a person ask an agent about their own work, such as
"what in my inbox needs my attention?"
> - Agents find API routes through the agent API catalog, which is built
from the OpenAPI registry
> - The catalog described `GET /api/agents/me/inbox/mine` as the current
agent's assigned items and listed no `userId` parameter. The skill
reference for the route is keyed with `?userId={}`, so it never attached
to the catalog entry
> - A chat agent called the route without `userId`, got 400, and then
answered from an unscoped company task search. It named company work
that is not in the person's inbox and did not link the person's Mine
list
> - This pull request documents the route as the person's Mine list with
a required `userId`, and adds one chat guidance paragraph for inbox
questions
> - The benefit is that inbox answers use the person's own task scope
and link their Mine list

## Linked Issues or Issue Description

**What happened**

A person asked a CEO chat agent: "What in my inbox needs my attention
today?" The agent called `GET /api/agents/me/inbox/mine` without
`userId` and got 400. Then it listed every company task, including an
unrelated agent-created maintenance task. It said the person's inbox was
board-only and did not link the Mine list.

**Expected behavior**

The agent reads the person's Mine list with `GET
/api/agents/me/inbox/mine?userId=<person>`. It answers from those tasks
only, links each task it cites, and links the Mine list at
`/<prefix>/inbox/mine`. If the read fails, it says so and links Mine.

**Steps to reproduce**

1. As a board user, participate in some tasks. Let an agent create an
unrelated task in the same company.
2. In Agent Chat with the CEO, ask "What in my inbox needs my attention
today?"
3. The agent calls the Mine route without `userId` (400) and lists all
company tasks.

## What Changed

- `server/src/routes/openapi.ts`: `GET /api/agents/me/inbox/mine` now
has a summary and description that say it is a person's Mine list, not
the agent's own queue. It documents the required `userId` query
parameter and the optional `status` parameter, and the 400 response.
- `server/src/services/agent-conversations.ts`: one chat guidance
paragraph. For inbox questions, read the Mine route with the
`authorUserId` of the person's message, answer from those tasks only,
and link `[Mine](/<prefix>/inbox/mine)` (it opens the Mine view in every
layout). If the read fails, say so and link Mine.
- Regression tests in `openapi-routes.test.ts` and
`agent-conversations.test.ts`.

## Verification

- `npx vitest run src/__tests__/openapi-routes.test.ts
src/__tests__/agent-conversations.test.ts
src/__tests__/heartbeat-list.test.ts` (in `server/`): 107 passed. The
two new tests fail without the source change.
- `npx vitest run src/services/native-runtime/runner-api.test.ts`: 2075
passed.
- Live chat eval `inbox-search.show-my-work` on the eval candidate with
this commit: see the comment below for the run result.

## Risks

- Low risk. The change is catalog text, one query schema in the OpenAPI
document, and one chat guidance paragraph. The route behavior does not
change.
- The chat prompt grows by about 500 characters.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 14:54:24 -06:00
Devin FoleyandClaude Opus 5.5 90ec0b9673 fix(sentry): report a reviewed chat setup wait only after its retry decision (#20205)
A reviewed chat attestation that exhausts its short lock or delivery wait fails setup with reviewed_chat_execution_binding_not_ready, and the bounded retry then owns recovery. The failed-status write sent the Sentry report before the retry was scheduled, so a wait that the retry recovered still created an unexpected-failure event.

The setup path now marks that write as pending and reports once after the retry decision. A scheduled retry keeps the failure local; an exhausted, unscheduled or failed retry decision still reports. Other setup failures keep their report on the failed-status write.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 12:32:15 -08:00
Dotta 98f49af70a feat: filter agents by model, status, harness, and connection (#20317)
Add shared filters to both All Agents layouts and views. Keep chart ancestors and bulk selections when filters hide agents. Use the same metadata for filter choices and table cells.

Add tests and update the filter journey and design guidance.
2026-11-09 14:21:38 -06:00
DottaandClaude Opus 5.5 61c169753e test(chaos): fire planned faults at a fixed time after launch with --fault-after-seconds (#20345)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness fires each task's fault at a seeded time
inside the workload's window (R1: 20–90 s after launch)
> - With a Paperclip-sized remote workspace on Daytona, preparation
(sandbox clone and the initial snapshot save) takes longer than 90 s
> - Every restart in a Daytona R1 wave therefore lands before the run
resolves its runtime mode, so only the pre-resolution recovery path is
ever tested
> - This pull request adds `--fault-after-seconds`, which fires every
planned fault at a fixed time after launch while keeping the rest of the
seeded plan
> - The benefit is that one seed can test a restart both during
preparation and after native resolution

## Linked Issues or Issue Description

**What happened?**
Daytona R1 waves w-20261109T163514-s9913 and w-20261109T185009-s9950
restarted the controller at 43 s and 84 s. Both runs were still
preparing their remote workspace, so the native recovery path for a
real-size workspace was never exercised.

**Expected behavior**
An operator can choose when the fault fires relative to the task launch.

**Actual behavior**
The fault time could only come from the seeded workload window.

## What Changed

- `--fault-after-seconds <n>` (5–3600) sets `faultAfterMs`. `planTasks`
still draws the seeded fire time, so nonces, holds and triggers are
unchanged, but it uses `faultAfterMs` for every planned fault except
K13.
- The option is rejected with storms and crash-point waves, which have
no planned fault times.
- README: one line under Options.

## Verification

- `npx vitest run --config tests/restart-chaos/vitest.config.ts`: 259
passed. The new scheduler test fails before the change. It checks that
every planned fault fires at 150 s, that the rest of the plan matches
the same seed without the option, and that out-of-range or storm use is
rejected.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`: clean.

## Risks

- A fixed fault time can land after a fast task finishes, in which case
the fault crosses no work for that task. The trigger is recorded as
missed, as for any late fault.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 14:13:07 -06:00
DottaandClaude Opus 5.5 7f55323abb fix(recovery): end a remote workspace epoch whose writer died during preparation (#20346)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A task with a remote workspace prepares that workspace before the
run selects its runtime, and preparation marks the workspace epoch dirty
with the run as its writer
> - If the server is killed during preparation, the run never selected a
runtime and never started provider work, but the dirty epoch remains
> - Failed-workspace preservation, the path that resolves a dead
writer's dirty epoch, applies to native runs only, so nothing owned this
epoch
> - The dirty epoch kept the dead run's sandbox lease as a pending
export and held the workspace against the task's retry, so the task
never recovered
> - This pull request lets the ownership reconciler end such an epoch at
the last committed save
> - The benefit is that a restart during workspace preparation no longer
wedges the task

## Linked Issues or Issue Description

**What happened?**
In a restart-chaos wave on Daytona (a remote workspace that clones a
real repository), the controller was killed while each run's first
workspace save was in flight. That save is now settled by #20326. Each
run ended `process_lost` with a bounded retry, and both runs were still
in `execution_stage: preparing` with `runtime_mode_resolved_at: null`.
The workspace authority stayed `dirty: true`, `snapshotId: null`, with
the dead run as writer. The dead run's environment lease was proven
owner-gone but was refused by its settle as `workspace_export_pending`,
and the reconciler reported `I2_settled` violations. The retries stayed
`queued` until the test harness deleted the environment 21 minutes
later. Then they failed `remote_workspace_execution_required`, because
the environment was gone.

**Expected behavior**
When a writer dies before runtime selection, its unsaved files are only
the restored source and preparation output, because no agent turn ran.
Recovery must end that epoch at the last committed save, so the lease
settles and the retry restores the workspace into a new directory. A
writer that reached runtime selection must keep its existing recovery.

**Steps to reproduce**
1. Run a task whose agent uses Paperclip Runner with a remote sandbox
workspace.
2. Kill the server while the run is still preparing the workspace (for
example during its first save).
3. After the restart, the run ends `process_lost` and its retry is
queued, but the workspace stays dirty and owned by the dead run, the
lease stays active, and the retry never starts.

**Paperclip version or commit**
`master` at 251c6d5c69 (after #20326).

Refs #20326 (settles the save that was in flight when the controller
died).

## What Changed

- New `settlePreparationOnlyRemoteWorkspaceEpoch`
(`recovery/remote-workspace-unsaved-files-settlement.ts`), wired as the
settle of the existing `remote_workspace_unsaved_files` ownership kind.
Once the writer run is proven gone, it ends the epoch with the existing
`abandonUnverifiedRemoteWorkspaceEpoch` only when all of these are true:
  - the run is terminal;
  - its runtime was never resolved (`runtime_mode_resolved_at` is null);
  - it ended in `execution_stage: preparing`;
  - it has no native finalization;
- durable records show no work began: no `adapter.invoke` or
`adapter.progress` events, no `turn.*`, `provider.*` or
`native.process_*` events, and no recorded process id, process group,
native session or runner instance on the run;
  - the workspace authority is still dirty, with this run as writer;
  - no save for that epoch is still `running` or `pending`.
The epoch then advances with no writer and no location, and the
abandoned epoch is recorded in `remoteWorkspaceAbandonedEpochs` with an
activity entry.
- After that, the lease no longer matches a prepared source, so the
existing environment lease settle releases it and the workspace writer
hold is free for the retry.
- The ownership inventory row for this kind names the new settle.

## Verification

- Red before the fix, green after: `ends a dirty remote epoch whose
writer died during preparation, and keeps every other writer's files`
(`ownership-reconciliation-report.test.ts`). On master it settles 0
epochs. With the fix it ends only the preparation-only epoch, and keeps
the dirty epoch of a writer whose runtime was resolved, a writer that
reached execution, a live writer, and a writer with a save still in
flight.
- Red before the fix, green after, end to end: `lets a retry prepare the
workspace again after its first writer was killed during preparation`
(`remote-workspace-save-concurrency.test.ts`). It runs the real reserve
and prepare. The first writer's initial save is cut off, which leaves a
first epoch with no committed save (`snapshotId: null`). The run is
reaped as `process_lost` while still preparing, with its lease left
active. Two reconciliation passes then end the epoch and release the
lease. The retry reserves the workspace and prepares it again from the
project base in a new directory, then commits its own initial save. On
master, the epoch stays dirty.
- The safety cases also keep a preparation-stage writer with an
`adapter.invoke` event, and one with a recorded process id.
- Red before the fix, green after: `releases the lease and frees the
workspace for the retry once a preparation-only epoch ends`. Before the
passes, the lease counts as a prepared source and the workspace writer
hold is `held` for a retry. After two reconciliation passes, the lease
is no longer active, the prepared source is gone, and the writer hold is
`free`. On master the prepared source remains.
- The reconciler, retained-source fallback, remote workspace lifecycle,
save-concurrency, warm-handoff recovery, and retained orphan sandbox
suites pass (139 tests, 1 environment-gated file skipped). After
rebasing onto #20326, the reconciler, save-concurrency, retained-source
fallback, and controller instance lease suites pass (91 tests).
- `PAPERCLIP_TYPECHECK_LOW_MEMORY=1 pnpm typecheck` in `server` passed.
- Evidence: the runs in the wave's kept database all had
`runtime_mode_resolved_at: null` and `execution_stage: preparing`. Their
`runtime_mode: legacy` is only the column default, not a resolved
runtime.

## Risks

- This ends unsaved files without saving them. That is safe only because
no provider turn can have started before runtime selection, and the
guard requires the writer never to have resolved a runtime. A writer
that did resolve one keeps its existing preservation path.
- The abandoned root stays in the sandbox and is not reused. The next
preparation restores the last committed save, or the source, into a new
directory.
- No schema or API change.

## Model Used

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

## 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-09 14:12:51 -06:00
DottaandClaude Opus 5.5 8a6b082274 fix(api): send call_api mutations once when responseText asks only for the first window (#20344)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native agents use the `call_api` fallback tool for API operations
that have no dedicated tool.
> - `responseText` is an optional `call_api` argument. It returns a
bounded text window of a GET response.
> - The server rejects every `responseText` on a mutation, also a
first-window request that cannot repeat the mutation.
> - A native agent attached `responseText` to a company rename PATCH. It
got the same 400 on each retry, and no call succeeded.
> - The same call also had no `body`. A PATCH without a body changes
nothing, but the route returns 200. With the first rejection removed,
the agent could read that 200 as a completed rename.
> - This pull request sends a mutation once when `responseText` asks
only for the first window, and it rejects a PATCH without a body before
dispatch.
> - The benefit is that a valid mutation goes through on the first call,
a later-page request still cannot repeat a mutation, and an empty PATCH
cannot look like a successful update.

## Linked Issues or Issue Description

Refs #18105. That open pull request keeps the 400 for all mutation
`responseText` values and corrects its message. This pull request
changes the same check. It accepts a first-window request and keeps a
corrected 400 for a later page. Whichever pull request merges second
must resolve this line.

**What happened**

A chat agent (OpenCode, `deepseek-v4.1-flash`) tried to rename its
company with `call_api`. We recovered the exact arguments from the
payload digest in the run events: `operationId: "PATCH
/api/companies/{companyId}"`, `pathParams.companyId`, `query: {}` and
`responseText: { limitBytes: 8192 }`. There was no `body`. The OpenCode
MCP bridge validates the arguments with Ajv without `useDefaults`, and
`8192` is not a default in any schema. Thus the model, not the bridge,
sent `responseText`. The server returned "responseText requires a GET
operation". The agent sent the identical call 10 more times and one
identical branding PATCH. No call succeeded. A second chat case in the
same cohort sent `responseText` on `POST /api/issues/{id}/inbox-archive`
three times before it recovered.

**Expected behavior**

`responseText` with no offset or offset 0 changes only how the response
is shown. The mutation must run once and return its response on the
normal inline or saved-artifact path. A later page (`offsetBytes > 0`)
must still fail before dispatch, because it would repeat the mutation. A
PATCH without a body must fail before dispatch with a message that names
`body`, the allowed fields and the received argument keys.

**Steps to reproduce**

1. Call `executeRunnerApi` with `PATCH /api/companies/{companyId}`, a
body, and `responseText: { limitBytes: 8192 }`.
2. Before this change: a 400, and no HTTP request. After: one PATCH with
the body, and the normal response.
3. Call it with no body. Before: the request is sent and the route
returns 200 with no change. After: a 400 that names `body`, and no HTTP
request.

## What Changed

- `validateRunnerApiCall`: for a non-GET operation, drop a first-window
`responseText` (no offset or offset 0) and continue. A later page still
returns 400 with "This request was not sent" and the reason.
- New `assertRunnerApiDispatchable`: reject a new PATCH with a required
request body when `body` and `files` are both absent. The message lists
up to 12 allowed fields and the received argument keys. `call_api` runs
it inside its receipt reservation, after saved and inherited receipts
are read and before it writes a pending receipt, so a rejected call
leaves no receipt. `executeRunnerApi` also runs it for direct callers.
Recovery and receipt replay use only `validateRunnerApiCall`, so a
body-less PATCH that an older server completed still returns its saved
result.
- Tests: first-window `responseText` on a PATCH dispatches once with the
body (three window shapes). A later page is rejected with no dispatch. A
body-less PATCH is rejected with no dispatch, and `body: {}` is still
accepted. The catalog-wide dispatch tests now send `body: {}` for PATCH
operations with a required body.

## Verification

- Red: the 5 new or changed cases failed on `origin/master`
(`runner-api.test.ts`).
- Green: `npx vitest run src/services/native-runtime/runner-api.test.ts
src/services/native-runtime/runner-tool-results.test.ts` passed (2,087
tests). `tests/runner-e2e/api-response-reading.test.ts` passed (14
tests).
- `tsc --noEmit -p server/tsconfig.json`: no new errors. The error count
is the same before and after this change; the remaining errors come from
packages that are not built in this worktree.
- Upgrade safety and receipts: a new integration test seeds a completed
receipt for a body-less PATCH. `recover()` and a same-callId `execute()`
return the saved result. A new body-less PATCH is rejected twice with
the same error, writes no receipt and sends no HTTP request, and the
corrected PATCH with that callId then succeeds. The test failed with the
rule in `validateRunnerApiCall`, and again with the rule after the
reservation; it passes now. `runner-api.integration.test.ts`,
`runner-api.test.ts` and `native-case-tools.integration.test.ts` pass
(2,164 passed, 2 skipped).
- One paid chat eval run (GitHub Actions run 42680925312) with the first
commit cherry-picked onto the eval branch: `companies.rename-company`
passed. The agent sent one correct PATCH with a body, attributed to the
agent. That run did not send `responseText`, so it does not exercise the
changed path; the unit tests cover it.

## Risks

- Behavior change: a mutation with a first-window `responseText` now
runs. Before, it failed. The response comes back on the normal path
(inline up to 24 KiB, else a saved artifact), not as a text window.
- Behavior change: a caller that sent a PATCH with no body to a route
with a required body now gets a 400. Such a call changed nothing before.
POST, PUT and DELETE are not changed, because some of them have useful
empty bodies.
- No route, permission or company check changes.

## 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
- [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-09 14:03:08 -06:00
DottaandClaude Opus 5.5 97b89981d3 test(chaos): wait for the closing save before reading a remote R1 workspace's files (#20348)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness grades Daytona R1 tasks by reading their
files through `GET /issues/:id/file-resources/content`
> - The server serves a remote workspace's files either from its live
lease or, once the closing save finishes, from the saved snapshot
> - The closing save takes about a minute for a Paperclip-sized
workspace, and during it the workspace is dirty with a writer, so reads
return 422 `workspace_unavailable`
> - The harness read once at `done` and once at rest, both inside that
window, so every Daytona R1 task failed its file checks even when the
agent did the work
> - This pull request retries a 422 read for up to 4 minutes, until the
saved snapshot is readable, and returns a 404 at once
> - The benefit is that Daytona R1 grades the agent's actual files

## Linked Issues or Issue Description

Follow-up to #20288.

**What happened?**
Daytona R1 wave w-20261109T191837-s9950 (K1 at +180 s, native runs)
recovered both tasks without a Retry (R1, R3 and R6 pass), but both
failed `T:module_present`. The server log shows every content read
returning 422, from the moment the task was `done` (19:23:04) through
rest (19:23:10). Track 3's R1 waves lost 10 of 16 tasks the same way.

**Expected behavior**
The oracle reads the files once the workspace's closing save makes them
readable.

**Actual behavior**
It gave up on the first 422.

## What Changed

- `remoteWorkspaceFile` retries a 422 every 2 s for up to 4 minutes
(`REMOTE_FILE_WAIT_MS`). Any other failure, including 404 for a file the
agent never wrote, returns `null` at once.
- README: notes the wait.

## Verification

- `npx vitest run --config tests/restart-chaos/vitest.config.ts`: 260
passed. New tests: a server that answers 422 twice and then the files
(red before), and a missing file that returns within 10 s.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`: clean.

## Risks

- A remote workspace whose closing save never completes now costs up to
4 minutes of grading per file read before failing, which is visible in
the wave's duration.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 13:54:15 -06:00
DottaandClaude Opus 5.5 93f7c3b5d3 fix(tasks): keep a budget-held Retry's failure visible and refresh the open task (#20338)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - When a task's automatic recovery stops, the person's Retry rechecks
the task's gates. If a gate refuses, for example a budget hard stop, the
Retry is saved and starts by itself once the gate allows it
> - After a person pressed Retry under a budget hold, the task page went
blank: no failure, no notice and no Retry. A person had no way to see
why nothing started
> - Two gaps caused it. The task notice drew only system-owned states,
and a budget hold presents as a person-owned state with no card of its
own. Also, admission saved the wait reason without publishing anything
that an open page listens to
> - This pull request keeps the failure visible and names the budget
wait, and it refreshes an open task when admission records why a Retry
waits
> - The benefit is that "failure remains visible if the new attempt
cannot start" holds without the person reloading the page
> - These two commits were pushed to #20306 after it was squash-merged,
so they are not on master

## Linked Issues or Issue Description

Refs #20306 (merged without these two commits) and #20270.

**What happened?**
In the chat eval
`chat-features.runner-opencode.local.recovery.retry-exhausted`, a person
pressed Retry on a task whose native recovery was exhausted while the
agent's budget was held. The Retry was accepted (`POST
/api/issues/{id}/retry` returned 202). The task page then stayed blank
for more than 60 seconds on the open page.

**Expected behavior**
The task keeps the failure visible and says that it is waiting for the
budget. Once the budget allows new work, the saved Retry starts once by
itself.

**Steps to reproduce**
1. Let a task's run fail, and put the assignee over a budget hard stop.
2. Open the task and press Retry.
3. The page shows nothing about the failure or the wait.

**Paperclip version or commit**
master at 76254ead22

## What Changed

- `ExecutionBlockerNotice.tsx`: a Retry held by a budget hard stop
presents as `waiting_for_user` with `userAction: "budget"`. The notice
now shows "Couldn't finish this attempt. Your message is saved. It is
waiting for the budget to allow new work. Raise the budget in Costs."
(`data-testid="task-execution-waiting"`). It shows no second Retry,
because the saved Retry starts by itself. Other person-owned decisions,
such as approvals and run-stopped confirmation, keep their own surfaces.
- `deferred-execution-wait.ts`: when the wait reason of a deferred
request changes, its task records an `issue.execution_waiting` activity.
Its live event refreshes an open task page. Callers inside a transaction
(`heartbeat/queue.ts`) pass their post-commit publication list, so the
event is published only after commit.

## Verification

- A probe on the real deferred-receipt fixture showed that a budget-held
Retry presents as `waiting_for_user` / `budget` / `canRetry: false` at 0
and 11 seconds. Before this change, the notice rendered nothing for that
state.
- `ExecutionBlockerNotice.test.tsx`: 18 tests pass. The budget-held
notice is covered, it offers no duplicate Retry, and approvals still
render nothing here.
- `deferred-execution-wait.test.ts` (new, embedded PostgreSQL):
- One live event is published for each new wait reason. The same reason
again publishes nothing.
  - Inside a caller's transaction, nothing is published before commit.
  - A request that is no longer deferred records nothing.
  - Two of these cases fail when the change is reverted.
- `deferred-execution-receipt-work.test.ts`: passes (49 tests together
with the new file).
- `pnpm check:token-gates`: clean.
- Paid live run: `recovery.retry-exhausted` on an eval branch that
includes both commits. The run ID will be added below.

## Risks

- Low. The new notice renders only for `waiting_for_user` with
`userAction: "budget"`, which rendered nothing before.
- The activity row is written only when a deferred request's wait reason
changes, so a gate that refuses repeatedly adds one row, not one row for
each evaluation.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 13:52:49 -06:00
DottaandClaude Opus 5.5 16e9157577 fix(api): publish agent-grant access operations as board_or_agent in the API catalog (#20349)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Agents read the API catalog (`server/src/routes/openapi.ts`) to
decide which operations they may call. The `x-paperclip-authorization`
actor tells them who performs each operation.
> - The invite, join-request and member-list routes admit agents that
hold the matching company grant (`users:invite`, `joins:approve`,
`users:manage_permissions`).
> - The catalog still listed these operations as board-only.
> - In a chat eval, a CEO agent with `users:invite` read `actor:
"board"` on `POST /invites`, treated it as a final boundary, and told
the person it could not invite anyone.
> - This pull request publishes those operations as `board_or_agent` and
names the grant each needs. It also names the Members page on member
edits, which stay board-only.
> - The benefit is that a granted agent uses the permission the board
gave it, and agents without the grant still get a 403.

## Linked Issues or Issue Description

**What happened?**

A board granted the CEO agent `users:invite` with `PATCH
/api/agents/:id/permission-grants`. The person asked in chat: "Invite
alex@example.test to join this company." The agent searched the API
catalog. It found `POST /api/companies/{companyId}/invites` with
`x-paperclip-authorization: { actor: "board" }`. It concluded that
invites are board-only and did not try the call. The route itself
accepts agents with `users:invite`.

**Expected behavior**

The catalog shows `board_or_agent` for operations whose route accepts an
agent with a company grant. The description names the grant. The agent
creates the invite and returns the join link.

**Steps to reproduce**

1. Grant an agent `users:invite`.
2. Read `GET /api/openapi.json` and find `POST
/api/companies/{companyId}/invites`.
3. See `x-paperclip-authorization: { actor: "board" }`, although
`assertCompanyPermission` admits the agent.

**Paperclip version or commit**

Master at the merge base of this branch.

**Deployment mode**

Any. Found in the runner full-stack chat eval `access.invite-teammate`.

## What Changed

- Remove six operations from `BOARD_ONLY_OPERATIONS`: `POST`/`GET
/companies/{companyId}/invites`, `GET /join-requests`, `POST
/join-requests/{requestId}/approve` and `/reject`, and `GET /members`.
Their routes use `assertCompanyPermission`, which admits agents with the
grant.
- Add descriptions that name the grant for each operation. The invite
description also says it returns a shareable `inviteUrl` (no email
field), and that membership starts only after acceptance.
- Member edits (`PATCH /members/{memberId}`, `/role-and-grants`,
`/permissions` and `POST /archive`) stay board-only. The route guard
requires board access, and a test keeps the catalog in line with that
guard.
- Describe those member edits as board-only even for agents holding
`users:manage_permissions`, and name the Members page
(`/<prefix>/company/settings/members`) where a person makes the change.
The member list description says the same. An agent then links the page
instead of searching for an agent route.
- Add a regression test in `openapi-routes.test.ts`.

## Verification

- `cd server && node ../node_modules/vitest/vitest.mjs run --config
vitest.config.ts src/__tests__/openapi-routes.test.ts` — 43 passed. The
new test also checks that the Members page is named. With master's
`openapi.ts`, the new test fails (`expected { actor: 'board' } to deeply
equal { actor: 'board_or_agent' }`).
- `src/services/native-runtime/runner-api.test.ts` — 2075 passed.

## Risks

- Low risk. This changes catalog metadata only. Route guards do not
change. An agent without the grant still gets 403.
- Agents with these grants will now attempt these operations instead of
stopping. That is the intended behavior of the grant.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 13:52:43 -06:00
DottaandPaperclip c67909cb0f fix: keep tool approvals valid through OAuth refresh and delayed review (#20340)
## Thinking Path

> - Paperclip lets people manage agent work and approve app actions.
> - An approval must bind the action that the person reviewed.
> - OAuth access tokens can expire while that person is away.
> - The old target snapshot treated each token refresh as a new
authorization. It also imposed a one-hour review deadline.
> - This pull request separates token renewal from authorization
identity and keeps the human decision durable.
> - Current access, the reviewed arguments, meaningful target changes,
task closure, and execution receipts still control dispatch.
> - People can approve later without repeating a decision after routine
connection maintenance.

## Linked Issues or Issue Description

**What happened?**

A pending app action returned `approved_tool_target_changed` after its
OAuth tokens refreshed. The tool and connection settings were unchanged.
The same card also expired after one hour. A sign-in problem could
discard an accepted decision before provider dispatch.

**Expected behavior**

The original card remains reviewable after routine OAuth refresh and a
server restart. A later approval runs the exact reviewed action once. A
connection repair preserves the decision while Paperclip verifies the
same authorization. Meaningful changes require a new review.

**Steps to reproduce**

1. Connect an OAuth MCP app and configure an action to require approval.
2. Request the action, then refresh its access and refresh tokens before
approving.
3. Approve the original card. Before this change, it returns HTTP 409
even when the authorization is unchanged.
4. Wait more than one hour. Before this change, the card expires without
a task-specific deadline.

**Paperclip version or commit**

Reproduced from master at `76254ead2229`. The final branch is rebased
onto current master.

## What Changed

- Add versioned approval target comparison. New snapshots bind
credential identity epochs, consent, permissions, ownership, connection
configuration, tool contracts, and credential references. Authenticated
OAuth refresh preserves identity. Manual credential replacement does
not.
- Remove the implicit one-hour deadline from ordinary app action
reviews. Keep explicit deadlines and terminal-task expiry. Synchronize
the nullable interaction deadline in shared contracts and UI.
- Keep accepted, undispatched actions when sign-in is required. Show a
saved-approval repair state. Resume on committed connection changes
after target and access revalidation, without an idle retry loop.
- Preserve the existing execution claim, result receipt, and
uncertain-outcome no-replay rules.
- Explain meaningful target changes without inviting the person to retry
the stale card.
- Add a production task-thread Storybook example and document
compatibility and recovery behavior.

## Verification

- Red: production-refresher invalid-grant handling and formal-approval
withdrawal during a sign-in wait also reproduced as failing regressions
before their fixes.
- Red: before implementation, the local OAuth HTTP fixture failed for
both public and confidential clients with `approved_tool_target_changed`
after token refresh.
- Green: the extended fixture passes for public and confidential clients
with personal and shared grants. It covers actual local HTTP token
exchanges, multiple refreshes, a rebuilt gateway, an ended originating
run, approval after 25 hours, duplicate approval, preserved sharing
arguments, sign-in waiting, and event-driven repair. The gateways use
the production OAuth refresher; an invalid refresh preserves the
accepted decision before provider dispatch. Omitted-scope responses
cover equivalent scope-reporting changes. The provider is an isolated
local fixture, not a deployed Notebook service.
- Negative controls refuse changed consent, scopes, tool definition,
destination, and manual credential replacement before provider
execution.
- Existing gateway service, connected-tool acceptance, secret owner,
secret service, unsigned-review queue, and connection-health suites
passed. Explicit-deadline fixtures still prove expiry behavior,
including deadlines on accepted sign-in waits. A withdrawn formal
approval cancels a waiting action before connection recovery.
- Approval card, task-chat, error-copy and target-comparison tests
passed. Server and UI typechecks passed. Token gates and Storybook
catalog checks passed.
- Inspected the new saved-approval repair card in local Storybook using
the production task thread. It shows a reconnect link and no second
approval button or false running state.
- `pnpm verify --explain` returned exit 2 and requested broad CI
coverage. [Full
CI](https://github.com/paperclipai/paperclip/actions/runs/42677855507)
passed on `eb4f1c7caa9ee6e802c314ffc5eadec475a5de0c`, including build,
typecheck/release registry, general and serialized tests, and all eight
browser shards.
- Greptile reviewed the same final head at 5/5. Both findings from the
first review are fixed and their threads resolved; no open follow-ups
remain.

## Risks

- No database migration is needed. The new comparison reuses the vault
identity epoch. Only authenticated OAuth refresh code can preserve that
epoch; HTTP credential-write schemas cannot select that behavior.
- Earlier signed requests keep their original exact-version checks and
saved deadlines. Already-expired cards are not revived. A historical
card whose credentials already changed needs a fresh review.
- A new consent, account, permission set, destination, or manually
replaced credential must not inherit the old authorization. Reconnection
continues automatically only when revalidation proves the same saved
authorization.
- The interaction enrichment now permits a null deadline. Existing
string deadlines remain valid. The web client and shared validator
change together.
- This PR does not deploy to staging or perform a live Notebook
publication.

## Model Used

OpenAI Codex, GPT-6 family, with repository tools, code execution, and
browser inspection. The runtime does not expose the exact model ID,
context-window limit, or reasoning-mode label; those values are not
asserted.

## 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-09 13:46:28 -06:00
DottaandPaperclip 0e325e8d52 fix(runner): settle saved handoffs after verified shutdown (#20336)
## Thinking Path

> - Paperclip manages AI agents and their work.
> - An agent can save a question or approval handoff before its provider
stops.
> - A missing provider terminal notification does not prove that the
saved work failed.
> - Runner already verifies provider exit, event drain, tool settlement
and suspension.
> - This change uses that proof to settle the same saved handoff.
> - Unproven shutdown holds continuation and explains that the work is
saved.
> - Recovery must never repeat the completed work or allow overlapping
execution.

## Linked Issues or Issue Description

Refs #20330. That PR extends the Codex terminal drain window. This
change handles the separate case where no terminal arrives but the owned
shutdown can prove stopped execution.

**What happened?**

A native run saved a human handoff, accepted cancellation, and later
stopped its provider. The missing terminal notification still made the
attempt fail and show “Couldn’t finish this attempt.”

**Expected behavior**

Keep the saved handoff. Accept its yielded result after either an
authoritative provider terminal or verified shutdown of the exact
execution. If shutdown remains uncertain, retain the handoff and explain
the hold without offering a duplicate Retry.

**Steps to reproduce**

Save a governed wait, acknowledge interruption, and omit the provider
terminal. The new regression uses the real Runner binary and a
controlled Codex process. The old runtime rejects the handoff instead of
asking the existing shutdown owner to settle it.

**Deployment mode**

Native Runner with Codex. Local verification used macOS arm64 and Node
26.4.0. Hosted sandbox recovery has not been qualified.

## What Changed

- Add an optional driver shutdown-settlement operation. Runner returns
proof only after its existing stop, drain, tool settlement and
suspension barrier passes.
- Bind the receipt to the run, normalized session and turn. Persist it
with the saved wait and validate it during recovery.
- Complete the same yielded handoff after verified shutdown. Retire the
stopped session from warm reuse. Do not invent a provider terminal or
complete usage.
- Retain a named shutdown-pending condition under the existing bounded
same-run recovery policy. Preserve actual provider failures and
integrity failures.
- Show saved-work shutdown guidance in task and inbox presentation. Keep
genuine approvals authoritative and disable duplicate Retry.
- Document the completion and restart contract. The checkpoint field is
additive; older checkpoints retain the terminal-or-shutdown path.

## Verification

- Red: the new runtime regression failed against the original
implementation because verified shutdown was never requested. Server
presentation and error-classification regressions also failed before the
fix.
- Red: both inbox shutdown states showed generic text before the inbox
change.
- Green: Runner runtime, shutdown and harness-backend suites pass 301
tests. Tests cover missing terminals, failed stop proof, wrong identity,
cancellation, late proof, genuine provider failure, and restart after
receipt persistence. Recovery completes once without another model turn.
- Green: the native executor and task-presentation suites pass 1,371
tests. Execution projection, inbox and blocker presentation pass 125
tests. Two test-authoring mistakes were corrected: an integrity-error
text expectation and an assertion for a nonexistent projection field.
Their initial failures remain in local logs.
- Runner TypeScript and full server layered type checks pass. Token
gates and diff whitespace checks pass.
- Live provider acceptance: real Runner and Codex with gpt-6.1-sol, one
disposable local workspace, one tool call, and one deliberately dropped
terminal notification. The saved file, bound settlement receipt, one
accepted yielded result, one provider turn, confirmed provider exit and
suspended Runner all passed. The tool handler and persistence port were
controlled. This does not claim a full server or hosted journey.
- The full browser question-and-answer cell stopped before execution
because OPENAI_API_KEY was absent. No model work ran in that attempt.
Hosted sandbox recovery and the full browser continuation journey remain
unverified.
- `pnpm verify` returned exit 2 because the execution-spec change
requires broad CI. Required CI will supply that coverage; a full local
repository suite was not repeated.

- Review correction in `8553da244e`: a failed trailing-event write could
hide a provider failure. A new regression reproduced red, then passed
after all ordinary drain errors retained the named pending condition. No
settlement receipt or result can be accepted after that error. The live
missing-terminal check passed again on the corrected runtime.
- The real PostgreSQL control-plane regression and three existing
governed-wait controls pass. The new case persists one accepted yielded
result, a bound shutdown receipt, and the original pending human
question without a provider terminal.
- First CI revision `7c2cb9b1`: test-program typecheck found a missing
`PrpEvent` annotation in the new fixture. The annotation is fixed and
the Runner test-program typecheck passes. Runner support also timed out
in the unchanged `registered roots remain protected even when equal to
genuine public static bytes` case after 30 seconds. One unchanged local
diagnostic passed in 3.69 seconds overall. The hosted timeout cause
remains unknown; no timeout, fixture, or assertion was weakened.
[Original failed support
job](https://github.com/paperclipai/paperclip/actions/runs/42670851821/job/129346900133).

- Rebased onto the merged #20330 and current recovery changes. The
combined branch passes 1,402 executor/presentation tests. Both prompt
and 18-second provider terminals still pass. A warm session with no
terminal passes after the full 30-second drain and verified
stop/suspension. Its initial overly broad assertion rejected a
nonterminal turn event; the corrected assertion rejects every terminal
type. The rebased CI gate was cancelled before any runner or step
started; its cause is unconfirmed and it supplies no validation credit.
Final revision `b49ec5a144` starts fresh CI and review.

- Final-head CI attempt 1 failed before creating jobs. GitHub reports an
internal server error (correlation
`2c576b07-b67f-4179-a69f-74f7d99cd926`), followed by empty
runner-selection outputs for the aggregate jobs. One unchanged-commit
retry was requested; the initial admission failure remains recorded. [CI
run](https://github.com/paperclipai/paperclip/actions/runs/42673159868).

- **Current blocker:** final head
`b49ec5a144e94e41e96e2508a79bd9aec91fe45a` has a fresh 5/5 Greptile
review and zero unresolved threads, but required CI is not complete.
GitHub reports an active [Actions and Checks API
incident](https://www.githubstatus.com/incidents/tp8cghg7tlc4). Attempt
2 ran only part of the full selected graph. Build, canary, Rust, sandbox
providers, admitted browser shards and other completed test jobs passed.
The E2E aggregate correctly failed because `policy` and `e2e_shards` had
`abandoned` results. Server shard 5/12 received a runner shutdown signal
at 18:36:55 UTC and its test step was cancelled; its job still reports
in progress. The shutdown cause is not established. The separate review
workflow had the same pre-execution internal-error class and passed one
bounded retry. No further retry was submitted during the incident.
Resume with the missing/cancelled coverage after GitHub recovers; do not
count the absent typecheck, Runner or support jobs as passes. [Failed
coverage
gate](https://github.com/paperclipai/paperclip/actions/runs/42673159868/job/129354318236),
[interrupted server
shard](https://github.com/paperclipai/paperclip/actions/runs/42673159868/job/129350854174).

## Risks

- A missing terminal can use one additional bounded shutdown interval.
Unknown shutdown still blocks continuation.
- Provider terminal and usage accounting keep their separate meaning.
The receipt proves stopped execution and settled tool outcomes, not
model success.
- Recovery rejects a foreign receipt, a later turn, an active turn with
a settled receipt, and an actual failed provider terminal.
- There is no database migration. Older checkpoint shapes remain
readable. Drivers without this optional proof retain a named pending
state.
- This PR does not deploy the fix or repair an existing hosted task.

## Model Used

OpenAI Codex, GPT-6 family, with reasoning, repository tools and local
code execution. The exact serving model ID and context window are not
exposed in this session. Live test inference used gpt-6.1-sol.

## 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-09 13:46:16 -06:00
DottaandPaperclip a67b936885 fix(server): redact invitation bearer paths from HTTP logs (#20343)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - People join a company through an invitation link. The link carries a
bearer token in its URL path.
> - The HTTP logger removes query secrets and URL user information. It
keeps the path.
> - So invitation bearer tokens reach the HTTP log in the request URL,
the log message, and the referrer header.
> - PR #17848 fixed this, but it did not merge. Since then, #13742
changed the same redactor file.
> - This pull request carries the #17848 commits onto current master.
The original author is kept on both commits.
> - The benefit is that invitation tokens do not stay in HTTP logs. The
chat eval for membership management can then accept a real invitation
safely.

## Linked Issues or Issue Description

Supersedes #17848 (same commits, rebased onto current master after
#13742).

**What happened?**

The HTTP logger writes invitation bearer tokens to logs. A synthetic
POST to `/api/invites/<token>/accept` puts the token in `req.url`, in
the log message, and in the `Referer`/`Referrer` header record.

**Expected behavior**

Logs keep the invitation route, the method, and the HTTP status. Logs do
not keep the bearer token.

**Steps to reproduce**

1. Check out master.
2. Run `server/src/__tests__/logger-invitation-redaction.test.ts` from
this PR.
3. The synthetic token appears in the request URL, the message, and the
referrer. 12 tests fail on master.

**Paperclip version or commit**

Master at the merge base of this branch.

**Deployment mode**

Source server. The tests use a disposable local HTTP listener. They use
no database and no provider.

## What Changed

- Remove the bearer segment from `/api/invites/<token>/...`,
`/invites/<token>/...`, and `/invite/<token>` URLs, including mounted
paths and absolute URLs.
- Apply the same URL policy to `referer` and `referrer` request headers.
- Keep record IDs on the non-token revoke route
`/invites/<inviteId>/revoke` and on company-scoped invite routes.
- Add regression tests through the real HTTP logger for 202, 400 and 500
responses, with both referrer header spellings.
- Original work by @cryppadotta in #17848. This PR only rebases it onto
master.

## Verification

- From `server`: `node ../node_modules/vitest/vitest.mjs run --config
vitest.config.ts src/__tests__/logger-invitation-redaction.test.ts
src/__tests__/redact-sensitive.test.ts
src/__tests__/http-log-policy.test.ts` — 46 passed.
- With the master `logger.ts` and `redact-sensitive.ts`, the same new
tests fail: 12 failed.
- `http-log-aborted-response`, `cloud-principal-log` and
`setup-token-session` tests pass locally. Some other HTTP-log suites
could not load locally because workspace package builds were missing in
this worktree. CI covers them.

## Risks

- Low risk. Referrer query and fragment values are now removed from
request logs, as they already are for request URLs. This removes some
diagnostic detail to protect secrets.
- A route that uses `/invites/<id>/...` for a non-bearer record ID,
other than `revoke`, now shows `[REDACTED]` in logs for that segment.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), tool use and code
execution, used to rebase and verify. The original change was authored
in #17848.

## Checklist

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

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-09 13:45:00 -06:00
Nicky LeachandPaperclip 79bf733012 fix(runner): share the bounded ACPX cleanup proof (#20332)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Runner keeps a lifetime fence until the original ACPX provider
stops.
> - A recovered Runner must prove that fence is free before it clears
saved cleanup state.
> - Suspension and other recovery paths already wait for transient
listener holders. Recovered shutdown used one immediate probe.
> - That difference made parallel Rust tests fail after they dropped
their original listeners.
> - This pull request uses the existing bounded proof for recovered
shutdown and removes the repeated test retry.
> - The benefit is one cleanup rule, with the same fail-closed proof and
no new work on normal task dispatch.

## Linked Issues or Issue Description

Fixes: #20210

Thanks to @genius-0963 for the test-only fix proposed in #20321. That
related alternative adds a test-only shutdown retry. This change applies
the existing bounded cleanup proof in the shared shutdown path and
removes the same retry pattern from native-provider integration tests.
It also tests a single shutdown call while the original quorum is still
held.

## What Changed

- Use `await_provider_lifetime_fence` for a recovered executor with no
live session and unconfirmed provider exit. Delete the unused one-shot
helper.
- Keep the acquired quorum held through the existing durable state
write. A failed proof leaves `providerExitUnconfirmed` set.
- Give the shared timeout message wording that applies to shutdown and
suspension.
- Extend the recovery test with a worker-start signal. Hold the original
listeners until the single shutdown call is waiting, then release them
and assert successful cleanup without a provider session.
- Use the same bounded proof in the retired-quorum fixture test. Remove
the duplicate retry loop from the native-provider test helper.
- Document the bounded recovery cleanup wait.

## Verification

- macOS arm64, Rust `1.97.1`, source base `3a2f71db31`, head
`368fc814f7ccf88ac4bf68ca9eb7f1a6c294307b`, isolated worktree. Only
targeted checks ran locally.
- `cargo fmt --manifest-path packages/paperclip-runner/runner/Cargo.toml
--all -- --check`: passed.
- `cargo test --manifest-path
packages/paperclip-runner/runner/Cargo.toml --locked -p
paperclip-runner-core --lib acpx_provider_backend::tests`: 45 passed on
the rebased head (5.21 seconds). The initial head also passed five more
parallel-test runs (45 tests per run), with no failure.
- `cargo test --manifest-path
packages/paperclip-runner/runner/Cargo.toml --locked -p
paperclip-runner-core --test native_provider_backend acpx_`: 14 passed
on the rebased head (9.43 seconds). These tests use the real Rust
executor and controlled sidecars.
- Negative control: restore only shutdown's original one-shot proof. The
new regression fails at `shutdown must wait while the original lifetime
still holds a quorum`. Restore the fix and the affected module passes.
- Independent parent verification on current commit `368fc814f7`: the
three exact recovery, held-quorum, and retired-quorum tests passed (3
tests, 5.06 seconds). Independent source review confirmed the unchanged
three-file scope and found no actionable issue.
- The active-turn recovery test still holds a live quorum through the
five-second bound. It checks that shutdown refuses cleanup, saved state
remains unconfirmed, and the provider never starts. It then releases the
quorum and confirms cleanup.
- Standard Greptile scored current commit
`368fc814f7ccf88ac4bf68ca9eb7f1a6c294307b` 5/5 with no review threads.
- Initial GitHub general-server shard 7 failed in unchanged
`remote-workspace-nested-verification.test.ts`, at its 15-second wait
for a child-process import checkpoint (expected one row, got zero; both
Vitest attempts). [Failed
job](https://github.com/paperclipai/paperclip/actions/runs/42668667033/job/129339371815).
That fixture runs Node, PostgreSQL, Git, and storage migration; it does
not run the native Runner or the changed Rust cleanup. The cause of the
checkpoint timing miss remains unknown. The focused retry passed on the
unchanged source ([successful
retry](https://github.com/paperclipai/paperclip/actions/runs/42668667033/job/129347569863)).
This proves recovery, not the cause of the original timing miss. GitHub
kept the reusable-workflow aggregate failed after that job-only retry
and rejected both a final-gate retry and a failed-job retry. A
subsequent run-level retry materialized only the gate and aggregate
jobs; its aggregate inputs reported prior jobs as `abandoned`, so it
failed without a new test failure. A new `full-ci` label event requests
a fresh workflow graph on the same commit. The reason for those
rerun-state results is not established.
- The fresh `full-ci` run
[42674033208](https://github.com/paperclipai/paperclip/actions/runs/42674033208)
then lost several runners mid-job. The maintained [runner-loss recovery
42677096525](https://github.com/paperclipai/paperclip/actions/runs/42677096525)
classified all four failed jobs as runner loss: typecheck, E2E shards 2
and 8, and server shard 4. The evidence was a shutdown log or
lost-communication annotation. It retried the failed jobs together;
canceled sibling jobs were also requeued. This is runner-interruption
evidence, separate from the earlier checkpoint assertion.
- The maintained recovery attempt again failed the same
partial-checkpoint assertion in unchanged
`remote-workspace-nested-verification.test.ts`, twice ([job
129361525237](https://github.com/paperclipai/paperclip/actions/runs/42674033208/job/129361525237)).
Neither checkpoint-failure job logged runner shutdown or lost
communication; their resource summaries reported no significant
constraint. The child phase at the timeout remains unknown. No further
blind retry or TypeScript change is included here. Related
startup-handshake work exists in #20339; its controlled-delay evidence
establishes a deadline-boundary defect but does not establish the phase
of these original CI failures.
- Rebased onto current master after upstream #20339 merged. That
separate change fixes a demonstrated child-startup/checkpoint-deadline
boundary. It does not prove which child phase caused the earlier
zero-checkpoint assertions. This PR still changes only its original Rust
and documentation files. Fresh GitHub CI on the new base will establish
recovery, not retrospective cause.
- Full local tests and a local release build were not run. Current-head
[PR CI
42681719411](https://github.com/paperclipai/paperclip/actions/runs/42681719411),
[Runner Windows
42681719803](https://github.com/paperclipai/paperclip/actions/runs/42681719803),
and [Docker Runner
42681719601](https://github.com/paperclipai/paperclip/actions/runs/42681719601)
all passed on `368fc814f7ccf88ac4bf68ca9eb7f1a6c294307b`. The full check
list has 59 successes and 5 intentional skips. GitHub supplied broad
typecheck, build, and optimized Rust coverage. This is new-base
recovery; the cause of the original checkpoint assertion failures
remains unproved.

## Risks

- A genuinely unconfirmed original provider can now hold recovered
shutdown for up to the existing five-second proof deadline. The proof
returns immediately when two candidate ports are free.
- The changed branch requires both no in-memory provider session and
unconfirmed saved cleanup. Live-session shutdown and ordinary dispatch
keep their existing paths. Outer controller close deadlines remain
authoritative.
- State saved by the previous release carries forward unchanged. No
schema, launch identity, profile digest, protocol, or retry-budget
change.
- No live provider session or hosted deployment was tested. The tests
cover cleanup proof, refusal, durable state, and controlled provider
execution.

## Model Used

- OpenAI GPT-6 through Codex, with reasoning, repository inspection,
code editing, and targeted test execution. The exact serving 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
#` OR (b) described the issue in-PR following the relevant issue
template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub references)
- [x] My branch name describes the change and contains no internal
Paperclip ticket id or instance-derived details
- [x] I have run 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-09 11:36:44 -08:00
DottaandClaude Opus 5.5 0f71ab796e fix(issues): name the existing order in dependency-cycle denials (#20342)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Tasks can wait for other tasks through blocking dependencies, and
the server rejects a dependency that closes a cycle
> - The rejection said only "Blocking relations cannot contain cycles"
> - In chat, an agent asked to "make Website wait for Design too" got
this 422 while Design already waited for Website
> - The agent had no sign that the existing order must stay, so it
guessed a swap: it removed Design's dependency on Website and added the
reverse
> - This pull request makes the 422 name the existing order, say that
nothing changed, and say to ask the requester which order they want
> - The benefit is that agents keep the existing prerequisite and ask
the person, instead of silently reversing it

## Linked Issues or Issue Description

**What happened**
A chat agent was asked to make Website also wait for Design. Design
already waited for Website. The server correctly returned 422 "Blocking
relations cannot contain cycles". The agent then removed the existing
Design-waits-for-Website dependency and added Website-waits-for-Design.
The person lost the existing prerequisite without being asked.

**Expected behavior**
The rejection keeps both saved dependency sets as they were. The agent
explains the conflict with the real task names and asks the person which
order they want before it changes any dependency.

**Steps to reproduce**
1. Create tasks Website and Design. Make Design blocked by Website.
2. PATCH Website with `blockedByIssueIds: [Design]`.
3. See that the 422 says only "Blocking relations cannot contain
cycles", with no task names and no next step.

## What Changed

- `server/src/services/issues.ts`: the cycle check now records the path
it finds. The 422 message names the existing chain by task identifier
(for example "COM-2 waits for COM-1, so COM-1 cannot also wait for
COM-2"). It never includes titles, because the chain can pass through
tasks the caller cannot read. Identifiers are the same fields that
locked relation stubs already show. It says no dependencies changed and
the existing ones are kept. It says not to remove or reverse an existing
dependency, and to ask the requester which order they want.
- The 422 `details` now carry `code: "blocking_cycle"`, the updated
issue, the requested blocker, and the existing chain as ids and
identifiers.
- `server/src/__tests__/issues-service.test.ts`: a regression test
checks the message and details for a direct cycle, the chain text for a
two-step cycle through a task with a private title, that no title
appears in the error, and that the existing dependency is unchanged.

## Verification

- `cd server && pnpm exec vitest run
src/__tests__/issues-service.test.ts` passes (152 tests). The new test
fails on the old message.
- Chat eval `task-coordination.reject-dependency-cycle` ran end to end
with the first commit (run 42678089404, which named tasks by identifier
and title). The agent kept Design waiting for Website and asked the
person which order to keep with a question card. The saved dependency
sets did not change. The second commit removes titles from the message;
the rest of the text is the same.

## Risks

- Low risk. The status code (422) and the first words of the message do
not change. Clients that match the full old message exactly would no
longer match. No code in this repository does that.
- The check reads the identifiers of the issues in the chain only when
it rejects a request, so the success path is unchanged.
- The error can show the identifier of a task that the caller cannot
read. Relation responses already show this identifier in locked stubs,
so this shows no new data.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 13:32:18 -06:00
DottaandClaude Opus 5.5 251c6d5c69 fix(recovery): settle a remote workspace save whose process died mid-save (#20326)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs in remote sandboxes save the workspace as snapshot
operations (`workspace_operations`, phase `remote_workspace_snapshot`).
Each save invocation is listed in `saveWork.activeIds` while it runs
> - When a controller process is killed during a save, its invocation
stays listed and the operation stays `running`
> - Nothing recorded which process ran the invocation, so the shared
ownership reconciler could not prove the owner gone and never settled
the save
> - The task's bounded retry then waited behind that save forever
> - This pull request records the owning process for each save
invocation, and lets the reconciler settle a save whose owner is proven
gone
> - The benefit is that a restart during a save no longer wedges the
task

## Linked Issues or Issue Description

**What happened?**
In a restart-chaos wave on Daytona with a real cloned repository, two
tasks each started their first remote snapshot save (`boundary:
initial`, epoch 1). A SIGKILL hit the controller during the save, before
any upload. Each run ended `process_lost` and got a bounded retry. The
retries stayed `queued` forever, the snapshot operations stayed
`running` with `saveWork.activeIds: [<dead invocation>]` and
`quiescedAt: null`, and the sandbox leases stayed active. The ownership
reconciler reported the saves as owner-gone but had no way to settle
them.

**Expected behavior**
After a restart, recovery must settle a remote snapshot save whose
owning process is gone. It is marked failed with provenance, so the
task's retry can continue. A save that is still running must stay
untouched.

**Steps to reproduce**
1. Run a native task with a remote workspace (for example Daytona with a
`git_repo` project source).
2. Kill the controller (SIGKILL) while the first workspace save is in
flight, before its upload.
3. After the restart, the run ends `process_lost` and its retry is
promoted to `queued`, but the save operation stays `running` and the
retry never starts.

**Paperclip version or commit**
`master` at 1b7fe33432.

Refs #20291 (the quiesced case: a save with no active invocation) and
#20233 (the same ownership pattern for local workspace commands).

## What Changed

- `saveRemoteWorkspace` records the controller process stamp of each
invocation in `saveWork.owners` when it registers the invocation. It
removes that entry when the invocation settles. A module-level set marks
invocations that are still running in this process
(`isRemoteWorkspaceSaveInvocationLive`).
- The ownership registry owns a `remote_snapshot_save` with active
invocations by that stamp. A save whose active invocations have no
stamps was written before this change and is owned by its run, as with
finalize attempts. A mix of stamps, or more than one process, is
unprovable.
- `settleAbandonedRemoteSnapshotSave` settles a save with active
invocations once its owner is proven gone:
- Process-owned: the proven stamp must still match every active
invocation, and none may be live in this process.
  - Legacy run-owned: the run must be terminal.
- Either way it clears the active invocations, records them as
`abandonedInvocationIds`, and marks the save `failed` with `lastFailure:
remote_workspace_save_abandoned`. The #20291 re-arm of blocked fallbacks
still applies.
- The controller instance lease counts running saves stamped with this
boot as held work, so a long save keeps its owner provably alive.
- The ownership inventory row for this kind describes the stamp.

## Verification

- Red before the fix, green after: `settles a remote snapshot save whose
process died mid-save, and keeps live, foreign or unprovable ones`
(`ownership-reconciliation-report.test.ts`). On master it settles 0
rows. With the fix it settles exactly two:
  - a save stamped by a dead process (proof `pid_dead`);
- the wave's shape: an unstamped active invocation on a `process_lost`
run.
It keeps five kinds of save running: a save stamped by this live
process, a stamp from another PID space, a legacy save on a live run, a
save with mixed stamps, and (in the older #20291 test) a stamped active
invocation.
- `stamps each save invocation with the process that runs it and clears
the stamp when it settles` (`remote-workspace-save-concurrency.test.ts`)
pauses a real save mid-flight. It checks the owner stamp and the
in-process live mark, then checks that both are cleared after the save
finishes.
- The reconciler, save-concurrency, remote workspace lifecycle, snapshot
retention, retained-source fallback, preparation gates, and controller
instance lease suites pass (153 tests, 1 environment-gated file
skipped).
- `PAPERCLIP_TYPECHECK_LOW_MEMORY=1 pnpm typecheck` in `server` passed.
- Not yet verified: the full Daytona wave replay that shows the queued
retry starts after the save settles. I asked the chaos harness owner to
replay it on this build.

## Risks

- A legacy save (written before this change) with an active invocation
is settled when its run is terminal. The finalize-attempt kind uses the
same rule. During a rolling deploy, an old-release process could still
be running such a save for a terminal run. If that save later succeeds,
its publication still marks the operation `succeeded`.
- If the retry in the wave is also held by the dead writer's dirty
workspace authority, settling the save may not be enough to start it.
The replay will show whether a second change is needed.
- No schema or API change. `saveWork.owners` is new optional metadata.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 13:27:44 -06:00
Dotta 0eacad0d0c fix: refresh preserved harness receipt facts without copying archives (#20322)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Volume-free deployment must preserve historical controller files
before removing the old disk.
> - A WEB preservation receipt binds an archive and source files to the
selected session and database facts.
> - Later metadata changes can make those facts stale while the
preserved files and archive remain unchanged.
> - The existing owner refuses every stale receipt, so preservation
cannot converge.
> - This pull request refreshes only facts freshness after it verifies
the same source bytes, checkpoints and archive.
> - Existing locks and an old-receipt digest comparison prevent a
concurrent replacement from being overwritten.

## Linked Issues or Issue Description

Refs #19814 for WEB evidence preservation without settling remote
recovery. Refs #19832 for bounded larger inventories. Related harness
receipt PRs were searched; no existing change provides this guarded
refresh.

**What happened?**

A valid WEB preservation receipt becomes stale after later run metadata
changes. Its original-volume owner refuses another preservation attempt
because an invalid receipt already exists. This leaves an eligibility
blocker even when the source and saved archive are unchanged.

**Expected behavior**

The original-volume owner can refresh stale facts only after it proves
every other persisted binding unchanged. It retains the prior receipt
and remote recovery state.

**Steps to reproduce**

1. Preserve a settled session's WEB files and controller evidence.
2. Add later run metadata so its database facts digest changes.
3. Preserve the same scope again. The previous implementation refuses at
the existing-receipt check.

**Paperclip version or commit**

The failure was reproduced on master
`12523fb677585fcd6da929f868014514a4e96e87`. This branch includes master
`1b7fe33432016b3b27ccdfbf7b8944da4b40726d`.

**Deployment mode**

Managed Linux server with PostgreSQL and durable object storage.

## What Changed

- Reuse the exact prior archive for existing WEB receipts. Recheck
current source bytes, controller history, checkpoint material and
archive bindings. Read back ciphertext and verify its size and hash.
- Permit only database/local-observation freshness changes. Selected
session, source run, controller, archive, WEB entries, and assets must
still match the prior proof. Local inode and timestamp observations may
change across attempts; preserved bytes, paths and modes must match.
- Recheck current facts and source files under the existing finalizer,
task and run fences. Accept a matching concurrent winner or require the
exact previous receipt digest before replacement.
- Retain the prior database and local facts digests, witness
fingerprint, observation time and receipt digest in the same
company-scoped activity transaction. Require those four fields to
reconstruct the exact prior proof from unchanged current bindings before
replacement. Normal audit redaction stays enabled.
- Preserve current-key checks and archive reads on ordinary idempotent
calls. Keep active-source, changed-evidence and cancellation refusals.
- Document the refresh contract. No schema, saved-run format or public
API change.

## Verification

- Genuine RED: the new real PostgreSQL/encrypted-archive case failed at
the old previous-invalid gate.
- Before the audit review follow-up, the full local owner suite passed
**94/94 integration cases in 50.01 seconds**. It covers same-archive
refresh with no PUT, audit history, current-key failures, unreadable
archives, cancellation, changed files, replaced volume, active sources,
matching concurrent winners and conflicting receipt replacement.
- The cases also cover a source becoming active during an idempotent
readback. Earlier 91- and 93-case runs preceded these final review
guards and are not the final result.
- Server typecheck, module boundaries and diff checks passed on the
final production/test bytes before the clean master merge. Required CI
supplies final-head breadth. Four narrow post-merge
refresh/current-key/idempotent regressions passed in 5.70 seconds on
`82e270f29fff98442c4184b96082f72f18a88721`.
- The fixtures use a real disposable PostgreSQL database and actual
local encrypted archives/storage. Controlled faults affect the stated
storage or concurrency boundary. They do not prove a hosted refresh or
archive plaintext cold restore.
- The existing integration file now exceeds 1,000 lines. Keeping these
cases with its private original-volume, controller, archive and
PostgreSQL fixture avoids duplicating that setup.
- Initial worktree preflight stalled on the global package-manager
wrapper. Preflight completed with the existing pinned toolchain. No
setup failure is counted as a product test pass.
- Review found that activity redaction could change a full prior-receipt
copy. A new regression enabled the real username censor and reproduced
that mismatch. The fix stores only the four changed scalar fields and
their original proof digest; a prepublication canonical reconstruction
guard rejects dropped or unknown fields.
- On the audit follow-up, 16 affected real PostgreSQL cases passed in
11.98 seconds; 80 unchanged cases were intentionally skipped. They
include real redaction with a username-containing WEB path,
reconstruction across two refreshes, unknown prior fields, CAS, active
sources, current-key failures, unavailable archives and cancellation.
Final server typecheck and boundaries passed. This was an intermediate
revision; the final-head results appear below.
- A second review found that comparing old inode/timestamp observations
also broke ordinary valid retries. Three real cases reproduced
valid-receipt touch/inode failures and stale-facts touch failure. The
fix permits metadata freshness changes across attempts while keeping
exact byte/path/mode/checkpoint/archive coverage and the existing locked
within-attempt metadata fence.
- Final local owner suite on the metadata follow-up: **102/102
integration cases passed in 44.04 seconds**, including valid
touch/new-inode retries, stale refresh with the same archive and no PUT,
four-scalar audit reconstruction, changed bytes/mode/entries, and a
touch during archive readback that must refuse. Server typecheck, module
boundaries and diff checks passed. The final-head CI and review results
appear below.
- Final head `c81475c927d0862dcb8d2c2356dc41ab4a27e63f`: [PR workflow
attempt
5](https://github.com/paperclipai/paperclip/actions/runs/42667287803/attempts/5)
completed successfully at 19:24:28 UTC on 2026-11-09. All 51 jobs are
accounted for: 49 passed and 2 were skipped. The actual [final ci /
verify](https://github.com/paperclipai/paperclip/actions/runs/42667287803/job/129382061558)
and [E2E
aggregate](https://github.com/paperclipai/paperclip/actions/runs/42667287803/job/129381486342)
passed. Typecheck, build and the selected test lanes passed. The
exact-head rollup is successful with no pending or failed checks.
- Attempt 5 resolved both trusted workflows to
`3a2f71db31aa99633610dc241f55f34ac65124de`, after #20339 merged. Its
routing gate passed and real jobs started. This verifies successful
startup and completion of this rerun; it does not exercise the
empty-runner fallback because routing succeeded.
- Earlier CI failures on `82e270f29fff98442c4184b96082f72f18a88721` and
attempts 1–4 of this final-head workflow remain nonpasses. Attempt 1 had
interrupted leaf jobs and failed aggregates. Attempts 2 and 3 ended in
workflow failure without a final verify job. Attempt 4 failed startup
with zero jobs and empty-runner errors. Those failures are not counted
as passes or labeled unrelated.
-
[Greptile](https://github.com/paperclipai/paperclip/pull/20322#issuecomment-6746395090)
and
[Ponytail](https://github.com/paperclipai/paperclip/pull/20322#issuecomment-6746327827)
reviewed the unchanged final head at **5/5**. Both historical review
threads are resolved; no inline thread remains open.
- Hosted application to an actual stale scope remains pending normal
release delivery.

## Risks

- Refresh must not bless different preservation evidence. The
unchanged-binding checks, fresh file and checkpoint proof, exact archive
reuse, source capability and locked receipt comparison remain mandatory.
- A fresh capability has a new witness fingerprint and expiry. The audit
retains the old fingerprint, database/local facts digests and
observation time. All unchanged proof bindings remain on the current
receipt. Their canonical reconstruction must match the old digest. The
new capability is validated at entry and publication.
- Readback verifies ciphertext size/hash and current-key checkpoint
material. This change does not claim a new archive GCM/plaintext
restore.
- Archive identity and rows remain unchanged, so retention ownership
does not change. Failed refreshes retain the previous receipt, original
files and unresolved remote recovery obligations.
- Older receipt shapes are not rewritten automatically. Normal
validation and named preservation refusals remain in force.

## Model Used

OpenAI Codex, GPT-6.1 Sol (`gpt-6.1-sol`), with reasoning, repository
tools and code execution. Context-window size is not exposed by the
runtime.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [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
2026-11-09 13:26:27 -06:00
DottaandClaude Opus 5.5 908d2aae7a test(chaos): run waves against S3-compatible storage with a fault proxy, and click Retry in the real board UI (#20288)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Cloud deployments keep run logs, traces, attachments, artifacts and
saved workspace files in S3-compatible object storage
> - The restart chaos harness ran only with local disk storage, so
storage failures and slowdowns were never tested
> - It also clicked Retry through the API, so a missing or broken Retry
button in the board UI went unnoticed (staging PAP-1683: "the
saved-files recovery Retry does nothing")
> - This pull request runs waves against a loopback MinIO behind a fault
proxy, adds store-outage, latency and throttling faults, and clicks
every Retry in the real board UI
> - The benefit is that storage outages and broken Retry paths now
surface in chaos waves. The first ST1 wave found that a run starting
during a store outage fails immediately and exhausts recovery

## Linked Issues or Issue Description

**What happened?**
Staging saves to object storage and has failures there that local chaos
waves cannot reproduce, because the harness used `local_disk`. The
harness also clicked Retry through the API, which skips the board's
actual button.

**Expected behavior**
Chaos waves can run the server in cloud storage mode, inject storage
faults, and press Retry the way a user does.

**Actual behavior**
They could not.

## What Changed

- `--storage s3`:
- starts a loopback MinIO (`RELEASE.2025-04-22T22-12-26Z`, from
`~/.cache/paperclip/tools/minio/minio` or `RESTART_CHAOS_MINIO_BIN`)
with its data under the instance temp root and random root credentials;
  - creates the bucket;
  - puts a fault proxy in front;
- points the server's `PAPERCLIP_STORAGE_*` settings at the proxy, with
path-style addressing and a per-wave prefix. The supervisor declares
these as explicit workspace storage for the isolation check.
- `storage-proxy.ts`: a transparent, non-retrying S3 fault proxy that
keeps the signed Host header. Rules match by method, path and remaining
count. Faults are outage (503 `ServiceUnavailable`), latency with
jitter, a 5xx or `SlowDown` error rate, connection reset, and 403
`ExpiredToken`. It counts requests by method and status, written to
`storage-proxy.json`.
- `--storage-faults ST1,ST2,ST3` (`storage-chaos.ts`): seeded 60–180 s
windows of outage, 1–10 s latency, or 30% 503 / `SlowDown`, applied
while the wave runs. They're logged in the timeline and in
`storage-faults.json`.
- `board-ui.ts`: every task and subtask Retry the grader needs is now
clicked in the board UI. The harness uses headless Chromium through
Playwright, opens `/issues/<identifier>`, waits for the visible Retry
button and captures the `POST /issues/:id/retry` the click sends.
- R3 fails a missing button as `ui_retry_button_missing (user saw: …)`
and a click that sends nothing as `ui_retry_no_request`.
  - Crash-point waves, which serve no UI, keep the API path.
- README: a cloud storage section.

## Verification

- `npx vitest run --config tests/restart-chaos/vitest.config.ts`: 244
passed. New tests:
- the proxy against a fake upstream: passthrough, bounded outage,
`SlowDown`, `ExpiredToken`, latency and reset;
- live MinIO through the proxy: `If-None-Match` returns 412, and an
injected 503 reaches the client;
- Playwright against local pages: a working button, a missing button,
and a dead button;
- the observer clicking through the board, and R3 signatures for UI
failures;
  - window planning, apply and clear;
  - `--storage` and `--storage-faults` parsing.

  The observer and grader tests failed before the change.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`: clean.
- Live waves on master 610b54f551 with this branch:
- Baseline `--storage s3`, A1/C1, K1/K2 (w-20261109T144738-s9901): 5/8.
There were 2,319 S3 requests through the proxy and 18 conditional-write
412 fences. All 3 Retries were clicked in the board UI, returned 202
Accepted, and the new run succeeded. The 3 failures are a Claude session
issue (#20285), not storage.
- ST1 (w-20261109T145628-s9902): 1/8. A run that starts during a store
outage fails within about 1 s as `adapter_failed`, automatic recovery
exhausts within the outage, and UI Retries are accepted but do not
recover. This has been reported for a product fix.

## Risks

- `--storage s3` needs a locally built MinIO. MinIO no longer publishes
community binaries. Without the binary the wave fails at start with the
path to provide, and the live store test is skipped.
- The UI Retry needs the dev UI to load. A slow first Vite compile can
delay the click, up to the 60 s wait.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 13:15:41 -06:00
DottaandPaperclip 3a2f71db31 fix(ci): handle missing runner output and await import fixture readiness (#20339)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - CI checks must report real result failures after their setup phase.
> - Required summaries must run even when the routing gate has no runner
output.
> - Process-recovery tests must start their checkpoint deadline after
the child loads its modules.
> - An empty runner prevents summaries from starting. Slow child startup
can consume the checkpoint deadline.
> - This pull request adds a fallback for the two summaries and a ready
signal for the recovery fixture.
> - Existing routing, checkpoint, key and recovery assertions remain in
place.

## Linked Issues or Issue Description

Refs #20322. Related open PRs #12512 and #12513 propose a broader runner
contract and caller activation. This change does not adopt that contract
or change normal routing. No current repair for an empty aggregate
runner was found.

**What happened?**

The PR workflow can fail before starting any jobs when the routing gate
has no runner output. GitHub reports an empty `runs-on` value for the
`verify` and `e2e` summary jobs. Their `always()` conditions make this
path possible when a dependency cannot run. A real workflow rerun showed
both errors at lines 801 and 1784 of the trusted workflow.

The first run of this PR also failed two process-recovery cases with no
saved child checkpoints. The fixture starts its 15-second checkpoint
poll before a new Node process imports the server modules. A controlled
16-second pre-import delay reproduces the same zero-checkpoint
assertion. That evidence proves a deadline-boundary defect. The original
CI run has no phase events, so it does not prove which stage caused
those two CI failures.

**Expected behavior**

The summaries can start on `ubuntu-latest` when routing has no runner
output. They must still fail when routing or required work did not
succeed. The process fixture waits for a bounded child-ready signal
before its unchanged checkpoint deadline. It must still prove checkpoint
reuse, process termination and refusal under a changed master key.

**Steps to reproduce**

1. Start or rerun PR CI with no runner output from the routing gate.
2. Let the two `always()` summary jobs evaluate their runner
expressions.
3. Observe that the previous expressions produce an empty value and
GitHub refuses the jobs.
4. For the fixture defect, temporarily delay the child before imports
for 16 seconds. The previous fixture fails its 15-second checkpoint poll
before the child is ready. The updated fixture passes with the same
delay. The temporary delay is excluded from this PR.

**Paperclip version or commit**

Reproduced with the workflow from
`f56b097fbd25607b3b84e418085ab42c2cbb758d`. The caller and both reusable
workflow files are byte-identical between that revision and the affected
PR head. This fix is based on current master
`4a349ea44754f0b1fb8e5e04dc6cdcb05d1c3396`.

**Deployment mode**

GitHub Actions PR CI.

## What Changed

- Add `ubuntu-latest` as the fallback only for `verify` and `e2e` when
the routing runner is empty.
- Extend the existing workflow contract test to require a usable
fallback and execute each real summary script with a failed, cancelled
or skipped gate and absent routing outputs.
- Wait up to 30 seconds for the recovery child to finish imports and
construct storage before starting the existing 15-second checkpoint
poll. Keep the 30-second work abort, 60-second test timeout, and every
kill, key, state and proof assertion.
- Preserve normal runner selection, task selection, permissions and all
result assertions. No production code, public command or product
contract changes.

## Verification

- The new local workflow contract failed before the two
runner-expression changes. The existing summary scripts rejected failed
routing while the test identified the missing fallback.
- **148 focused tests passed** across CI planning, routing and E2E
sharding in 16.00 seconds. These execute the actual summary Node scripts
and preserve failure and cancellation checks.
- `actionlint -oneline -shellcheck= -pyflakes=` passed for the trusted
workflow, routing workflow and caller. `git diff --check` passed.
- The canonical worktree preflight completed with the pinned toolchain
and shared build cache.
- This test does not implement a GitHub expression evaluator. The real
GitHub startup error establishes the absent-output failure; the source
contract and actionlint validate the proposed expression. Hosted
execution of this fallback remains to be verified after the trusted
workflow reaches master.
- The unchanged two process cases passed locally before the fixture
change: 2 passed in 31.06 seconds. A controlled 16-second startup delay
then reproduced the zero-checkpoint failure in 21.81 seconds. The ready
handshake passed with the same delay in 37.11 seconds. The temporary
delay and phase logging were removed. These are measured test durations,
not a measurement of the original CI child startup.
- The final full nested-verification fixture passed: **38/38 cases in
132.30 seconds**, with disposable local PostgreSQL. This includes
partial checkpoint reuse, changed-key refusal and all existing recovery
controls.
- Fresh final head `da35ea034bdee603c021c4eba5b0f78ee6ef52d9`: the [full
PR
workflow](https://github.com/paperclipai/paperclip/actions/runs/42678538191),
[final ci /
verify](https://github.com/paperclipai/paperclip/actions/runs/42678538191/job/129374227690)
and [incremental
qualification](https://github.com/paperclipai/paperclip/actions/runs/42678537930)
passed. The [previously failing server
lane](https://github.com/paperclipai/paperclip/actions/runs/42678538191/job/129366881302)
passed. The final rollup has 61 successful checks, 6 expected skips, 2
neutral checks and no pending checks or failures. Snyk passed.
-
[Greptile](https://github.com/paperclipai/paperclip/pull/20339#issuecomment-6748154727)
and
[Ponytail](https://github.com/paperclipai/paperclip/pull/20339#issuecomment-6748242241)
both reviewed that exact final head at 5/5. There are no inline review
threads. Ponytail permits retaining the source check with actionlint,
which is the validation used here.
- The [first workflow
run](https://github.com/paperclipai/paperclip/actions/runs/42674612619)
failed the two cases described above; it is not counted as a pass. No CI
retry was used to obtain the final result. CI uses the merged trusted
workflow; its policy tests validate this proposed source file.

## Risks

- A routing failure now produces a normal failed summary instead of an
invalid runner expression. The unchanged assertions require successful
routing, policy, valid scope and selected suite results.
- Only lightweight summary jobs can fall back to GitHub-hosted capacity.
Heavy jobs and authorization rules retain their existing route.
- The new readiness allowance is bounded. A child that does not become
ready, fails its work deadline, or cannot produce the expected
checkpoint still fails. The original CI failure cause remains unproved
beyond the observed zero-checkpoint assertions.
- The fallback assertion is an exact source contract. A formatting
change can require an assertion update. The separate actionlint check
validates expression syntax; the text check does not emulate GitHub.
- This does not repair a GitHub internal error or runner interruption.
Those failures remain nonpasses.

## Model Used

OpenAI Codex, GPT-6.1 Sol (`gpt-6.1-sol`), with reasoning, repository
tools and code execution. Context-window size is not exposed by the
runtime.

## Checklist

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

> - Paperclip is the open source app people use to manage AI agents for
work
> - Remote (sandbox) workspaces are restored from a durable snapshot
before a later turn, so restore time delays that turn's first token
> - #20280 added run-log timing for the storage steps of a save and a
restore
> - A Daytona wave with a Paperclip-sized workspace (~750 MB) spent 39.9
s preparing the workspace of a restored run. The storage steps
(download, decrypt, verify, publish) took 2.4 s, and
`workspace.snapshot.sandbox_restore` took 35.6 s
> - The archive helper unpacks the same archive locally in 3.6 s, so
most of that time is elsewhere, probably copying 750 MB into the
sandbox. One span cannot say which
> - This pull request splits that span into the upload into the sandbox
and the extraction in it
> - The benefit is that the next measurement shows which part to fix

## Linked Issues or Issue Description

Refs #20280

**What happened?**
A restored run on Daytona spent 35.6 s in
`workspace.snapshot.sandbox_restore` (wave `w-20261109T170632-s9921`, R1
workload, 754,838,327-byte snapshot). The run log records only that
total, so it cannot show whether the copy into the sandbox or the unpack
inside it is slow.

**Steps to reproduce**
1. Run a remote workspace task with a large repository on Daytona and
let a later run restore it.
2. Read the restored run's `run.performance.span` events.
3. Only `workspace.snapshot.sandbox_restore` covers the time after the
storage download.

**Expected behavior**
The run log separates the upload into the sandbox from the extraction
there.

## What Changed

- `remote-workspace-transport.ts` (`restoreRemoteWorkspaceDirectory`):
the archive copy into the sandbox runs inside
`workspace.snapshot.sandbox_upload`, and the in-sandbox restore command
inside `workspace.snapshot.sandbox_extract`. Both are children of the
existing `workspace.snapshot.sandbox_restore`.
- `execution-preparation-timing.ts`: the two new phase names. A traced
`measureRunPhase` scope also carries its preparation ancestor's run
(`recordRunId`). Without it, with an OTLP endpoint set, a step nested
inside another step (here, upload and extract inside `sandbox_restore`)
never reached the run log. Execution attempts still record nothing.
- `doc/run-log-events.md`: the two rows in the durable storage timing
table.
- No behavior change. This is a run-log change: the rows stay in the
instance database.

## Verification

- New test `times the restore's sandbox upload and extraction as
separate run-log steps` (`remote-workspace-transport.test.ts`). It
restores a real archive through the real helper, inside the production
`sandbox_restore` step with an OTLP endpoint set, with a 60 ms pause in
each step, then reads the drained run phases. It fails without the spans
or without the scope fix.
- New test `records a step nested inside another step when an OTLP
endpoint is set` (`execution-preparation-phases.test.ts`). It fails
without the scope fix.
- `npx vitest run` on `execution-preparation-phases`,
`execution-preparation-timing`, `execution-timing-context`,
`database-operation-attribution`, `native-workspace-finalization-work`,
`remote-workspace-transport` and `remote-workspace-snapshot-storage`:
256 passed.

## Risks

- Low risk. Two more measured steps on the restore path; spans under 50
ms are not recorded.

## Model Used

Claude Opus 5.5 (`claude-opus-5-5`, Anthropic), 1M context window, tool
use and code execution, via 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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 12:56:25 -06:00
DottaandClaude Opus 5.5 4d2a77cf5a perf(workspaces): verify saved Git objects without a single-threaded full fsck (#20329)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Remote (sandbox) workspaces are saved to durable storage as
encrypted snapshots, and restored before a later turn
> - Saves and restores are a reported source of slowness on staging, so
#20280 added run-log timing for each step
> - A Daytona storage wave with a Paperclip-sized workspace (R1, ~750
MB, 19.5k entries) showed each save taking 58–68 s. Storage was only 2–3
s of that; capture inside the sandbox was 44–47 s
> - The first save runs while the first run prepares its workspace, so
it delays the first token
> - A local profile of the same capture found one cause: `git fsck
--full` re-hashes the whole history on one thread on every save
> - This pull request keeps the same object checks but verifies packs on
several threads
> - The benefit is about 10 s less per save on a Paperclip-sized
repository, with the same corruption protection

## Linked Issues or Issue Description

Refs #18990, #20280

**What happened?**
Saving a remote workspace with a large Git history is slow. On Daytona
(3 vCPU), a Paperclip clone's save took 58–68 s; capture was 44–47 s of
it. The first save is on the first run's critical path:
`heartbeat.prepare.workspace` took 90–110 s before the first turn.

**Steps to reproduce**
1. Clone Paperclip (`git clone
https://github.com/paperclipai/paperclip.git`, ~750 MB, one 459 MiB
pack).
2. Run the generated archive helper: `node archive.cjs capture <clone>
<out.pcws>` (from `remoteWorkspaceArchiveScript()`).
3. It takes ~30 s. A wrapper on `git` shows 22.5 s in one `git fsck
--full --no-dangling` on the normalized copy.

**Expected behavior**
Capture verifies saved Git objects as #18990 requires (a missing or
corrupt object blocks the save), without spending most of the save
re-hashing unchanged history on one thread.

## What Changed

- `remote-workspace-snapshot-archive.ts`: both `git fsck --full
--no-dangling` calls (the offline source check and the normalized-copy
check) now call a new `verifyGitObjects`. It runs:
- `git fsck --connectivity-only --no-dangling`: every object reachable
from refs, the index and saved Git state is present.
- A direct hash of each loose object (inflate, then SHA-1 or SHA-256 per
the repository's object format) against its file name. A mismatch or
unreadable object fails with the existing `invalid_git_repository`
reason.
- `git index-pack --verify --threads=N` for every pack (N = available
CPUs, at most 8). This re-hashes every packed object, on several
threads.
- `fsck --no-full` was not used: in a shallow clone it reports reflog
entries that point at packed objects as invalid.
- Tests (`remote-workspace-snapshot-archive.test.ts`):
- New: capture refuses a repository missing a staged object, or a
committed object.
  - New: capture refuses a repository with a corrupt byte inside a pack.
- The existing #18990 test `rejects corrupt available objects in
full/shallow/partial Git before publishing a receipt` (valid zlib, wrong
hash) still passes. It failed with connectivity-only fsck alone, which
is why the loose-object hash is there.

## Verification

- `npx vitest run` on `remote-workspace-snapshot-archive`,
`remote-workspace-transport`, `remote-workspace-migration`,
`remote-workspace-online-source` and
`remote-workspace-readonly-capture-budget`: 526 passed.
- Local benchmark: a fresh clone of
`https://github.com/paperclipai/paperclip.git` (753,506,675-byte
archive, 19,506 entries), alternating runs on one host:

  | Capture | Run 1 | Run 2 |
  |---|---|---|
  | master helper | 32.04 s | 30.05 s |
  | this branch | 21.07 s | 20.09 s |

Both produce the same archive: equal byte size, manifest digest
(`e5facf033409…`) and sha256 (`a4eb843bff1f…`).
- Component timings on the same clone: `fsck --full` 20.5 s; `index-pack
--verify` 19.5 s on 1 thread, 10.7 s on 3 threads; `fsck
--connectivity-only` 0.9 s.
- Daytona evidence for the problem (wave `w-20261109T170632-s9921`, R1,
3 vCPU, MinIO): save `captureMs` 44–47 s, `transferMs` 11–20 s, storage
`putMs` 1.3–1.8 s, `encryptMs` 0.4–0.7 s. I did not rerun Daytona on
this branch: its image must match the checkout's runner content. The
local helper run is the same code the sandbox executes.

## Risks

- Same guarantees as before for missing and corrupt objects. `fsck
--full` also applies its object syntax checks to every packed object;
`index-pack --verify` checks hashes and pack integrity. Commits and
trees are still parsed by the connectivity walk, so malformed ones still
fail.
- More CPU threads during capture, for a shorter time. Capture already
runs inside the sandbox, off the control plane.
- A follow-up could skip re-verifying a pack that an earlier save of the
same workspace already verified. Most saves keep the same clone pack, so
that would remove the remaining ~10 s on repeat saves. Not in this PR.

## Model Used

Claude Opus 5.5 (`claude-opus-5-5`, Anthropic), 1M context window, tool
use and code execution, via 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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 12:56:21 -06:00