Commit Graph
2519 Commits
Author SHA1 Message Date
DottaandClaude Opus 5.5 68b7852cd1 fix(ui): keep another company's prefix on task links in markdown (#21026)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Chat replies, comments and documents render Markdown through
`MarkdownBody`. Task links in that Markdown become company-aware links
> - `parseIssueReferenceFromHref` turned every task link into an
unprefixed `/issues/<id>` path. The router then added the viewer's
active company prefix
> - In chat-eval cohort 4 (`access.other-company`), a reply in company
COM linked `/BIR/issues/BIR-1`. The chat rendered it as
`/COM/issues/BIR-1`, which points into the wrong company
> - This pull request keeps the company that a task link names: an
explicit `/BIR/issues/...` prefix, or an identifier whose prefix belongs
to a known company
> - The benefit is that a link to another company's task opens that
company's task, not a same-numbered task or an error in the current
company

## Linked Issues or Issue Description

**What happened?**
A chat reply in company COM contained `[BIR-1](/BIR/issues/BIR-1)`. The
rendered link pointed to `/COM/issues/BIR-1`.

**Expected behavior**
A task link that names a company keeps that company: `/BIR/issues/BIR-1`
stays `/BIR/issues/BIR-1`. A bare `BIR-1` or `/issues/BIR-1` goes to BIR
when BIR is a company that the viewer knows.

**Steps to reproduce**
1. Have two companies, COM and BIR.
2. In a COM chat or comment, render the Markdown
`[BIR-1](/BIR/issues/BIR-1)`.
3. Inspect the link. Its `href` is `/COM/issues/BIR-1`.

**Paperclip version or commit**
master at f1aef0cb58.

## What Changed

- `ui/src/lib/issue-reference.ts`: a new `issueHref` builds the internal
link. It keeps the company prefix of an explicit `/PREFIX/issues/...`
path. When the path has no prefix, it uses the identifier's prefix if
that prefix is in the known company prefixes. Otherwise the link stays
`/issues/<id>` and resolves in the active company, as before.
- `ui/src/components/MarkdownBody.tsx`: passes the known company
prefixes to the explicit-link path too (the auto-link path already had
them).
- Tests: a red/green unit test with the recorded shape in
`issue-reference.test.ts`, and a rendered test in
`MarkdownBody.test.tsx` (COM and BIR known; explicit, bare and
unprefixed BIR links all render `/BIR/issues/BIR-1`). Existing
expectations that asserted the stripped `/issues/<id>` form now assert
the company-prefixed form.

## Verification

- `cd ui && npx vitest run src/components/MarkdownBody.test.tsx
src/lib/issue-reference.test.ts src/lib/company-routes.test.ts`: 105
tests pass.
- Red/green: with `issue-reference.ts` and `MarkdownBody.tsx` from
`origin/master`, the new tests fail (`/issues/BIR-1` instead of
`/BIR/issues/BIR-1`). With the change, they pass.
- `pnpm check:token-gates` is clean. `tsc -p ui/tsconfig.json` reports
only existing errors in
`ui/storybook/support/feature-map/FeatureOverview.tsx`, which this
change does not touch.
- A paid chat-eval run with this change is linked in a comment below.

## Risks

- Same-company links now carry the explicit prefix (`/COM/issues/COM-1`
instead of `/issues/COM-1` resolved to the same place). The route is the
same.
- An explicit link with a bare identifier as its target (`[x](JIRA-2)`)
is now gated by known company prefixes like auto-linked text. Such a
link to a non-Paperclip key stays an ordinary link.
- Low risk otherwise: no API, data or saved state changes.

## 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-11 23:51:54 -06:00
cf1def0ca6 feat(inbox): rebuild compact cards and share task review controls (#20961)
## Thinking Path

> - Paperclip helps people manage AI agents and their work.
> - The inbox collects decisions, requests, and updates for the people
in a company.
> - People need a clear personal queue and a consistent way to answer
each request.
> - Repeated headings, inconsistent card actions, and separate review
flows make that work harder.
> - This pull request connects the inbox UI to the routing foundation in
#20959.
> - Compact cards use shared task controls and open supporting work in a
side panel.
> - People can answer, claim, transfer, archive, and inspect outcomes
from one place.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

The inbox cards, personal views, shared task controls, and decision
history.

**Subsystem affected**

UI. This PR targets master and uses the routing foundation merged in
#20959.

**Current behavior**

The inbox repeats headings and counts. Cards have different layouts and
action placement. The requester can repeat the signed-in person's name.
Review and question controls differ between the inbox and task thread.

**Proposed behavior**

Open Assigned to you by default. Show one count line above the cards.
Use a compact card layout with consistent actions and supporting
details. Show the requester's avatar and name only when they differ from
the signed-in person. Share question controls, including a radio option
for Other that reveals a text field when selected.

**Reason and benefit**

People can scan their queue and complete requests without losing the
surrounding task or creating separate drafts for the same review.

**Breaking changes**

The default inbox view becomes personal. The all-assignee and shared
views remain available. Card close menus distinguish personal archive
and snooze from shared withdrawal.

**Additional context**

Refs #20959, #19363, and #19520. Refs #20268 for related
actionable-request work. This stack adds personal routing and shared
controls. ROADMAP.md includes Multiple Human Users and Agent Reviews and
Approvals.

## What Changed

- Add personal, unassigned, company, all-assignee, and archived inbox
views. Add claim, transfer, personal close actions, receipts, and
history controls.
- Use compact decision cards with a single count line, one-pixel
borders, and equal space above, below, and to the right of each primary
action.
- Open artifacts and synchronized confirmation/approval controls in a
shared side panel. Share their revision drafts and action state with the
task thread. Other requests keep one editable form on the inbox card
while reviewing an artifact.
- Make Other match the question options and reveal its field only when
selected. Apply this to inbox and task question cards.
- Hide self-requester labels, space agent avatars, and use the standard
canceled status icon for withdrawn requests.
- Use connector logos in connector cards. Keep the connector promotion
background and sidebar border continuous.
- Add company and project decision-owner controls and document the
updated inbox behavior.

## Verification

- The complete [GitHub Actions CI
matrix](https://github.com/paperclipai/paperclip/actions/runs/43141559641)
passes on `8d81a75e49b518fff6b6ff4c23dd67979b0b97ee` (Linux/Node 24:
typecheck, build, general and serialized tests, Runner support, browser
shards, and aggregate gates). Greptile is 5/5 on that exact head with
zero open findings, and the security scan passes.
- Local UI typechecking passes. Eight focused suites pass 382 tests: 236
inbox/review/question/connector tests plus all 146 composer tests.
Regressions passed red to green for attachment links, prompt rendering,
single-form artifact review, connector scope copy, accessible headings,
and the composer test harness's missing theme provider. The final commit
changes only that test harness; production code is unchanged from
`59d015833f`.
- Browser acceptance on the production implementation confirms preserved
selected answers with one editable question form, accessible Markdown
prompts, inert image references, accurate connector scope copy, native
PDF downloads, image sidebar previews, and Open file requesting the
attachment endpoint while preserving the inbox URL. Request reads and
file responses were intercepted; no question answers were written. The
preview is healthy on the final head.
- Visual and interaction checks cover Other radio behavior in both
themes and on mobile, the loaded Google Calendar logo, and 20px above,
below, and to the right of the primary action across all 23 visible
cards. The affected desktop/mobile navigation and sidebar specs pass
locally and in the final CI browser matrix.
- Real-editor browser checks confirm Ctrl/Cmd+Enter cannot bypass
another person's confirmation. Cancel preserves the draft and unlocks
silently; Continue submits once. Approval write failures remain visible.
- `pnpm verify` returns exit 2 because DESIGN.md requires broader
coverage; the final full CI matrix supplies every requested gate. Maps
and Storybook catalog structural checks pass. The routing foundation
#20959 merged after passing 249 database tests, server typechecking,
full CI, Greptile 5/5, and security scanning. Its backend and SDK
registry are preserved.
- Demo data is retained for testing; database suites and browser specs
use separate disposable databases.

## Risks

- The inbox uses the routing contracts merged in #20959. Personal
filters and history affect shared task entry points; the affected
controls are included in local regression coverage and full CI.
- Personal defaults and close-menu wording change inbox behavior.
Existing resolver permissions still control shared actions.
- Shared revision state and live invalidation affect two surfaces.
Focused tests cover draft synchronization, action locks, and incoming
updates.
- Visual changes use the existing design tokens and component
primitives.

## Model Used

OpenAI GPT-6 through Codex. Used repository tools, code execution, and
browser checks. This session does not expose a more specific runtime
model ID or its 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 described the issue in-PR following the relevant issue
template
- [x] I have not referenced internal/instance-local Paperclip issues or
links
- [x] My branch name describes the change and contains no internal
Paperclip ticket id
- [x] I have run focused tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Scott Tong <scott@scottsmbpm5max.lan>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-11 21:05:00 -08:00
DottaandClaude Opus 5.5 b033cc124d fix(evals): repair private human steps for webhook, device and key entry; name failed private steps (#21012)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The chat evaluation catalog checks agent replies and then the human
step that follows, such as a person who reveals a key or approves a
device
> - Some human steps run in a separate private browser process, because
they handle credentials. CI keeps the output of that process private on
purpose
> - Four cases failed only in that private step, so CI showed one fixed
message and no cause
> - I ran each private step locally against a dev server, where its
output is visible, and found the cause of three failures
> - This pull request fixes the harness and two product defects, and
adds a closed failure record so that the next CI failure names its step
> - The benefit is that these human steps work, and a private-step
failure is diagnosable without any credential in logs

## Linked Issues or Issue Description

Bug report (following `.github/ISSUE_TEMPLATE/bug_report.yml`).

**What happened?**
- `routines.webhook-trigger`: the private person opens the routine
overview. The trigger cards are only in the Triggers section, so the
step waits for a missing button. An unhandled response wait then stops
the process.
- `connection-setup.secure-api-key`: a key-protected MCP server answers
401 without a challenge header. Paperclip shows "Remote app returned
HTTP 401" and does not ask for a key. The person cannot enter the key in
the normal flow.
- `assistant-connections.device-login`: an expired device code page
shows "This item doesn't exist or was moved." The server sends "This
code is expired, already decided, or invalid." in the OAuth
`error_description` field, and the UI drops it.
- `onboarding.new-company`: the private step reads back all saved state
and then fails one proof check. CI does not say which check.

**Expected behavior**
- The person reveals the webhook key in the Triggers section and sends
authenticated events.
- A pasted MCP link that answers 401 shows the key field.
- An expired device code says that the code is expired.
- A private-step failure names its fixed step and error class, with no
value.

**Steps to reproduce**
1. Start a local dev server.
2. Create a routine with a bearer webhook, and run the private webhook
worker. It times out on the "Edit webhook" button.
3. Paste the address of an MCP server that answers a plain 401 into
Connectors > Add by link. No key field appears.
4. Open `/mcp-device?user_code=<expired code>`. The page shows generic
not-found text.

**Paperclip version or commit**
`master` (reproduced on origin/master before this change)

**Deployment mode**
Local dev server (embedded PGlite) and the CI chat-eval runner

**Installation method**
Source checkout (`pnpm dev`)

## What Changed

- `integrator-webhook-private-worker.ts`: open `<routine>/triggers`
(also for the setup-resume link), and attach a handler to the early
response wait so a failed click reports its real step.
- `server/.../catalog/discovery.ts`: a 401 with no `WWW-Authenticate`
header, to a connection that sends no credential, is handled like a
challenge without discoverable sign-in (`oauth_challenge`). The existing
UI guidance then asks for a key. A 401 to a connection that already
sends a credential keeps the old error.
- `ui/src/api/errors.ts`: read the OAuth `error_description` field as a
readable server message.
- New `private-child-failure.ts`: a private child records only a fixed
step name and an error class from a fixed list. The parent echoes a step
only when it is in the caller's closed list. Error text, stdout and
stderr stay private.
- The webhook, private browser and identity children use it. Each has a
closed step list file.
- `identity-new-company-native-proof.ts`: each proof check has a fixed
reason name. The private child reports `new-company-proof-<reason>` when
a check fails. The accepted conditions do not change.
- Tests: server red/green test for the bare 401, UI red/green test for
`error_description`, unit tests for the failure record (no text leaks,
closed list only, extra fields dropped).

## Verification

- Local dev server (embedded PostgreSQL), private workers run directly
with the real child process:
- Webhook, ready and setup-pending triggers: before, timeout on "Edit
webhook". After, receipt with invalid status 401, valid status 202 and a
linked task.
- Secure key entry with the synthetic notes provider: before, timeout on
the key field. After, an active connection with health `ok` and 2
authorized `tools/list` calls.
- Expired device page: before, `at step device-expired-page
(TimeoutError)`. After, `expiredPageVisible: true`.
- `server`: `vitest run src/__tests__/tool-access-service.test.ts -t
"pasted link answers"` (fails before the discovery change with 502,
passes after).
- `ui`: `vitest run src/api/errors.test.ts` (fails before, passes
after).
- `pnpm test:e2e:runner:typecheck`, `pnpm test:e2e:runner:unit`, server
`tsc --noEmit`.
- `node tests/runner-e2e/source-sdk-public-inputs.mjs <HEAD sha>
--check`: unchanged.
- One paid run of the four cases on this branch. Results are in a PR
comment.

## Risks

- A plain 401 from an MCP link now returns 422 `oauth_challenge` instead
of 502. The draft connection is removed, as for other challenges. Agents
that call the connect API see the "needs sign-in" answer instead of a
raw status. This only applies when the connection sends no credential.
- Some API errors that carry `error_description` now show that text
instead of generic copy. That text is written for people.
- The failure record is opt-in per child and contains only constants.
The parent ignores a record with any other field.
- `onboarding.new-company` is not fixed here. Its native hello probe
cannot start locally on macOS, so the failing check is not yet known.
This change makes the next run name it.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use (shell,
file edits, local browser automation through Playwright). Extended
reasoning was on.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 22:06:29 -06:00
DottaandClaude Opus 5.5 5626a0299c fix(opencode): keep the answer written before the completion call in the saved reply (#20999)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents that use OpenCode models (for example DeepSeek through
OpenRouter) reply in chat through the Runner's OpenCode driver
> - The driver picks one assistant text part as the turn's final
response. Paperclip saves that text as the reply comment and delivers it
to other channels
> - In a chat eval, the model wrote its full answer with links, called
`paperclip_finish`, and then wrote "Answered above — two inbox items
need your attention ..."
> - The driver saved only the later line. The chat showed both texts
from the run stream, but the saved reply, its deliveries and the next
turn's history held only a line that pointed at nothing
> - This pull request keeps all prose written after the last work tool
in the saved reply in that case, and lets the chat show it once
> - The benefit is that the saved reply holds the answer the person saw

## Linked Issues or Issue Description

**What happened?**
An OpenCode chat turn wrote its answer before the completion call, then
a short line after it that refers back to the answer. The saved reply
comment held only "Answered above — ...". The full answer with its links
existed only as progress text in the run stream.

**Expected behavior**
The saved reply holds the answer. Other channels, notifications and the
agent's next turn read the same answer that the person saw in chat.

**Steps to reproduce**
1. Use an OpenCode agent. Ask a question in chat that the agent answers
with links.
2. The model writes its answer, calls `paperclip_finish`, and then
writes a short line that refers to "above".
3. Read the saved reply comment with `GET /api/issues/{id}/comments`. It
holds only the short line.

**Paperclip version or commit**
master at 9dfdeaad86.

## What Changed

- `OpenCodeServerDriver` settles the final response from all eligible
prose parts when it selects a later message after the completion call
and the model also wrote prose after its last work tool and before that
call. It joins the parts in order with a blank line. Other selection
paths do not change.
- The chat transcript adapter removes the latest progress messages when
they compose the saved response exactly, so the text does not show
twice.
- Added a fake-server fixture and a driver test for the "answer, then a
line that refers back" turn. Updated the existing post-tool test: the
saved reply now keeps the earlier prose too, because structure cannot
tell commentary from an answer.

## Verification

- `cd packages/paperclip-runner && npx vitest run
src/drivers/opencode/opencode-server-driver.test.ts` (80 tests pass).
- `cd ui && npx vitest run
src/components/task-chat/transcript-adapter.test.ts` (117 tests pass).
- Red/green: with the driver change removed, the new test and the
updated post-tool test fail. With the change, they pass.
- `npx tsc -p packages/paperclip-runner/tsconfig.json --noEmit` passes.

## Risks

- A turn that wrote commentary after its last work tool, then its answer
after the completion call, now saves both texts. The person already saw
both in chat. We accept this. Losing the answer is a worse failure than
keeping a short note.
- No saved run state, session state or event shape changes. A run that
the previous release saved resumes without change; only the text of a
newly settled final response differs.

## 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-11 20:19:38 -06:00
8818402e7a feat(inbox): route requests to people and record personal outcomes (#20959)
## Thinking Path

> - Paperclip helps people manage AI agents and their work.
> - The inbox collects requests that need a human decision.
> - Teams need to know who owns each request and who may act on it.
> - A shared close operation must differ from hiding a card for one
person.
> - This pull request adds personal routing and durable outcomes to the
existing attention service.
> - People can claim or transfer work and inspect what another person
did for them.
> - Domain permissions still decide who may resolve each request.

## Linked Issues or Issue Description

**Subsystem affected**

Server, shared contracts, and database. This is the routing foundation
for a stacked inbox UI change.

**Problem or motivation**

A company-wide attention feed does not give teams a clear personal
queue. Archive, withdrawal, transfer, and decisions by another person
need distinct scopes and records.

**Proposed solution**

Resolve a request audience from its addressee, task participants, and
configured owners. Provide personal, unassigned, company, all-assignee,
and archived views. Add claim, transfer, personal archive and snooze,
shared withdrawal, receipts, and personal history.

**Alternatives considered**

A single shared inbox leaves ownership and personal hiding ambiguous.
The service keeps one canonical request and projects its audience
instead of copying requests for each person.

**Roadmap alignment**

This extends the existing Multiple Human Users and Agent Reviews and
Approvals capabilities in ROADMAP.md.

**Additional context**

Refs #19363 and #19520 for the current inbox navigation and confirmation
replacement rules. Refs #20268 for related actionable-request work. That
PR filters actionability; this stack adds audience routing and personal
outcomes.

This is part 1 of a two-PR stack. The inbox UI PR #20961 targets this
branch.

## What Changed

- Add nullable company and project decision owners and a company budget
owner.
- Add audience views and personal badge counts to the canonical
attention projections.
- Add claim, transfer, withdrawal, personal archive, snooze, and restore
operations with permission checks and activity records. Lock addressed
transfers to the current addressee or routing administrators, and
validate the recipient against canonical resolution policies.
- Add receipts and scoped history for work handled by another person.
- Keep closed-task requests and replaced confirmations out of current
cards.
- Add an idempotent migration and a concurrent receipt-history index.
- Update typed fixtures so the existing UI remains compatible with the
new contracts.

## Verification

- Server layered typechecking passes, including all three test-tree
partitions.
- Seven routing and interaction-resolution suites pass all 249 tests
against a dedicated external PostgreSQL test database on `4caecc21d8`.
Six new regressions failed before the repair; they cover takeover
followed by approval, legitimate delegation, excluded creators/review
requesters, and unauthorized claims.
- Prior focused verification covers private approval history and
receipts, transfer visibility, audience escalation, withdrawal
governance, hidden-card counts, per-recipient confirmation replacement,
and SDK source contracts.
- A synthetic three-person company passed 37 routing and lifecycle
acceptance checks. The UI stack adds browser acceptance for the shared
controls.
- The complete [GitHub Actions CI
matrix](https://github.com/paperclipai/paperclip/actions/runs/43134997331)
passes on `4caecc21d891677323a7335f23b1a015cce24c13` (Linux/Node 24:
typecheck, build, general and serialized tests, Runner support, browser
shards, and aggregate gates). Greptile is 5/5 on that exact head with no
open findings. The fresh security scan passes after explicitly
rechecking the addressee-transfer repair.
- `pnpm verify` reports exit 2 because a CI-global script changed; full
CI supplies that broader coverage. Embedded PostgreSQL could not start
on this Mac because shared-memory slots were exhausted, so local
database checks use a separate external test database. Linux CI covers
the embedded setup.

## Risks

- Personal badge counts change to the viewer's assigned requests.
Accessible feed totals and task membership keep their existing scope.
- The migration adds nullable owner fields and a dismissal fingerprint.
Its index runs outside a transaction. The migration is classified and
rerunnable.
- Routing changes the addressee used by resolution authorization. The
current addressee or a routing administrator must authorize an addressed
transfer; the recipient must satisfy writer access, canonical resolver
exclusions, and task review policy under row locks.
- The shared validator export barrel crosses 1,000 lines because it
exports the new attention schemas. Schema implementation stays in the
dedicated attention validator module.

## Model Used

OpenAI GPT-6 through Codex. Used repository tools, code execution, and
browser checks. This session does not expose a more specific runtime
model ID or its 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 described the issue in-PR following the relevant issue
template
- [x] I have not referenced internal/instance-local Paperclip issues or
links
- [x] My branch name describes the change and contains no internal
Paperclip ticket id
- [x] I have run focused tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
(OpenAPI contracts and docs/api/attention.md)
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Scott Tong <scott@scottsmbpm5max.lan>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-11 17:49:39 -08:00
TonioandPaperclip 9dfdeaad86 Refine agent workspaces and organization charts (#20973)
Refine agent details, identity editing, responsive controls, and organization charts with three card tiers, clearer save feedback, and stable filtering.

Guard instruction-file renames and root configuration updates in one transaction through the agent lifecycle integration, preserving conflict checks, rollback, and configuration history.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-11-12 01:23:59 +00:00
5e2243f081 fix(ui): align sidebar avatars and navigation icons (#20926)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The left navigation mixes agent avatars, task status icons, and page
icons.
> - Agent avatars used 20px boxes. Task and page icons used 16px boxes.
> - The different widths moved agent labels 4px to the right of other
labels.
> - This pull request gives these icons the same 16px square slot.
> - Shared label positions make pinned and recent items easier to scan.

## Linked Issues or Issue Description

Refs #19685, which added mixed agent and page pins. No duplicate
alignment PR was found.

**What happened?**

Agent avatars in pinned and recent navigation were wider than task and
page icons. Their labels started 4px farther to the right. Inline icon
layout also produced different box heights.

**Expected behavior**

Use the existing 16px task and page icon size for all leading navigation
icons. Center each icon in a square slot. Start the labels at the same
horizontal position.

**Steps to reproduce**

1. Pin an agent conversation and a page such as Routines.
2. Open agent conversations and tasks so both appear in Recents.
3. Compare the icon boxes and label positions across the rows.

**Paperclip version or commit**

Base commit: `c5e6ab5ca813`.

**Deployment mode**

Local source checkout. This is a client layout issue.

## What Changed

- Use 16px agent avatars for pinned and recent conversation rows.
- Give `SidebarNavItem` a fixed 16px square icon slot with centered
contents and left-aligned text. The existing token scale supplies the
size.
- Add light and dark Storybook examples with mixed icons and long
labels. The browser assertions compare slot sizes, label positions, and
glyph bounds.
- Add a production `CoordinatorSidebar` regression test that compares
pinned and recent avatar dimensions with a task status icon.

## Verification

- `pnpm exec vitest run ui/src/components/SidebarNavItem.test.tsx
ui/src/components/CoordinatorSidebar.test.tsx
ui/src/components/SidebarAgents.test.tsx` passes: 59 tests across three
files.
- Regression proof: the new production-avatar test fails with the
original 20px avatars (`expected 20 to be 16`) and passes with 16px
avatars. This addresses the initial review's test coverage gap.
- `pnpm check:token-gates` passes.
- `pnpm --filter @paperclipai/ui typecheck` passes.
- `pnpm --filter @paperclipai/ui build` passes on the final source tree.
- `pnpm verify --base origin/master` returns exit 2 because the new
Storybook file needs broad CI coverage. The focused local tests run
separately. CI must supply the remaining coverage.
- Browser inspection on macOS checked all 13 mixed rows in light and
dark themes. Every icon slot measured 16 by 16 CSS pixels. Every label
started at the same position. Before and after captures use production
components with fixture data; they do not prove server persistence or
permissions.
- Review `sidebar-icon-alignment--dark` and
`sidebar-icon-alignment--light` in Storybook to inspect the mixed rows
and run their layout assertions.
- [Greptile
review](https://github.com/paperclipai/paperclip/pull/20926#issuecomment-6806138954):
5/5 on `e779f4707e7826d577b23a2991f3f36755e954be`. The initial 4/5
review requested production Recents coverage. The added regression test
addresses that finding; the thread is resolved.
- CI queue recovery: the superseded first run was canceled. The next run
stalled before runner assignment; GitHub accepted force-cancel but left
it queued and refused the rerun request. The final empty commit requests
fresh CI without changing the source tree. Required CI is tracked in
[run
43095809539](https://github.com/paperclipai/paperclip/actions/runs/43095809539).
Its jobs are still queued; CI completion is not yet claimed.
- External blocker: after more than 15 minutes, the fresh run still has
zero jobs. Other repository PR runs are also queued. A repository
Actions or runner administrator must restore dispatch before the
required typecheck, test, build, and E2E gates can report results. The
root cause of the dispatch delay is not established.

## Risks

- Low risk. The shared icon slot also affects other navigation rows.
Existing badge, rail, and expanded-state tests cover the shared
component.
- Agent avatars become smaller in pinned and recent navigation, as
requested. No data or API contracts 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. This change is a
scoped UI bug fix.

## Model Used

- OpenAI GPT-6 via Codex. The host does not expose the exact served
model snapshot or context window size. Used reasoning, code editing,
shell execution, and browser 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
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Scott Tong <scott@scottsmbpm5max.lan>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-11 14:01:22 -08:00
DottaandClaude Opus 5.5 34f71f7d57 fix(server): defer a run that meets a busy agent directory instead of failing it (#20829)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Each run of an agent with managed agent files gets a private copy of
the agent directory. Remote copies serialize their prepare, checkpoint,
collect, and release operations through one Postgres advisory lock per
agent, taken with `pg_try_advisory_xact_lock`
> - When the lock is busy, the operation fails at once with "Agent
directory operation is already active"
> - On staging, a deploy restarted the controller while three runs of
the same agent were active. When one run resumed, another run of that
agent was checkpointing its directory. The resume failed, used its last
attempt, and the task became blocked with "Retry or send a message"
> - A run must not fail because another run of the same agent is briefly
using the agent directory
> - This pull request keeps the fail-fast lock and, when it is busy
during preparation, puts the same native run back on the existing
same-run retry schedule a few seconds later
> - The benefit is that lock contention delays a run by seconds instead
of ending it as failed

## Linked Issues or Issue Description

**What happened?**
A task's run failed with `native_continuation_requires_reconciliation`
and the error "Agent directory operation is already active". The task
was blocked and asked the user to retry. The same error ended at least
two other runs of the same agent within one day.

**Steps to reproduce**
1. Use an agent with managed agent files in a remote sandbox.
2. Let one run of the agent checkpoint or collect its agent directory.
3. At the same time, let another run of the same agent prepare (for
example, a same-run resume after a controller restart).
4. The second run fails immediately with "Agent directory operation is
already active".

**Expected behavior**
The second run waits until the first operation is done, then continues.
It does not end as failed.

## What Changed

- `server/src/services/agent-directory-working-copies.ts`: The busy lock
now throws `AgentDirectoryOperationBusyError`, still a 409 with the same
message. Lock behavior is unchanged from `master`: a fail-fast
`pg_try_advisory_xact_lock` held for the life of the holder's
transaction.
- `server/src/services/heartbeat/agent-directory-busy.ts`:
`prepareWithAgentDirectoryDeferral` wraps only instruction preparation
and turns the busy error into `AgentDirectoryPreparationDeferral`
("Another run of this agent is still using its files"). A busy lock met
while collecting files after a turn is not wrapped and keeps its
existing path.
- **Before native admission** (a brand-new run, such as the first run of
a task): `server/src/services/heartbeat/retries.ts`
`finalizeAgentDirectoryBusyDeferral` cancels the run as an expected
control-plane wait (`providerWorkStarted: false`) and schedules an
`agent_directory_busy` retry 5–15 seconds later. The task keeps its
execution lock on the retry. The lane spends no failure retries
(`execution-recovery-attempt.ts`), its count is the retry run's
`scheduledRetryAttempt`, and it stops after 30 attempts.
`pre-dispatch-execution-evidence.ts` classifies this wait like the
existing workspace and AI-connection waits, so retry scheduling does not
ask for reconciliation.
- **A resumed native run**:
`server/src/services/native-runtime/native-controller-recovery.ts`
`deferNativeRunForBusyAgentDirectory` keeps the same run on the existing
same-run retry schedule without spending an attempt. It uses the same
guards as `deferNativeControllerRecovery`: it locks the task, requires
the task's execution run to be this run, requires the current lease
owner, and refuses after a Stop, a startup cancellation or a pending
native cancellation, so those settle normally. Its count is stored in
the run's `contextSnapshot`, which a resume claim does not clear, and it
stops after 30.
- `server/src/services/heartbeat.ts`: routes a preparation deferral to
the same-run path or, before admission, to the scheduled-retry path.
After the bound it follows the normal failure path with the plain
message.
- No schema or migration. Postgres releases a dead holder's advisory
lock with its session. The staging evidence showed a live holder
(another run's checkpoint), not a stale one.
- `doc/DEVELOPING.md`: Documented both paths and the bound.

Earlier revisions of this PR waited on the lock and added idle-session
expiry, keepalive, ownership fences, fenced remote removal and attempt
slots. This revision removes all of them.

### How the directory-wait retry keeps the original wake

The retry no longer replaces the wake. `scheduleBoundedRetryForRun`
takes `preserveWakeContext` for this wait: the retry keeps the original
`wakeReason`, `retryReason` and the whole wake context (interaction,
comment, continuation and admission fields), and its wake request keeps
the original reason. The wait is recorded separately as
`preparationWait: { kind: "agent_directory_busy", attempt, nextAttemptAt
}`, next to the carried `executionRetryAccounting`. Gates that key on
the wake reason therefore treat the retry exactly as they treated the
original wake.

| # | Site | Treatment for the directory wait | Why |
| --- | --- | --- | --- |
| 1 | `heartbeat/retries.ts` `scheduleBoundedRetryForRun` | Changed:
`preserveWakeContext` keeps the original wake context and reason and
adds `preparationWait` | Root cause: the retry overwrote the wake reason
|
| 2 | `run-dispatch/domain/wake-context.ts`
`isResolvedInteractionContinuationWakeContext` /
`PREPARATION_WAIT_RETRY_REASONS` | Special case removed | The original
wake context is kept, so the continuation is classified from it |
| 3 | `heartbeat/retries.ts` `interactionRunnerWait` | Special case
removed | The interaction fields are kept in the context |
| 4 | `run-dispatch/adapters/postgres.ts` scheduled-retry dependency
facts | Changed: a preparation-wait retry whose original wake was
admitted as a dependency-blocked interaction
(`dependencyBlockedInteraction`, recorded at wake time) is admitted the
same way | Without the wait the original wake would have run; this gate
is only met because of the wait |
| 5 | `run-dispatch/domain/wake-context.ts`
`isNonAssigneeWorkspaceBusyRetry` + `heartbeat/retries.ts`
`finalizeAgentDirectoryBusyDeferral` | Kept:
`preparationWaitDeferredWhileAssignee`, derived only from the recorded
admission authority | The scheduled-retry ownership gate has no
wake-reason path; the workspace and AI waits use the same recorded flag
|
| 6 | `heartbeat/retries.ts` explicit user continuation admission |
Kept: admits the `agent_directory_busy` scheduling reason | The
successor needs its own authorization receipt |
| 7 | `execution-recovery-attempt.ts` uncharged lane and
`agentDirectoryWaits` | Kept | Cap and accounting |
| 8 | `pre-dispatch-execution-evidence.ts` (`isPreDispatchResourceWait`,
`isAgentDirectoryPreparationWait`), used by
`legacy-execution-recovery.ts`, `cancelled-native-startup.ts`,
`explicit-native-continuation.ts` | Kept | Describe the cancelled
pre-provider parent run, not the retry's wake |
| 9 | `explicit-native-continuation.ts`
`undeliveredLegacyUserCommentIds`, `run-dispatch/adapters/postgres.ts`
`unstartedUserTurnOrigin` | Kept | The cancelled parent did not consume
the user's comment |
| 10 | `execution-projection.ts` label and saved accounting,
`task-interruption-presentation.ts`, UI toast and chat suppression |
Kept | Presentation of the wait |
| 11 | `run-dispatch/domain/policy.ts` AI-wait lock requirement;
AI-subscription lock recheck; AI wait detection
(`execution-configuration.ts`, `provider-limit-wait.ts`, `heartbeat.ts`,
`ai-connections.ts`, `ai-connection-runtime.ts`, `local-ai-login.ts`) |
Not applicable | AI-subscription specific |
| 12 | `workspace-busy-wait.ts`, `environment-selection.ts`,
`workspace-preparation.ts`, UI workspace-holder banners | Not applicable
| Workspace holder parking; the directory wait is a timed retry |
| 13 | `cancelled-native-startup.ts` paused workspace-retry repair;
`failureRetriesBefore*Wait` | Not applicable | Historical workspace and
AI records only; every directory wait saves its ledger |
| 14 | `failedRunRetryBudgetSpent`; storage-wait exhaustion escalation;
`status-relevance.ts` `isAdmissionWait` | Not applicable | A directory
wait never fails the run, stops at its own bound, and its run has
started |

## Verification

- Original wake kinds after a directory wait
(`heartbeat-agent-directory-busy.test.ts`, real heartbeat): an
assignment runs; a plain user comment on a dependency-blocked task is
answered with `dependencyBlockedInteraction`; an accepted interaction
continuation keeps its parked-task and reassignment gates; a user
message continuation runs with its comment; an accepted non-assignee
mention runs; and a reassignment during preparation does not let the
previous assignee restart. Each retry keeps the original `wakeReason`
and records `preparationWait`. With `preserveWakeContext` off, five of
these fail. Restart recovery keeps its attempt credit
(`native-runner-restart-recovery.integration.test.ts`).

- Non-assignee wake: an accepted mention for an agent other than the
assignee meets a busy directory; its retry records
`preparationWaitDeferredWhileAssignee: false`, survives promotion, runs
and succeeds once the directory is free. Without the shared clause in
`isNonAssigneeWorkspaceBusyRetry`, promotion cancels it.

- Interaction continuations: `heartbeat-agent-directory-busy.test.ts`
runs an accepted interaction continuation into a busy directory. The
retry keeps `mutation`, `interactionId`, `interactionKind` and
`interactionStatus` and is still classified as a continuation. If the
task is moved to todo, or reassigned, while the retry waits, the retry
does not run and the task stays parked or with its new owner. With the
old explicit storage/Runner lists, both cases fail.

- Red/green, new run:
`server/src/__tests__/heartbeat-agent-directory-busy.test.ts` runs a
real heartbeat for a new assignment whose first instruction preparation
meets a busy lock. Without the preparation wrapper, the run ends failed
(red). With it, the first run is cancelled as a wait, an
`agent_directory_busy` retry is scheduled with the task still locked to
it, the retry runs and succeeds, and no run of the task is failed
(green).
- Red/green, same-run: with the deferral as a no-op, the same-run
deferral tests failed (`expected false to be true`); they now pass.
- `native-controller-recovery.integration.test.ts` covers repeated
claim-and-defer cycles of a resumed run: each cycle keeps the attempt
count, cannot be claimed before the delay, stores the count on the run,
and the fourth deferral past a bound of 3 is refused. It also covers
Stop after the busy error, a foreign lease owner and a moved task (each
refused, nothing changed), a run before admission, and that a raw busy
error (as from collection) is never deferred.
- Focused suites: 232/232 passed (`agent-directory-working-copies`,
`instruction-working-copy`, `heartbeat-agent-directory-busy`,
`native-controller-recovery.integration`, `heartbeat-retry-scheduling`).
`tsc --noEmit` for `server` reports no errors in the changed files.
- Staging evidence (read-only): the failed run had a graceful restart at
07:25Z, then same-run resumes. Its third attempt failed at 07:31:08Z.
Another run of the same agent recorded a warm checkpoint receipt at
07:31:37Z. A second run of the same agent failed the same way at
07:59:46Z.

## Risks

- A deferred run waits 5 to 15 seconds per deferral, up to 30 times. If
the holder is still busy after that, the run fails as before with a
plain message.
- Before admission, deferral cancels the run and creates a retry run,
like the existing workspace-busy 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-11 13:56:45 -06:00
Nicky Leach 25f0957c24 Select the AI Connection sign-in computer automatically (#20906)
## Thinking Path

> - Paperclip manages agents and their AI Connections.
> - Codex and Claude subscription sign-in can run on a company agent
computer.
> - Core stores the credential and uses connection permissions to
control access.
> - The Connections page asks the user to choose a computer just to host
sign-in.
> - This change lets Core choose an eligible computer when sign-in
starts.

## Linked Issues or Issue Description

Refs #20410 and #20417.

**What existing behavior does this improve?**

OpenAI and Anthropic subscription sign-in from Connections with an agent
computer as the sign-in environment.

**Current behavior**

The user must choose a sign-in computer before the provider flow can
start.

**Proposed behavior**

Start sign-in from the provider form. Core chooses a computer in the
company and sign-in environment. The session stays bound to that
computer and reports when no eligible computer is available.

**Reason and benefit**

The saved credential and its permissions are independent of the computer
that hosts sign-in, so users can proceed without picking an agent.

## What Changed

- Select an eligible computer when a managed Codex or Claude login
starts without an explicit target. Prefer ready computers and skip
active login leases.
- Remove the picker and let the browser resume the existing session by
environment and connection intent. Continue to honor explicit agent
targets in agent settings. Remove the unused picker fallback from the
legacy form.
- Update the AI Connections guide, the plugin environment contract, and
the OpenAPI descriptions.

## Verification

- Passed server and UI typechecks.
- Passed 317 tests across the login environment, setup-token route,
device-login service, and two UI suites. The Postgres test covers
eligibility, inherited environments, explicit targeting, occupied
computers, and the no-computer response.
- Passed 45 OpenAPI route tests.
- Reran the affected UI suites on the final revision. All 230 tests
passed.
- UI tests cover starting from the provider form and resuming both
provider flows with a server-selected target.
-
[CI](https://github.com/paperclipai/paperclip/actions/runs/43086849302)
and [Greptile
5/5](https://github.com/paperclipai/paperclip/pull/20906#issuecomment-6805135968)
passed on the final commit.
- Local verification used test transports. Live provider sign-in remains
to be checked in a Cloud composition that contains this change.

## Risks

- Two sign-ins that select a computer before either lease is recorded
can contend for the same computer. The existing provider admission guard
handles the conflict. A retry can select another computer once the lease
is visible.
- The computer selection uses existing lifecycle and lease data. Plugin
admission still validates the live computer.
- No schema migration or plugin protocol change is required.

## Model Used

OpenAI Codex (GPT-6), with shell execution and local test tooling.

## 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-11 11:42:31 -08:00
DottaandClaude Opus 5.5 6dc36700b1 fix(costs): mark unpriced usage in the agent, model and project cost reports (#20889)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Cost reports show what agents and projects spent
> - A run can have usage with no known price; Paperclip saves it with
costStatus "unpriced" and costCents 0
> - The summary and the provider, biller and user reports count those
events, but the agent, agent-model and project reports did not
> - An agent that read the by-agent-model report saw a native run at
costCents 0 and told the person it cost $0.00
> - This pull request adds unpricedEventCount to those three reports and
tells agents what it means
> - The benefit is that missing charges are reported as unpriced, not as
free

## Linked Issues or Issue Description

**What happened**
In the chat eval budgets-costs.spend-breakdown, the person asked "How
much did Launch cost this week, broken down by agent?". The week
included a real native run whose usage had no known price (data-quality
reason missing_price). The agent read /costs/by-agent-model and
/costs/by-project. Both returned costCents 0 for that usage and no
unpriced count. The reply showed "1 native run ($0.00)" and gave the
total as complete.

**Expected behavior**
The reports say which rows include unpriced usage. The agent says that
usage is unpriced, marks the total incomplete, and links the record.

**Steps to reproduce**
1. Record one priced and one unpriced metered cost event for an agent in
a project.
2. Call GET /api/companies/{companyId}/costs/by-agent, /by-agent-model
and /by-project.
3. See costCents counts only the priced event, and no field shows the
unpriced event.

## What Changed

- `costService.byAgent`, `byAgentModel` and `byProject` return
`unpricedEventCount` (same filter as the summary: unpriced and not
subscription_included).
- Shared types `CostByAgent`, `CostByAgentModel` and `CostByProject` add
`unpricedEventCount`.
- The OpenAPI catalog entries for the cost reports that count unpriced
usage now say that costCents counts priced usage only, that unpriced
usage must be reported as unpriced (never $0.00), and that
/costs/data-quality lists those runs.
- Tests: embedded-Postgres service test (red on master, green here) and
an OpenAPI description test. A UI test fixture adds the new field.

## Verification

- `npx vitest run src/__tests__/costs-service.test.ts` (19 passed). The
new test fails with the master service.
- `npx vitest run src/__tests__/openapi-routes.test.ts
src/services/native-runtime/runner-api.test.ts` (2146 passed).
- `tsc --noEmit` for packages/shared and server.

## Risks

- Low risk. One added aggregate column per report and an added response
field. The UI does not read it yet.

## 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-11 13:36:03 -06:00
DottaandClaude Opus 5.5 c02de922af fix(chat): deliver the person's time zone to chat agents (#20898)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - People talk to agents in chat, and agents create routines and other
scheduled work from that chat
> - Paperclip does not record the person's time zone, so a chat agent
cannot read "every weekday at nine" or "today" in the person's zone
> - The agent then saves schedules in UTC, and the person gets the work
at the wrong hour
> - This pull request saves the browser time zone with the person's
personal preferences and gives it to chat agents in the task prompt
> - The benefit is that schedules and dates from chat match the person's
local time

## Linked Issues or Issue Description

**What happened**
A person in America/Chicago wrote in chat: "Every weekday at nine in the
morning, summarize Launch for me." The agent saved the routine schedule
`0 9 * * 1-5` with the UTC time zone. It replied that it used UTC
because the person's time zone is not recorded.

**Expected behavior**
The agent knows the person's time zone. It saves one enabled `0 9 * *
1-5` schedule with the time zone America/Chicago. Its reply names the
zone.

**Steps to reproduce**
1. Open the board in a browser with the time zone America/Chicago.
2. Open a chat with an agent.
3. Send "Every weekday at nine in the morning, summarize Launch for me."
4. Look at the routine trigger that the agent creates. Its time zone is
UTC.

## What Changed

- Database: add the nullable `time_zone` column to
`user_workspace_preferences` (migration `0468`). This is an
instance-wide personal preference by user, as AGENTS.md section 5
permits.
- Shared: `WorkspacePreference` has `timeZone`.
`updateWorkspacePreferenceSchema` accepts an optional IANA `timeZone`
(max 64 characters, checked with `Intl.DateTimeFormat`).
`coordinatorWorkspaceEnabled` is now optional. The request must contain
at least one field.
- Server: the preference service returns `timeZone` and updates only the
fields in the request. The activity log entry stays the same. The
OpenAPI entry uses the shared schema.
- UI: a new `useBrowserTimeZoneSync` hook in `Layout` saves the browser
time zone one time when it is different from the saved zone. It is
silent and best-effort. It uses the selected company as the audit
context.
- Prompt: for a conversation (any adapter), the task context reads the
chat owner's saved zone. Recovery keeps the zone that the first turn
admitted. The chat block adds one line only when the zone is known:
`The person's time zone is America/Chicago (from their browser). Read
times and dates they give, such as "nine in the morning" or "today", in
that zone, and save schedules such as routine triggers with that
timezone; say the zone when you report a schedule.`

## Verification

- `cd server && npx vitest run
src/__tests__/workspace-preferences.test.ts
src/__tests__/workspace-preferences-routes.test.ts
src/services/heartbeat/task-markdown.test.ts
src/__tests__/heartbeat-task-context.test.ts
src/__tests__/openapi-routes.test.ts
src/services/native-runtime/runner-api.test.ts`
- `cd ui && npx vitest run src/hooks/useBrowserTimeZoneSync.test.tsx
src/hooks/useCoordinatorWorkspaceEnabled.test.tsx
src/components/Layout.test.tsx`
- Red/green: without the task-context and task-markdown changes, the two
new prompt tests fail. With the changes, they pass.
- `pnpm check:token-gates` is clean. `tsc --noEmit` passes for
`packages/shared` and `packages/db`.
- Manual: open the board in a browser with a non-UTC zone. Then ask a
chat agent to create a weekday routine at nine in the morning. The
trigger uses the browser zone.

## Risks

- Low risk. The migration adds one nullable column.
- The PUT body is now more flexible: `coordinatorWorkspaceEnabled` is
optional. An empty body still gets a 400 response.
- The prompt grows by one line only for conversations with a known zone.
- The saved zone changes when the person uses a browser in a different
zone. The latest browser wins.

> 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 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-11 13:11:57 -06:00
Nicky LeachandClaude Opus 5.5 14cb69245f fix(projects): do not offer a single project storage option (#20875)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A project uses one workspace storage owner. The user chooses it when
they create the project.
> - The **Available in** selector shows the storage owners that the user
can choose.
> - Sometimes the default environment cannot be used, and only one
storage owner is available. In that case, the dialog shows a selector
with only one option. The user must select it before they can create the
project.
> - A selector with only one option is not a real choice.
> - This pull request uses the only available storage owner as the
default. The selector appears only when the user has two or more
options.
> - The benefit is that project creation asks for a choice only when
there is one.

## Linked Issues or Issue Description

Refs #20835

**What happened?**
An example is an instance in managed-sandbox-only mode that has the
agent-computer plugin but no managed sandbox. The local environment is
hidden, and the managed-only rule finds no default. The
project-environments endpoint returns one storage group and an error.
The new project dialog shows **Available in** with only one option and
disables **Create project** until the user selects it. An API request
without `environmentId` fails with `project_environment_unavailable`.

**What did you expect?**
When only one storage owner is available, the project uses it. No
selector appears.

**Steps to reproduce**
1. Turn on `enableManagedSandboxOnly`. Install the agent-computer
plugin. Do not add a sandbox environment.
2. Open **Create project**.
3. See an **Available in** selector with one option, and a disabled
**Create project** button.

## What Changed

- `projectEnvironmentOptions`: when the configured default is
unavailable and only one storage group exists, the first environment of
that group becomes `defaultEnvironmentId`, and `error` is null. When two
or more groups exist, an unavailable default still produces the error.
The server does not guess.
- Project create uses the same default, so an API request without
`environmentId` now succeeds in the single-group case.
- `NewProjectDialog` shows the selector only when `groups.length > 1`.
- Tests:
- A new server test covers managed-only mode with only the
agent-computer environment.
- A new server test covers an unavailable default with two storage
groups (the server still refuses to guess).
- The archived-default test now expects the server to use the only
remaining storage.
  - A new UI test checks that the dialog never shows one option.
- Docs: `doc/project-environment-compatibility.md` and
`docs/api/goals-and-projects.md`.

## Verification

- `pnpm vitest run src/__tests__/project-environments.test.ts` in
`server/`: 16 passed.
- `pnpm vitest run src/components/NewProjectDialog` in `ui/`: 12 passed.
- `tsc --noEmit` in `ui/`: clean.
- Server `tsc` reports 9 errors on this machine. None are in the changed
files. They are in `packages/adapter-utils` and a native-runtime test,
because local dependencies are stale. CI typechecks with a fresh
install.

## Risks

- Low risk. The change applies only when the default is unavailable and
exactly one storage group exists. In that case, the server picks the
group that the user would have had to choose anyway. Run placement does
not depend on the project, so this does not change where runs execute.
- No migration.

## 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
- [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-11 09:49:33 -08:00
DottaandClaude 889b33da72 fix(native): apply a saved accepted result on Retry instead of a new turn (#20819)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A native run on a remote sandbox can finish with an accepted result,
and then fail to save its workspace. Saved-work recovery saves the files
again from the retained sandbox, with no provider turn.
> - #20758 made a save that finds the sandbox disk full stop at once as
`sandbox_disk_full` and hand the task to the board with a Retry.
> - When the user frees space and presses Retry, the save now succeeds.
But the Retry then started a new agent turn (`strategy: continue`). That
turn repeats work that already finished and spends provider budget
again. The accepted result was not applied (`replacementDenied:
explicit_user_continuation`).
> - The decision for this case: when a durable record proves that the
run's result was already accepted, Retry applies that saved result and
starts no new turn.
> - This pull request makes Retry admission wait for the run's
saved-work holds and then applies the accepted result through the
existing preserved-result finalization.
> - The benefit is that a Retry after a failed save finishes the task
from the work that was already done, with no repeated provider work or
spend.

## Linked Issues or Issue Description

Refs #20758 (full-disk save rests as `sandbox_disk_full` with a Retry),
Refs #20810 (chaos fault DF1 for a full disk on the final workspace
save)

**What happened?**
A native run had an accepted, succeeded result. Its workspace save
failed because the sandbox disk was full, and the task rested at
`sandbox_disk_full` with a Retry. The user freed space and pressed
Retry. The save succeeded, and then Retry admission started a new run
with `strategy: continue`. The accepted result stayed unapplied (the
finalization coordinator stayed in `terminal_failure`).

Two paths caused this:
1. Retry admission let the Retry through as soon as the saved files were
verified. That can happen before preserved-result finalization runs: the
recovery worker dispatches the Retry right after one hold settles, while
the hold that Retry added for the same run is still scheduled. On master
this is deterministic in that order, and a race otherwise.
2. Preserved-result finalization reads the newest saved-work hold of the
run. While that hold was still scheduled, it declined, and nothing held
the Retry back.

**Expected behavior**
After the save succeeds, and a durable record proves that the run's
result was already accepted, Retry applies that result. The run and task
finish from it. No new `turn.start` and no new provider spend. If the
save fails again with a full disk, the task keeps the
`sandbox_disk_full` rest and a working Retry. If there is no proven
accepted result, Retry keeps the previous behaviour and starts one run.

**Steps to reproduce**
1. Give a native run an accepted, succeeded result whose copy-back
failed (`native_workspace_sync_out_retry_exhausted`).
2. Make the saved-work recovery save fail with
`remote_workspace_sandbox_disk_full`. The hold goes to the board.
3. Free space. Press Retry. Let recovery save the files, then dispatch
the Retry before preserved-result finalization runs.
4. On master, a new run starts with `previousRunId` set to the source
run, and the accepted result is not applied.

## What Changed

- `preserved-result-finalization.ts`: the eligibility checks move into
one read (`readPreservedResult`). They are unchanged: an accepted,
succeeded result of this run, a completed saved-work recovery with a
verified receipt, no Stop, no newer run, no current execution, and no
applied generation. A new read-only
`canResumePreservedResultFinalization` uses the same read without locks.
`resumePreservedResultFinalization` keeps the finalizer's lock order
(coordinator first) and takes the issue and run rows with NOWAIT, so it
cannot deadlock with the finalizer, owner-stop confirmation or Retry
dispatch; a busy row is retried briefly, and the last attempt waits for
its locks. It also writes an activity entry,
`issue.saved_result_finalization_resumed`, when it applies a result.
- `explicit-native-continuation.ts`: Retry admission holds an accepted
Retry (`evaluate`) of a run whose saved files are verified and whose
accepted result is still held, with the wait `finalizing_saved_result`.
It holds while a saved-work hold of that run is still `scheduled`,
`running` or `awaiting_evidence` and the result is otherwise applicable
(no Stop, newer run, other owner or current execution), or once
`canResumePreservedResultFinalization` is true. In every other case
admission is unchanged.
- `heartbeat/run-control.ts` (`resumeTaskRetry`): after dispatch, if
admission held the Retry with `finalizing_saved_result`, it calls
`resumePreservedResultFinalization`. That coalesces the Retry into the
source run with `strategy: finalize_result` and re-arms finalization.
This is one key read after admission; the resumption runs only for a
held Retry. The recovery worker already calls the same function after
each saved hold, so either path finishes the work.
- Tests: `workspace-recovery-disk-full.test.ts` now proves the new Retry
behaviour (red on master), a dispatch that lands while another hold of
the run is still saving (red on master), a Retry while the disk is still
full, and three negative cases that keep one new run.
`workspace-recovery-preparation-gates.test.ts` (s12 and RC-50b) and
`explicit-native-continuation.test.ts` (copy-back + `task_retry`)
encoded the old "new run" result for the same situation. They now expect
the saved result to be applied. A fixture helper `recoverRunHolds` runs
the recovery steps for every hold of a run, as the worker does.
- `ui/src/components/WorkspaceExportRecovery.tsx`: at the
`sandbox_disk_full` rest, the retained-file panel no longer shows its
own "Recover saved files" button and no longer hides the task's Retry.
Before, the API said `canRetry: true` and the note said "Retry saves the
files again", but the board showed only "Recover saved files" (seen in
the DF1 chaos wave). The panel now says to free space and use Retry. A
new UI test covers it. `pnpm check:token-gates` is clean.
- `doc/execution-semantics.md`: documents the Retry behaviour and its
conditions.

## Verification

Red/green (`cd server && npx vitest run
src/services/workspace-recovery-disk-full.test.ts`):

- On master source with the new tests: 3 failed, 3 passed (the 3
negative cases pass on master, as they should). Each failure shows a
second heartbeat run (the new agent turn): `expected [ …(2) ] to deeply
equal [ Array(1) ]`.
- On this branch: 7 passed. A fourth negative case (an operator Stop
while a hold is still settling keeps one new run) was added after
review; it also passes on master.
- Guard removal 1: with the `canResumePreservedResultFinalization` check
replaced by `true`, the negative test "an operator Stop recorded on the
run" fails (`expected [] to have a length of 1 but got +0`: the Retry
waits forever instead of starting its one run).
- Guard removal 2: with the "hold still saving" check removed, the test
"holds a Retry dispatched while another saved-work hold of the run is
still settling" fails (a second run starts).

Related suites pass on this branch: `workspace-recovery-disk-full`,
`workspace-recovery-preparation-gates`, `workspace-recovery-admission`,
`native-exhausted-finalization-retry`, `explicit-native-continuation`
(641 tests).

On the first revision, `task-retry`,
`workspace-recovery-retry-copyback`, `workspace-recovery-warm-handoff`,
`preserved-native-response-wait.integration`, `deferred-execution-wait`
and `deferred-execution-receipt-work` passed (317 tests). Other retry
and recovery suites were not run locally; required CI `general_tests`
covers them. After the review fixes, the five related suites above
passed again (642 tests).

`npx tsc --noEmit -p server/tsconfig.json` is clean.

`pnpm verify --explain` selects a broad plan (exit 2) because
`doc/execution-semantics.md` is a global CI input. Required CI supplies
that coverage.

## Risks

- Behaviour change: an accepted Retry of a terminal run with an
accepted, succeeded result and verified saved files now applies that
result and starts no run. Before, it started one new run. This is the
intended change. It also covers the s12 and RC-50b recovery-handoff
cases, which used to start a new run.
- A held Retry waits while any saved-work hold of the run is still
saving. The recovery worker drives those holds to `complete` or
`exhausted`. When a hold ends `exhausted` or the records do not prove
the result, the wait ends and the Retry starts one run, as before. A
hold that stays in `awaiting_evidence` keeps the Retry waiting with
"Paperclip is finishing the saved result."
- Upgrade-safe: no persisted shape changes. The checks read existing
records (`native_run_results`, `native_run_finalizations`, the recovery
action evidence, workspace receipts). A record written by an older
server that lacks any required fact keeps the old behaviour. An older
server that reads the new state sees a coalesced Retry and a re-armed
finalization, which it already handles.
- No new work before `turn.start` on the normal path. Admission adds the
eligibility read only when it has already proven an accepted result and
verified saved files for a Retry of that run. `resumeTaskRetry` adds one
key read after admission.
- Lock order: `resumePreservedResultFinalization` keeps the finalizer's
order (coordinator, then issue, then run). Because it can now run beside
Retry dispatch (which locks the issue first), it takes the issue and run
rows with NOWAIT. A busy row ends the attempt, and it retries up to 9
times at 100 ms. The 10th attempt waits for its locks, as this
resumption always did, so a recovered result is never left unapplied.

## Model Used

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

## Checklist

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

> - Paperclip is the open source app people use to manage AI agents for
work
> - A repository project gets its working files from core staging or
from a workspace storage plugin
> - #20459 reported that an installed storage plugin attached every new
repository project, so sandbox agents could not use those projects
> - #20587 fixed creation with an **Available in** choice that selects
storage
> - #20651 then saved an environment on every new project and used it
before the agent default to place runs. That solved a problem that did
not exist: the project needs storage, not a run environment
> - Creation saves the first environment of the chosen group, so a
project silently sends every agent's runs to that environment. Project
Configuration also shows two controls (Environment and Workspace
storage) for one decision
> - This pull request removes the project run environment and makes
storage the only placement choice a project owns. Storage is fixed at
creation
> - The benefit is that runs follow the agent again, and project
settings show one clear, read-only fact about storage

## Linked Issues or Issue Description

Refs #20459. Follow-up to #20587 and #20651.

Refs #11644. That issue asks for a project-level run environment
override. This pull request removes the override that #20651 added for
it, because the override is not necessary to fix #20459. #11644 stays a
separate feature request.

Related: #18176 (open) repairs placement through the legacy
workspace-policy field. This pull request does not restore project
placement, so it conflicts with the direction of #18176.

## What Changed

- **Remove the project run environment.** Migration `0466` drops
`projects.environment_id`. Run placement no longer reads the project.
Runs use the agent default, then the instance default, then local
execution. The low-trust task sandbox and the enforced sandbox policies
keep their precedence.
- **Keep the #20651 recovery and binding guards.** A recovered run still
uses its admitted environment lease (source `run`). Company-binding
checks before admission and during lease creation stay.
- **Keep the creation choice.** **Available in** in New Project still
selects storage. It still shows only when the environments need more
than one storage owner. That can only occur when a storage plugin is
installed. `POST /projects` with `environmentId` selects the storage of
that environment and does not save the environment.
- **Fix storage at creation.** `PATCH /projects/:id` rejects any storage
change with `409 project_storage_locked`, from core to a plugin and from
a plugin to core. Before, a core project could move to a plugin.
- **Drop legacy writes.** Older clients can still send
`executionWorkspacePolicy.environmentId`. The service removes it on
create and update.
- **Simplify Project Configuration.** Remove the Environment and
Workspace storage selectors. Show an advisory **Available in** list of
the environments that can use the project's storage. Show it when a
storage plugin is installed or when the project uses one. When the
project's plugin is not installed, show **Storage plugin unavailable**.
Without a storage plugin, show nothing.
- **Issue workspace card.** Remove the "project default" environment
label.
- **Shared contract.** Remove `Project.environmentId`, the
`environmentId` project field, and `expectedStorageProviderPluginId`
from the update schema. The create schema keeps `environmentId` and
`expectedStorageProviderPluginId` from #20587.
- **Docs and maps.** Update `doc/project-environment-compatibility.md`,
`docs/api/goals-and-projects.md`, and the project feature and
agent-access maps.
- **SDK source pins.** Refresh
`tests/runner-e2e/source-sdk-public-inputs.json` for the two shared
files. The shared file blobs return to their values before #20651. The
refresh also corrects the `pnpm-lock.yaml` pin and `lockSha256`:
`master` pins an older lockfile blob than the one it contains.

## Verification

- Targeted server suites pass (227 tests): `project-environments`,
`project-workspace-provider.integration`,
`heartbeat/environment-selection`, `execution-workspace-policy`,
`environment-selection-route-guards`, `environment-run-orchestrator`,
`openapi-routes`.
- Related server suites pass (349 tests): `environment-service`,
`environment-routes`, `plugin-workspaces.integration`, `issues-service`.
- Targeted UI suites pass: `ProjectProperties.concurrency`,
`IssueWorkspaceCard`, `NewProjectDialog`,
`NewProjectDialog.managed-sandbox`, `ProjectDetail`.
- New tests:
- Creation saves storage, not an environment. Core and plugin projects
both reject a storage change with `project_storage_locked`.
  - A legacy policy `environmentId` is dropped on create and update.
- A new run follows the agent default for any project. A recovered run
keeps its admitted lease after the agent default changes.
- Project Configuration has no storage or environment selector. The note
is hidden without a plugin. It lists the core or plugin environments for
the project's storage. It shows **Storage plugin unavailable** for a
removed plugin.
- `pnpm --filter @paperclipai/server typecheck`, `pnpm --filter
@paperclipai/ui typecheck`, and `pnpm --filter @paperclipai/shared
typecheck` pass.
- `pnpm --filter @paperclipai/db check:migrations`, `pnpm
check:token-gates`, `node scripts/check-maps.mjs`, and `node
tests/runner-e2e/source-sdk-public-inputs.mjs <HEAD> --check` pass.
- Not run locally: the full `pnpm test:run`, `pnpm build`, and browser
suites. CI supplies that coverage.
- Manual check: open Project Configuration on an instance with a storage
plugin. Confirm that there is no Environment or Workspace storage
selector, and that **Available in** lists the environments.

## Risks

- **Migration.** `0466` drops `projects.environment_id` and its foreign
key. The migration deletes the saved project environments. Apply it
before you serve the new revision. An old server revision that reads
`projects` fails until it stops. This is the same pattern as earlier
drop-column migrations (`0229`, `0289`).
- **Behavior change for existing projects.** Projects created after
#20651 shipped have a saved environment that overrides the agent. After
this change, their runs follow the agent default again. This is the
intended fix. Before #20651, a project environment selection was not
applied to runs (#11644), so no older behavior is lost.
- **No path from core to a plugin.** A core project, including one
created before a plugin was installed, cannot move to the plugin. To use
plugin storage, create a new project.
- **API.** A `PATCH` that sends `environmentId` or
`expectedStorageProviderPluginId` no longer has an effect. The schema
strips these fields.

> 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`) by Anthropic, in Claude Code, with
tool use (shell, file edits, test runs). Extended thinking was
available.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 08:00:21 -08:00
DottaandClaude Opus 5.5 0b8ba1ac92 fix(queue): show messages a booting run's first turn takes as sent (#20822)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Task chat lets a person send follow-up messages to a running agent,
and steer them into the live turn
> - A sandboxed run (for example on Daytona) can take minutes to boot
before its provider session exists
> - A message sent during that boot cannot be steered, so it waits in
the steering pane
> - The runner then puts the message into its first turn, and the server
removes it from the queue, but the view has no signal for this, so the
message stays in the steering pane as "queued"
> - That stale local entry also hides the queue id, so Steer and Discard
fail for every later message
> - This pull request tells the view which messages the first turn
takes, and drops local queue entries whose run has settled
> - The benefit is that a message sent while the sandbox boots shows as
sent at once, and later messages keep working Steer and Discard

## Linked Issues or Issue Description

**What happened?**
A person sent a follow-up while the agent's sandbox was booting. The
message stayed in the steering pane with Steer unavailable. After the
agent started, the message stayed there. The agent had received it in
its first turn. Later, Discard on that message failed with "Couldn't
discard. Message is still queued.", and a new message also stayed in the
steering pane.

**Steps to reproduce**
1. Assign a task to a Paperclip Runner agent that runs in a Daytona
sandbox.
2. While the sandbox boots, send a follow-up message.
3. Wait for the agent's first response. The message is still in the
steering pane.
4. After the run hands off to a successor run, send another message and
try to discard the first one.

**Expected behavior**
A message sent while the run is starting shows as a sent message. Later
messages keep working Steer and Discard.

**Actual behavior**
The first message stays in the steering pane. Discard fails, and the new
message cannot be steered until the page reloads.

## What Changed

- `firstTurnQueuedCommentIds` (server, `first-turn-queued-input.ts`)
returns the messages in a deferred queue that the target run's first
turn already sealed, or will take while it is still starting. It uses
the same eligibility rule as `claimFirstTurnQueuedInput`. It reads only
lean columns, so the queue poll does not transfer run snapshots or large
wake payloads.
- The queue read path and the queue mutation path both put these ids in
a new optional `firstTurnCommentIds` field on `IssueQueuedCommentQueue`.
- The task view puts each of those messages in the conversation as input
of that run, at its submission time, and not in the steering pane.
- The task view no longer treats a saved message as a pending local
queue entry after its target run is no longer live. A request that is
still in flight keeps its original target.

## Verification

- Red-green server test: `issue-queued-comments-routes.test.ts` "reports
the messages a preparing/native run's first turn takes, before and after
it seals them" and "reports no first-turn messages once the run's turn
already started". Before the fix, `firstTurnCommentIds` was `undefined`.
- Red-green UI tests: `IssueDetail.test.tsx` "shows a message the
booting run's first turn takes as sent, not queued" (still starting, and
sealed) and "keeps queue controls for a new message after the run that
took an earlier one hands off". Before the fix, the queue held
`['new-message', 'boot-message']` and the queue id was withheld.
- Reproduced on staging first: the run's context held a first-turn
receipt for the stuck message, and the server queue had no entries,
while the view still showed it as queued.
- `npx vitest run` for `issue-queued-comments-routes`,
`queued-comment-consumption`, `queued-comment-postgres`,
`queued-comment-use-cases`: 172 passed. The existing queue-poll byte
budget tests still pass.
- `npx vitest run src/pages/IssueDetail.test.tsx
src/components/task-chat src/lib/issue-queued-comment-queue
src/lib/optimistic-issue-comments`: 1288 passed.
- `tsc --noEmit` for shared, server, and ui: no new errors.
- `pnpm verify` exits 2 because the change touches `packages/shared`.
Required CI supplies the broad coverage.

## Risks

- Low risk. The new field is optional and only for display. Delivery
still belongs to the first-turn claim and the deferred wake.
- If a starting run fails before its first turn, the field goes away and
the message shows in the queue again for the next run. This is correct,
because the deferred wake still holds it.
- The queue read adds one lean run read and one comment read only while
a deferred queue targets a running run.

## Model Used

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

## Checklist

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

> - Paperclip is the open source app people use to manage AI agents for
work
> - Tasks can depend on other tasks. A task detail shows its blockers in
the properties panel and in a "Still blocked by" notice.
> - The task detail response embeds a summary of each blocker, including
its status.
> - Live updates refresh only the changed task's own cached detail. They
do not refresh other cached tasks that embed it.
> - So when a user marks a blocker done, an open dependent task keeps
the old blocker status. The "Blocked by" chip shows "blocked" while the
Subtasks chip shows "done". The page looks stuck until a reload or until
a run starts.
> - This pull request refreshes every cached task detail that embeds the
changed task in `blockedBy` or `blocks`.
> - The benefit is that dependent tasks show the current blocker status
immediately.

## Linked Issues or Issue Description

**What happened?**
A user marked a blocker task done by hand. The dependent task page still
showed the blocker with a "blocked" icon and a "Still blocked by"
notice. The same blocker showed as "done" in the Subtasks row of the
same panel. The server had already resolved the dependency and woke the
dependent task. Only the open page was stale.

**Steps to reproduce**
1. Open task A, which is blocked by its subtask B.
2. In another view, mark B done.
3. Go back to A without a reload. "Blocked by" still shows B as blocked,
and the notice says "Still blocked by B". "Subtasks" shows B as done.

**Expected behavior**
The "Blocked by" chip and the notice update when B changes, the same as
the Subtasks row.

## What Changed

- `LiveUpdatesProvider`: when activity changes a task's status, retry or
recovery fields, invalidate every cached `issues.detail` query whose
`blockedBy` or `blocks` summaries contain that task. The match uses the
UUID or the identifier, because authenticated events can omit the
identifier.
- Add a regression test for this in `LiveUpdatesProvider.test.ts`.

## Verification

- Red: the new test failed before the fix. The dependent detail was not
invalidated.
- Green: `pnpm vitest run src/context/` in `ui` passes (311 tests).
- `pnpm verify` passes (549 selected test files).
- `tsc -p ui --noEmit` shows no new errors. Existing errors in
`ui/storybook/support/feature-map` are unrelated.
- Staging data check: the server returned the blocker as `done` in the
dependent's `blockedBy`. The stale display came only from the client
cache.

## Risks

Low risk. The predicate scans only cached task details. It refetches
only details that embed the changed task. Refetches use the existing
invalidation batcher. The list of actions excludes comments and document
edits, so busy tasks do not cause extra refetches of their dependents.

> 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 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-11 09:23:50 -06:00
DottaandPaperclip aaf1337c7b fix(slack): require mentions only for channel roots (#20821)
## Thinking Path

> - Paperclip helps people manage AI agents for work.
> - Slack connects people to those agents through channels and threads.
> - A mention can start a conversation without making the bot answer all
channel traffic.
> - The current setting also requires mentions on every thread reply.
> - This interrupts an existing conversation and can cancel a queued
reply.
> - This change requires mentions on channel roots and allows replies
without a mention.
> - People can continue a thread without adding the bot name to each
message.

## Linked Issues or Issue Description

**What existing behavior does this improve?**
The Slack connection setting that requires an at-mention.

**Current behavior**
When the setting is on, a channel thread reply must mention the bot. The
delivery and execution checks reject a reply that has no mention.

**Proposed behavior**
Require a mention on a top-level channel message. Allow replies in
existing threads without a mention. Keep the current DM access controls.

**Reason and benefit**
A person can continue an existing conversation without repeating the bot
name.

**Breaking changes**
Existing connections with the setting on will accept thread replies
without a mention. The saved setting, default value, and API field stay
the same.

## What Changed

- Rename the control to **Require at-mention in channels**. Explain the
thread and DM rules in the help text and operator documentation.
- Keep the mention policy in the Slack module. Use the thread root and
message IDs to identify a reply.
- Call that policy from durable admission, wake authorization, and
queued run start. These shared paths must enforce the same setting when
a delivery resumes. Other providers keep their current behavior.
- Update PostgreSQL integration tests for thread replies, DMs, queued
deliveries, and combined wake inputs. Add tests that reject incomplete
reply identity.

## Verification

- Reproduced rejected thread replies and cancelled queued reply runs
before the fix.
- Passed 16 focused PostgreSQL integration cases after the fix.
- Passed 71 tests on revision `31b1adede2b92bad24b8fa21b8f6fffa9bd26d04`
after the rebase. This includes the full durable wakeup suite, Slack
policy tests, and eight selected Slack delivery cases.
- Command: `pnpm exec vitest run
server/src/services/chat-channels/slack/wake-authority.test.ts
server/src/__tests__/durable-chat-wakeup.test.ts
server/src/__tests__/chat-channels.integration.test.ts -t "at-mentions
by default|thread replies without mentions|rechecks queued
Slack|preserves running Slack tool authority|Slack channel mention
policy|durable inbound chat scheduler receipts"`.
- Strengthened the queued-root test to check the mention-rejection
reason and content redaction. A temporary change that disabled mention
enforcement caused this test to fail with the unrelated-message reason.
The real policy is restored.
- Removed the added source comments to follow the contribution rules.
- `node scripts/check-module-boundaries.mjs` and `git diff --check`
pass.
- `pnpm verify` returns exit 2 because the operator documentation is a
global CI input. Required CI must supply the wider coverage.
- Local preflight installed dependencies and built the plugin SDK. It
could not build Runner Rust binaries because this environment has no
`cargo` executable. The focused server tests run without those binaries.
- Live Slack and browser tests were not run. This change uses the
existing provider message and thread identities.

- Required CI passed on `31b1adede2b92bad24b8fa21b8f6fffa9bd26d04`: [PR
workflow](https://github.com/paperclipai/paperclip/actions/runs/43035551294),
including typecheck, build, server/chat tests, Runner checks, and
browser E2E. Current-head Greptile and Paperclip reviews are both 5/5
with no open findings.

## Risks

- Existing connections with mention gating on will now accept
unmentioned thread replies. This is the intended behavior change.
- A missing or invalid thread or message ID does not grant the reply
exception.
- No schema migration is required. Destination, sender, and company
access checks stay in place.
- The existing chat integration test file is large. This change updates
its current Slack cases to reuse the full delivery and authorization
fixture.

## Model Used

OpenAI Codex, based on GPT-6, with reasoning and code execution tools.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-11 08:26:52 -06:00
DottaandPaperclip 123ee31552 fix(ui): omit task IDs from parent breadcrumbs (#20804)
## Thinking Path

> - Paperclip helps people manage AI agents and their work.
> - Task headers show a path from the parent task to the current task.
> - The parent link adds a task number before its title on desktop.
> - That extra number makes the parent title change when users open the
task.
> - This pull request shows the parent title without its number at every
width.
> - Users can follow the parent link with a consistent task title.

## Linked Issues or Issue Description

Related: #20366 added parent task navigation. #10933 added the current
task number.

**What existing behavior does this improve?**

Parent links in task header breadcrumbs.

**Current behavior**

A desktop parent link includes its task number. A private parent chip
also shows its number on desktop.

**Proposed behavior**

Show only the parent title at all widths. Show Private for a private
parent. Keep the current task number, title limits, and responsive
truncation.

**Reason and benefit**

The parent title stays the same when the user follows the link.

## What Changed

- Remove the parent identifier from the task page breadcrumb data.
- Remove the unused viewport dependency from the breadcrumb effect after
review.
- Show the Private label at all widths for a private parent.
- Update component tests, page navigation tests, and Storybook
assertions.
- Document title-only parent links in DESIGN.md.

## Verification

- Six updated page regressions fail before the fix and pass after it.
- All 285 focused BreadcrumbBar and IssueDetail tests pass.
- UI typecheck and token gates pass.
- Catalog validation and all 30 map/catalog tests pass.
- Storybook build passes. All ten Chromium stories finish render/play
with no page errors.
- Browser checks cover 1440, 768, 390 and 320 pixel widths, light and
dark themes, private parents, and keyboard parent navigation.
- Parent numbers are absent at every width. Both long titles truncate on
phones. The current task number stays visible, and the page has no
horizontal overflow.
- Private labels stay the same during desktop to phone to desktop
resizing. Seven screenshots are saved as task evidence.
- These checks mount the production header with fixture data. Page tests
use mocked APIs. No live backend authorization or pixel baseline
comparison is claimed.
- [CI run
43020142521](https://github.com/paperclipai/paperclip/actions/runs/43020142521)
passed on c848073ee1. Greptile reviewed
that commit at 5/5.
- [The independent
review](https://github.com/paperclipai/paperclip/pull/20804#issuecomment-6795977289)
found one unused viewport dependency. Commit
d32c7aa6dc removes it. All 285 focused UI
tests and UI typecheck pass after this change.
- Greptile and the independent Paperclip review both give
d32c7aa6dc a clean 5/5. There are no
inline review threads.
- [CI run
43022686830](https://github.com/paperclipai/paperclip/actions/runs/43022686830)
attempt 1 passed all jobs except Runner Rust and its summary gate. The
Rust process-reaping test reported one unattributed orphan after its
owned tool had stopped. The Rust source tree and test command match the
preceding passing run. Same-user orphan scans can overlap with parallel
tests; the log does not identify the orphan's source.
- [CI attempt
2](https://github.com/paperclipai/paperclip/actions/runs/43022686830/attempts/2)
passed on d32c7aa6dc after the one
failed-job retry. Runner Rust and the summary gate now pass. All 63
current contexts are complete: 59 success, one neutral Paperclip review,
and three conditional skips. There are no pending or failed contexts.
The saved responsive screenshots remain current.
- Workspace preflight could not complete because Cargo is absent. The
shared and plugin SDK builds passed. No Runner source changed.

## Risks

- Low risk. The change affects parent header labels.
- Parent routes and collection filters stay intact. Navigation tests
cover route state, saved state, and legacy query links.
- Private parents remain noninteractive.
- No API, schema, stored title, or shared breadcrumb renderer changes.

## Model Used

- OpenAI Codex, based on GPT-6, with reasoning, tool use, code editing,
and browser verification. The exact served snapshot ID and context
window size are not exposed.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-11 07:09:12 -06:00
DottaandClaude f77b8a288e fix(ui): keep the GitHub star prompt off tasks that need a Retry (#20713)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A task that fails shows a banner with a Retry button. The user
clicks Retry to run the task again.
> - Self-hosted boards also show a "You completed 5 tasks! Star on
GitHub" modal (#20453).
> - The modal can open on the task page while the task shows its Retry
banner.
> - The modal is a Radix dialog. It sets `aria-hidden="true"` on the
rest of the page and sets `pointer-events: none` on the body. The Retry
button is visible, but a click does not reach it and assistive
technology cannot find it.
> - This pull request makes the star prompt wait for a calm moment. It
does not open on a task or agent chat page, and it does not open while a
task Retry or status banner is on screen.
> - The benefit is that a visible Retry is always reachable.

## Linked Issues or Issue Description

Refs #20453

**What happened?**
A restart chaos run left a task in the failed state with the Retry
banner. The "You completed 5 tasks!" star modal opened over the same
page. The Retry button was in the DOM and visible. Playwright
`getByRole('button', { name: 'Retry' })` did not find it, because the
modal had set `aria-hidden="true"` on the banner. The body also had
`pointer-events: none`, so a user click on Retry did not reach the
button.

**Expected behavior**
A promotional prompt never covers a task that needs the user. A visible
Retry is always reachable.

**Steps to reproduce**
1. On a self-hosted instance, finish five tasks in one company. Do not
open the star prompt before.
2. Let a sixth task fail so that it shows "Couldn't finish this attempt"
with Retry.
3. Open that task. The star modal opens over the Retry banner.

**Paperclip version or commit**
master at 200c6e40e2

## What Changed

- `GitHubStarPrompt` waits while the location is one task
(`/:prefix/issues/:ref`) or one agent chat (`/:prefix/chats/:agent`).
Task list views (`issues/all`, `issues/done`, and so on) do not count as
one task.
- `GitHubStarPrompt` waits while a task Retry
(`data-testid="task-execution-retry"`) or a task status banner
(`role="status"` with `aria-label="Task status"`) is on screen. It uses
the same DOM observer that already waits for other dialogs.
- If the prompt is open and the user goes back to a task or chat page,
or a task Retry or status banner appears, the prompt closes.
- `useModalOpen` in `AnnouncementWell.tsx` takes an optional selector.
The default is the existing modal selector, so other callers do not
change.
- New tests in `GitHubStarPrompt.test.tsx`: the prompt stays closed
while a Retry banner is on screen and the banner is not `aria-hidden`;
the prompt stays closed on a task and a chat page and opens on the task
list. The prompt closes when the user goes back to a task, and when a
Retry appears.

## Verification

- Red first: the two new tests failed on master. A temporary probe in
the red run showed the prompt open, the banner with
`aria-hidden="true"`, and `document.body.style.pointerEvents ===
"none"`.
- Green: `cd ui && npx vitest run
src/components/GitHubStarPrompt.test.tsx
src/components/BoardDialogs.test.tsx` passes.
- `pnpm verify` passes (481 UI test files).
- `pnpm check:token-gates`: all gates clean.
- `npx tsc --noEmit -p ui`: no errors in the changed files.

## Risks

- Low risk. The prompt can show later than before, because it waits for
a non-task page. It still shows only once per browser.
- The "seen" marker is written only when the prompt opens, so a user who
stays on task pages does not lose the prompt.

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-11-10 22:36:40 -06:00
DottaandPaperclip 7f72ea7534 fix(links): retain PR status across page reloads (#20654)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Task messages contain GitHub pull request links.
> - A hover loads PR status into a shared memory cache.
> - A page reload clears that memory and returns each link to an unknown
state.
> - The browser already saves selected reads for each user and company.
> - This pull request adds learned link previews to that cache.
> - Reloads show the saved status. Only user interest can refresh stale
previews.

## Linked Issues or Issue Description

**What happened?**

After a user loaded PR status, a page reload removed the learned state.
The browser persistence rules excluded link-preview queries. Related
implementation: #20593. Searches of open and closed PRs found no
duplicate reload-cache fix.

**Expected behavior**

Keep learned PR status across reloads for the same account and company.
Do not load every PR on page render. Reuse fresh results and refresh
stale results on hover or keyboard focus.

**Steps to reproduce**

1. Open a task with repeated GitHub PR links.
2. Hover one link and wait for its status.
3. Reload the page.
4. Before this fix, both links return to unknown until another hover.

**Paperclip version or commit**

Reproduced against master source at
`30403781b9418355c78dc0ed978366af77036e76`.

## What Changed

- Add link-preview queries to the existing user and company persistence
rules.
- Keep at most 100 previews across accessible companies. Use the
existing 24-hour retention, build boundary, and sign-out cleanup.
- Keep the original read time and freshness when restoring previews.
Other query types keep their existing restore behavior.
- Add regressions for saved status, negative results, bounds, age,
account changes, membership loss, and sign-out.
- Add a production-component Storybook example and browser reload
checks. Update the cache documentation.
- Use one query-key definition for the component, persistence rules, and
cache tests.

The browser read-cache module owns persistence and account cleanup. This
change extends that owner. The link service and provider API contracts
stay the same.

## Verification

- Red: the new reload and retention tests failed because a fresh client
had no saved PR data.
- Green: 102 focused UI tests passed across the cache, sign-out,
account-query, and query-client suites.
- UI typecheck passed.
- Storybook catalog generation and the static Storybook build passed.
- All 21 browser checks passed with retries disabled. They prove status
and negative-cache restoration after real reloads, no eager requests, no
fresh-hover refetch, and one stale-interest refresh shared by duplicate
links. Existing light/dark and narrow PR states, keyboard access, exact
URLs, and card positioning also pass.
- The first browser attempt could not launch because this session lacked
Chromium. Chromium was installed before the complete passing run; that
first attempt supplies no test verdict.
- `pnpm verify --explain` returned exit 2. The browser test path
requires broad CI coverage. No full local suite is claimed.
- The supplied staging page was not accessible through the web tool. No
deployed acceptance is claimed.
- CI on the first head failed in a Slack long-publication timeout and a
font-loading-frame browser check. Both files are outside this change.
The two publication cases passed when run locally in isolation. The font
case also passed locally against the built UI. [Final-head
CI](https://github.com/paperclipai/paperclip/actions/runs/42909433167)
passes on `e19d5b8bb1a910b1cbb9bb9a62ab87cdf684737f`. Typecheck, build,
tests, E2E, and Rust checks pass. There are 58 successful check runs,
three expected skips, and one neutral Paperclip Review check. The legacy
Snyk status passes.

Both Greptile and the supplemental Paperclip review report 5/5 on the
final head. No actionable findings or unresolved review threads remain.
The Paperclip Review check itself has a neutral conclusion.

## Risks

- PR display data now remains in browser local storage until expiry,
build change, or sign-out. Each query requires the matching user and an
accessible company.
- A stale saved status can remain visible for up to 24 hours. Hover or
keyboard focus refreshes it after the provider freshness window.
- Storage restrictions retain the existing memory-only fallback. No
schema migration is required.

## Model Used

OpenAI Codex, based on GPT-6, with reasoning, repository tools, code
execution, and browser automation. The runtime does not expose the exact
serving model ID or context-window size. No subagents were used.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-10 17:43:06 -06:00
Devin FoleyandPaperclip bbf5cfeb62 fix(agents): return to the index after termination is accepted (#20667)
Return to the Agents index when termination is accepted, including while asynchronous cleanup continues, so the profile does not remain on an Agent not found error.

Cover both action menus, roster refresh, failed requests, and unsaved changes during pending termination.

Verified with 53 focused tests, the full UI suite (11,949 passed; one skipped), UI typecheck and build, all applicable CI checks, and Greptile 5/5 with no unresolved comments.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-11-10 15:38:00 -08:00
Nicky Leach 0d4b1d8c89 Gate agent plugin tabs by execution environment (#20657) 2026-11-10 15:04:44 -08:00
DottaandPaperclip a3b34a9d88 fix(ui): allow queued message deletion before queue acknowledgement (#20553)
## Thinking Path

Paperclip lets a user queue a message while an agent works. The user
must be able to remove that message before delivery starts. A new local
message can clear the displayed queue ID until the server accepts it.
This also disables trash for older saved messages. The page rejects the
action before it sends a request.

This change keeps trash available when the server says the user can
discard the message. When the queue ID is absent, the page uses the
existing comment cancellation route. The server finds the queue and
checks the author and dispatch state in the same transaction.

## Linked Issues or Issue Description

**What happened?**

Trash stays disabled for a saved queued message while another message
waits for queue acknowledgement. The page handler also throws before it
sends a cancellation request.

**Expected behavior**

Trash removes the selected queued message before dispatch. A message
that has started dispatch stays visible with an error. Other queued
messages stay queued.

**Steps to reproduce**

1. Send a follow-up while an agent works.
2. Send another follow-up before the queue read includes it.
3. Try to discard the first saved message.

**Paperclip version or commit**

Reproduced from source at `0e3fdebea825`.

**Deployment mode**

Source checkout on Linux. UI tests use jsdom. Route tests use isolated
embedded PostgreSQL.

Related work: #15865 changes mixed-queue interrupt handling. #15939
changes recovery after a queued message is discarded. #15465 changes
discard permissions for agent-authored entries. This change fixes trash
for a user's own saved messages while queue acknowledgement is pending.

## What Changed

- Keep trash enabled for a saved message with discard permission when
the displayed queue ID is absent.
- Use the existing author-only cancellation route when queue identity is
pending. Keep the revision-checked route when queue identity is present.
- Keep failed cancellations visible. Refresh the queue after
cancellation and remove successful deletions from comment caches.
- Cover deletion, dispatch conflicts, permission failures, network
failures, history reloads, final-message cleanup, and concurrent
dispatch.

## Verification

- Red: three regression cases failed on the original production code.
The trash button was disabled. Both page cases made no cancellation
request.
- Green: five complete UI suites pass, with 767 tests and one existing
skipped test. These cover the page, queue component, thread, queue
merge, and optimistic comments.
- Green: 11 focused PostgreSQL route cases pass. They cover both routes,
queue promotion, final-message cleanup, authorship, dispatch conflicts,
and a concurrent dispatch claim.
- `pnpm check:token-gates` and `git diff --check` pass.
- `pnpm --filter @paperclipai/ui typecheck` passes.
- The GitHub tree is `d4f0d61cbef2620c1fd62e6988ad80ccb1ba6173`. It is
identical to the tested local tree after merging master `902f3767960d`.
- `pnpm verify` selected more than 1,500 server test files. That broad
local run was stopped without a pass claim. CI will supply broad build
and test coverage. The preflight helper could not build Rust because
Cargo is absent.
- [CI
Build](https://github.com/paperclipai/paperclip/actions/runs/42835584447/job/129897150252)
and [browser
E2E](https://github.com/paperclipai/paperclip/actions/runs/42835584447/job/129901676175)
pass for head `44dea22faea51a6664a6f45852a42f62f5c72bbd`.
- [Greptile
review](https://github.com/paperclipai/paperclip/pull/20553#issuecomment-6770202777)
gives the current head 5/5 with zero comments and no unresolved review
threads.
- [Final CI
run](https://github.com/paperclipai/paperclip/actions/runs/42835584447)
passes for head `44dea22faea51a6664a6f45852a42f62f5c72bbd`: 55 check
runs passed and 4 were skipped. No failures or pending checks remain.
Typecheck, Build, all general test shards, serialized suites, Runner
checks, and browser E2E passed. Snyk also passed.

## Risks

The fallback has no client queue revision. The existing server
transaction finds the queue for the selected comment and checks
ownership and dispatch state. Tests cover rejection after dispatch and
when another user owns the message. No API contract or schema changes
are required. No visual tokens change. Existing user documentation
remains accurate.

## Model Used

OpenAI GPT-6 in Codex. The runtime does not expose a more specific
provider revision or context window size. Used reasoning, repository
tools, code edits, and test execution.

## Checklist

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

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-10 16:38:58 -06:00
Nicky LeachandPaperclip f30ac226d6 Keep project environment mappings independent of shared defaults (#20651)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A project supplies work context and repository storage to task runs.
> - Creation selected a storage owner but did not retain the selected
environment on the project.
> - Configuration stored an environment inside checkout policy, but
policy reads and run selection dropped it.
> - Projects therefore continued to follow shared agent and instance
defaults.
> - This pull request saves placement on each project and uses it for
new runs.
> - The benefit is an independent Project to Environment association
with the existing storage, trust, and recovery safeguards.

## Linked Issues or Issue Description

Refs #20459 and #11644. Follow-up to #20587.

The reported journey is Project > Configuration: changing one project's
environment must not change another project's mapping or shared
defaults.

Related open PR #18176 repairs placement through the legacy
workspace-policy field. This follow-up introduces a separate project
association, saves creation defaults, and migrates saved legacy
references. It overlaps that PR's placement, permission, and recovery
safeguards. The changes should be reviewed together. #9588 concerns
environment-variable response privacy and does not address placement.
ROADMAP.md has no separate project-association work planned.

## What Changed

- Add nullable `projects.environment_id` and expose
`Project.environmentId`. Save the effective creation default once on the
project. Keep environment placement separate from checkout/isolation
policy.
- Read and write Environment directly in Project Configuration. Keep the
control available with isolated workspaces disabled. Each form reads its
project's value, even when environment choices share a query cache.
- Offer environments compatible with the current storage owner. Change
the target and storage together when the existing storage selector
attaches a core project to a plugin. Preserve retained-data restrictions
and validate the observed storage owner again on save.
- Resolve new task runs from their organizational project's target
before agent and instance defaults. File sources from another project do
not change placement. Explicit low-trust sandbox choices and enforced
sandbox policies retain precedence. External provider placement remains
separate.
- Restore admitted native runs from their owned environment lease. Later
project edits apply to new admissions. Require workspace-write
permission for environment changes and clears. Check company bindings
before runtime admission and during lease creation.
- Migrate valid saved policy references into the project column.
Normalize legacy API writes. Count and clear matching project references
when an environment is deleted. Update API and behavior documentation.
- Refresh the maintained SDK source registry for the two changed shared
files. Runtime pins are unchanged.

## Verification

- [Hosted
CI](https://github.com/paperclipai/paperclip/actions/runs/42908045146)
is green on `8b3b7d5b5aed51fc4c8e9c2e3506508bbaf9e00c`: **60 successful
checks**, three skipped gates, no failures, and `ci / verify` passed.
GitHub reports no merge conflicts. Standard Greptile scored the same
commit **5/5** with both review threads resolved.
- Source checkout on macOS arm64, Node 26.12.0, with fresh isolated
PostgreSQL. No production data or live provider calls.
- The two-project component test changes the first form with isolation
disabled and a shared query cache. The second form keeps its target.
Storage changes update only the selected project. A saved target remains
selected while the list loads, and an explicit clear remains possible.
- API integration creates two projects from the same default, patches
one, reads both, and checks that agent and instance defaults remain
unchanged. A later instance-default change does not rewrite either saved
project target. Tests cover unavailable/incompatible choices, anonymous
and cross-company denials, and storage preconditions.
- The migration test runs the actual SQL against an existing project row
and verifies its environment and disabled checkout policy survive.
Deletion accounting and clearing affect only matching project
references.
- The final guard set passes **53 tests across four files**:
`project-environments.test.ts`,
`environment-selection-route-guards.test.ts`,
`heartbeat/environment-selection.test.ts`, and
`ProjectProperties.environment.test.tsx`. All **64
environment-run-orchestrator tests** pass, including direct lease
acquisition without an extra caller argument. The broader affected set
passed **109 tests across eight files** before the additional admission
guards. These sets overlap.
- UI typecheck, migration numbering/safety, token gates, module
boundaries, query-error rendering, and diff checks pass. `pnpm verify
--explain` returns exit 2 and requires broad CI because shared
schema/runtime paths and behavior documentation changed. Broad coverage
is delegated to current-head CI.
- Early iterations failed due to a legacy parser that drops the
environment field, a fixture without real company membership, and
incorrect expectations for privacy-preserving 404 denials. The service
now reads legacy references before normalization, the API fixture has
real membership, and denial assertions match the existing privacy
contract. The orchestrator mock also needed the reused company-binding
guard in its service contract. The focused suites above pass after those
corrections.
- Local server typecheck remains blocked by installed ACPX, Slack, AI
SDK, and Runner type mismatches in unchanged dependency-facing code. It
reports no errors in the changed production paths. This is not a full
local typecheck or suite pass. Hosted CI supplies full current-head
coverage.
- SDK source-proof and privacy qualification passes **56 tests across
three files**, including the real built SDK. The maintained source
registry matches the committed revision; runtime pins are unchanged.
- Review reproduced two failures: an incoming policy without a storage
owner could be validated before the existing owner was restored, and
uppercase legacy UUIDs missed migration matching. Preserve the owner
before validation and compare normalized UUID text. Both regressions now
pass. The post-fix project/storage/orchestrator batch passes **107
tests**: `node node_modules/vitest/vitest.mjs run
server/src/__tests__/project-environments.test.ts
server/src/__tests__/project-workspace-provider.integration.test.ts
server/src/__tests__/environment-run-orchestrator.test.ts --silent`.
- The first hosted run failed build, typecheck, and canary at the same
`run-completion.test.ts` call because lease acquisition required a new
argument. The lease method now enforces binding internally. Its existing
caller compiles without a new field, and the orchestrator tests prove
the binding flag reaches the runtime. The full server check no longer
reports that error; the local dependency errors above remain.

## Risks

- Apply the normal database migration before serving the new revision.
The migration adds a nullable foreign key and copies valid legacy
project selections. It does not move repository data or resize provider
storage. The five newest schema snapshots are retained.
- New projects retain their creation target when shared defaults change.
Existing projects without a saved selection continue to inherit
defaults. A null target explicitly opts into inheritance.
- Existing saved project selections now affect placement. Missing,
archived, incompatible, and foreign-company targets fail validation;
they do not silently choose another storage owner. A running admitted
process keeps its original owned lease.
- Local dependency limitations are listed above. No live-provider,
deployed-browser, or production rollout qualification is claimed.

## Model Used

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

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-10 14:31:48 -08:00
DottaandClaude Opus 5.5 353945c86d fix(tool-gateway): let agents create gateway tokens without receiving the value (#20613)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Tool gateways let external MCP clients use a company's tools with a
gateway client token
> - Since delegated connection management (#17737), an agent can create
and revoke gateway client tokens for its responsible person
> - #20581 stopped agents from getting the one-time token value, but it
did this by making token creation person-only
> - The product decision is that agents keep their delegated authority
to create and revoke tokens
> - This pull request restores agent token creation, but the agent never
receives the token value: the person reveals the value once on the
gateway's Tokens page
> - The benefit is that an agent can do the rotation work for the
person, and the secret still never enters chat, run events, logs or tool
output

## Linked Issues or Issue Description

Refs #17737
Refs #20581

**What happened**
After #20581, `POST /api/tool-gateway/gateways/{gatewayId}/tokens`
returns 403 `gateway_token_requires_person` to every agent caller.
Agents lost the delegated authority to create gateway client tokens that
#17737 gave them. Before #20581, an agent that created a token received
the plaintext value in its tool result and could repeat it in chat.

**Expected behavior**
An agent with delegated connection management can create and revoke
gateway client tokens. The agent never receives a token value. The
responsible person gets the value once, on the gateway's Tokens page.
Person callers keep today's behavior.

**Steps to reproduce**
1. Give an agent delegated connection management for a responsible
person with `tools:admin`.
2. As the agent, call `POST
/api/tool-gateway/gateways/{gatewayId}/tokens` (for example through
`call_api`).
3. See 403 `gateway_token_requires_person`.

## What Changed

- Agent create: `POST /api/tool-gateway/gateways/:gatewayId/tokens` from
an agent with delegated authority returns 201 with token metadata only
(`id`, `tokenPrefix`, `expiresAt`, `pendingReveal: true`), plus
`tokensPath`, a `reveal_credential` `requiredUserAction` for the
responsible person, and a `nextStep` instruction. The saved row has no
usable value: its stored hash is of a value that is generated and then
discarded. Gateway authentication also rejects `pendingReveal` rows.
- Person reveal: new person-only `POST
/api/tool-gateway/gateway-tokens/:tokenId/reveal`. It generates the
value, stores its hash, clears `pendingReveal` and returns the value
once. A conditional update makes a second or concurrent reveal return
409. Only the token's responsible person can reveal it (403
`gateway_token_reveal_requires_responsible_person`). Agent callers get
403 `gateway_token_requires_person`. A token revoked before reveal
cannot be revealed.
- Why a reveal that generates the value: no stored plaintext existed to
reveal, and storing one (even encrypted) for later display adds a secret
at rest. Generating the value at reveal time keeps the existing one-time
display and stores only a hash.
- Schema: `tool_mcp_gateway_tokens.pending_reveal boolean not null
default false` (migration `0460_gateway_token_pending_reveal`). Shared
`ToolMcpGatewayToken.pendingReveal`, and `ConnectionRequiredUserAction`
gets `reveal_credential` / `gateway_token`.
- UI: the Tokens page shows a "Waiting for you to reveal" section for
pending tokens with a "Reveal token" button. The value appears in the
existing "New token — copy now" one-time block. Pending tokens show the
status "Not revealed".
- Agent gateway create: the 201 for agent callers also includes
`gatewayPath`, `tokensPath` and a `nextStep` that tells the agent to
link the saved gateway.
- The agent create-token `nextStep` and the catalog say not to revoke
the old token in the same turn: its client keeps working on it until the
person installs the new value.
- The reveal update and its `tool_gateway.token_revealed` activity entry
commit in one transaction. The Tokens page offers Reveal only for tokens
that act for the signed-in person.
- API reference: new `docs/api/tool-gateway-tokens.md` (create, reveal,
revoke) in the docs nav.
- Revoke stays allowed for agents with delegated authority (unchanged
route).
- API catalog: create-gateway, create-token, reveal and revoke
descriptions now say who acts, that agents never receive values, the
page paths, and that the old token is revoked only after the client
works with the new one.
- Agent access map: create-token policy note and a new reveal entry.
- Evals `gateways-profiles.create-gateway` and
`gateways-profiles.rotate-token`:
- Remove the keyword grader (`human|operator|admin|manage`) and the
handoff text checks. Grade saved state, rendered links and secret
absence. A plain reply or a question card is accepted.
- Remove the setup probes that called the create routes as the agent.
Under delegated authority, those probes created a real gateway
("Forbidden agent gateway"), which failed create-gateway before the
agent ran.
- create-gateway: exactly one gateway for the intended profile and
company, created by the agent (or by the person after a handoff); the
person opens it from a rendered link; attribution matches the creator;
any agent-created token is still pending reveal; no token value in chat,
cards or run events.
- rotate-token: after the agent's turn, both setup tokens still work and
any replacement the agent created is pending the person's reveal; the
person follows the rendered Tokens page link and reveals (or issues) the
value in a private browser; the new value works before the old token is
revoked; the unrelated client keeps access; no token value appears
anywhere, including a regex scan for any full `pcgw_<id>.<secret>`
value.
- The changed scenario context and outcomes are recorded in
`chat-full-catalog-testops-manifest.json` `changedFields` and pinned in
the manifest test. The inventory digest, descriptors (`oracleVersion`
v2, `sourceFiles`) and SDK public input pins are updated. The unused
`external-gateway-handoff.ts` module is removed.
- Fixture realism: the create-gateway profile now allows a real read
tool (an empty profile made the agent ask which apps to add instead of
finishing), and the rotation tokens get a neutral owner note instead of
"Disposable rotation fixture" (the agent read that note as an eval
hint).
- Shared private-browser helpers (`external-private-browser.ts`,
`external-private-browser-worker.ts`) get one optional `pendingTokenId`
input and one `revealedPending` receipt field.

## Verification

- Red: on `master` route code, the new integration tests `lets agents
create and revoke gateway client tokens without ever receiving a token
value` and `reveals an agent-created gateway token value only to its
responsible person` fail (403 `gateway_token_requires_person`).
- Green: `cd server && npx vitest run
src/__tests__/connection-management.integration.test.ts -t gateway` (25
passed). The test checks that the agent `call_api` result has no `token`
field and no token-shaped value, that the agent cannot reveal, that the
person reveals once (second reveal 409) and the value authenticates,
that agent revoke works and stops the value, that a token revoked before
reveal never gets a value, and that person create still returns the
value.
- `npx vitest run src/__tests__/tool-gateway.test.ts
src/__tests__/openapi-routes.test.ts
src/services/native-runtime/runner-api.test.ts` (2247 passed).
- `cd ui && npx vitest run
src/pages/apps/gateways/panels/TokensPanel.test.tsx
src/pages/apps/gateways/gateway-helpers.test.ts` (16 passed); `pnpm
check:token-gates` clean.
- Eval graders and catalog: `pnpm exec vitest run --config
tests/runner-e2e/vitest.config.ts` on the gateway oracle tests,
`external.test.ts`, catalog consistency/contract, TestOps
export/retained, phase/clock callers and SDK input/type tests (all
pass); `node --test` on the TestOps manifest, adapter, contract,
streaming and reconcile tests (all pass); `node
tests/runner-e2e/source-sdk-public-inputs.mjs <HEAD> --check`
(unchanged).
- `node scripts/check-maps.mjs` passes. `tsc --noEmit` for `server`,
`ui` and `tests/runner-e2e`: no errors in changed files.
- Paid end-to-end run of both eval cases, run
[42872921890](https://github.com/paperclipai/paperclip/actions/runs/42872921890)
on 39f38a7f6c (before the last three commits): both FAILED, for
agent-behavior reasons the later commits address. Not rerun yet.
- rotate-token: the agent created the replacement (pending reveal, no
value), then revoked the old token in the same turn, despite the create
response saying to wait. Its reasoning cited the eval-looking fixture
note. The grader rejected the premature revocation. The reply linked the
Tokens page correctly.
- create-gateway: the agent created the gateway and one pending token,
then asked a question card about which apps to add to the empty profile,
with no link to the gateway. The grader rejected the missing link.
- Token values: a scan of all evidence files of both cells found no full
`pcgw_<id>.<secret>` value and no `pcgw_<uuid>` form. Agent `call_api`
payloads in run events are redacted; the integration test proves the
agent tool result has no value.

## Risks

- Behavior change for agents: token create returns metadata and a Tokens
page path, not a value. Agents that expect `token` in the response must
send the person to the Tokens page.
- Migration adds one boolean column with a default. Existing tokens get
`false` and keep working.
- A pending token that is never revealed stays as an unusable row until
someone revokes it. It cannot authenticate.

## 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-10 15:13:34 -06:00
DottaandClaude Opus 5.5 2e6850e745 perf(task-chat): open tasks from a preloaded cache and cold tasks in one request (#20592)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - People open tasks from task lists, the Inbox and the sidebar many
times each hour
> - A task's chat shows its first frame only after about seven separate
reads finish
> - #20355 added one batched `chat-bootstrap` read that returns all of
these reads. #20450 used it only for the sidebar's Recents
> - The hosted database is CPU-bound, so the UI must open tasks faster
and must also send fewer requests
> - This pull request uses the bootstrap for task lists, for open intent
on any task link, for cold opens and for a hard load of a task URL. It
shares one coordination module between all of these paths
> - The benefit is that a task opens from cache more often, a cold open
sends one chat request instead of about seven, and the first content
paint comes sooner

## Linked Issues or Issue Description

Refs #20355 (server read that this pull request consumes). Refs #20450
(Recents preload, now on the shared path).

**Is your feature request related to a problem?**
A click on a task in a task list waited for about seven separate chat
reads. The default task list (Coordinator) had no preload at all. A
press or a resting hover on a task link sent the same seven reads, and
the page then sent a second set. A hard load of a task URL started its
chat reads only after the JavaScript bundle and the company list loaded.
Sidebar project task links had no open-intent prefetch.

**Describe the solution you'd like**
Use the single `chat-bootstrap` read everywhere a task is about to open.
Let the page's gating reads share a bootstrap that is in flight instead
of sending their own requests. Keep the core issue read separate so the
header does not wait for the larger read.

**Describe alternatives you've considered**
Preload 30 rows per list in three requests (the first prototype). Each
task costs the server about fifty queries, and a resting hover warms any
other row before the click, so this change preloads 10 rows in one
request.

## What Changed

- `ui/src/lib/issue-chat-bootstrap.ts`: one module decides when to send
a bootstrap and lets page reads share it (`readFromIssueChatBootstrap`).
Seeding uses the existing `issueChatBootstrapCache` (replace-style reads
are refreshed when newer, merge-style reads are filled only when empty,
other companies are ignored). It skips tasks that are loading, loaded in
the last 30 seconds, or already fresh in cache, under the id or the
identifier. It never aborts a request in flight, so a click that
unmounts the list still uses the answer. Each request is bound to the
account that sent it; a request sent before the first session read is
bound to the first account the session reports, and a sign-out revokes
it. A page read shares a bootstrap only while its key holds no data (a
refresh after a write always reads the server), and waits at most 5
seconds for it. Every ref of a task (id and identifier) is tracked at
request start. Entries expire after 30 seconds.
- Task lists preload the chat of their top 10 rows in one request, after
the list's own reads settle and the browser is idle
(`usePreloadTaskChats`, `when-page-settles.ts`): the Coordinator task
list (`TaskList` with `preloadCompanyId`), the streamlined and legacy
issues lists and the streamlined Inbox. Recents now uses the same path,
so a task in both places loads once. A list that stays open preloads
each task at most once in five minutes: live events (#20563's lanes)
keep a preloaded task's cache current or mark it stale, and a stale key
is read from the server, never from an older bootstrap. All seeded keys
are per-task keys on the fast live lane, not the ambient lane.
- Open intent (press, click, resting hover or focus) on a task with no
chat history in cache warms it with one bootstrap request instead of six
separate reads (`prefetchIssueDetailForNavigation`). A task seen before
still refreshes its cached reads one by one.
- A cold open starts one bootstrap. The gating reads (recent comments,
activity, runs, input facts, interactions, attachments, live runs,
active run) wait for it instead of sending their own requests. The core
issue read stays independent.
- A hard load of a task URL starts `GET /api/issues/:ref/chat-bootstrap`
from the inline preload script, in parallel with the JavaScript bundle.
The page adopts that answer.
- Sidebar project task links (Coordinator sidebar) get the same hover,
press and focus warming as Recents and `IssueLinkQuicklook`.
- Tests: unit tests for the coordination module, Recents, `TaskList`
preload, sidebar warming and the cold-intent path. A new layout journey
checks that a task the Inbox preloaded reveals once at its latest
message without reading its chat again. Journeys that hold one separate
read now also hold or fail the bootstrap. The Inbox reveal journey held
a read that the chat no longer sends on open (it was flaky on master
too), so it now holds the recent comment window.
- The perf harness gains `PAPERCLIP_ISSUE_PERF_LIST_DWELL_MS` and the
README documents it.

## Verification

Measurement setup: production UI builds of master (`e01fe7f84a` and the
earlier base `8d2bf78323`) and of this branch, served by one local
server with `PAPERCLIP_UI_DEV_MIDDLEWARE=false` and embedded PostgreSQL.
Only `index.html` changed between variants (both asset sets were
present), so the server, database and seeded task were the same. Four
rounds of the issue-detail harness, with master and branch alternated in
each round: 20 pairs per S1 cell, 40 per S2 cell. The host was shared
with other agents (load average 16 to 43), so the spread is large.

Content paint (the task is readable), median paired delta, branch minus
master:

| Scenario | Profile | Pairs | Content paint | Branch faster in | API
requests before content (master / branch) |
|---|---|---:|---:|---:|---:|
| S1 preloaded click (dwell 1500 ms) | unthrottled | 20 | -45 ms | 15/20
| 25 / 18 |
| S1 preloaded click (dwell 1500 ms) | Fast 4G, 4x CPU | 20 | -23 ms |
13/20 | 26 / 18 |
| S1 click, no dwell | unthrottled | 20 | -51 ms | 15/20 | 25 / 18 |
| S1 click, no dwell | Fast 4G, 4x CPU | 20 | -21 ms | 12/20 | 26 / 18 |
| S2 cold deep link | unthrottled | 40 | -61 ms | 28/40 | 54 / 40 |
| S2 cold deep link | Fast 4G, 4x CPU | 40 | -129 ms | 29/40 | 47 / 39 |

Medians (p25 to p75), content paint in ms: S1 dwell unthrottled 295
(283-315) → 248 (217-276). S1 dwell Fast 4G 1133 (1114-1226) → 1093
(853-1176). S1 no dwell unthrottled 295 (277-328) → 260 (233-332). S1 no
dwell Fast 4G 984 (842-1196) → 923 (816-1110). S2 unthrottled 1002
(677-1235) → 896 (619-1280). S2 Fast 4G 4643 (4155-5061) → 4516
(4298-4894).

Header paint is neutral to slightly later in two cells: S1 dwell
unthrottled +39 ms (3/20 faster) and S2 Fast 4G +56 ms (17/40). In those
cells the header now paints in the same frame as the content. In the
first two rounds, the median gap from header to content fell from
105-157 ms to 29-74 ms unthrottled and from 557 ms to 298 ms for S2 on
Fast 4G. Other header cells are -27 to +1 ms.

The local server answers each read in a few milliseconds, so the network
wait that the preload removes is small here. The request count is the
stable result: a cold open sends one chat request instead of about
seven.

List page: the preload request starts after the first task row is in the
DOM (64 ms after it unthrottled, 200-280 ms after it on Fast 4G, page
clock). Time to the first visible row, 30-task list: unthrottled master
1863 ms vs branch 1563 ms (6 pairs, branch faster in 5). Fast 4G master
3626 ms vs branch 3727 ms (14 pairs, branch faster in 6, mixed signs).
The preload starts after the row paints, so it cannot delay it; the Fast
4G difference is noise. One 10-task bootstrap took 78-162 ms on the
server.

Checks run:
- `pnpm exec vitest run` for the bootstrap, cache, prefetch, Recents,
sidebar, quicklook, `TaskList`, issues list, Inbox, issue detail and
Coordinator tests: 18 files, 646 tests pass.
- `pnpm exec tsc -p ui/tsconfig.json --noEmit`: clean, except missing
generated Storybook catalog files that this change does not touch.
- `pnpm check:token-gates`: clean.
- `tests/e2e/task-loading-frame.spec.ts`,
`disconnect-resilience.spec.ts` and `connection-reviews.spec.ts`
(production build): 21 passed, 1 skipped.
- `tests/perf/issue-detail/layout-stability.spec.ts` and
`plan-startup.spec.ts` against the production build: all pass. The Inbox
reveal journey and the new preloaded journey passed 4 of 4 repeats each.
- `pnpm verify` asks for broader CI coverage because an e2e spec
changed.

## Risks

- More server work for list views: one bootstrap of up to 10 tasks per
list visit (about fifty queries per task), skipped for tasks already
loaded in the last 30 seconds. It runs only after the page's own reads
settle and the browser is idle, and only while the tab is visible.
- A resting hover on a never-opened task now sends one bootstrap instead
of about six reads. The server work per task is about the same; the
request count is lower.
- If a bootstrap fails, each page read falls back to its own request.
The fallback is slower than before for that one open (the reads start
when the page mounts, not at the press).
- The header can paint a few tens of milliseconds later in the same
frame as the content when everything is cached.
- An answer that arrives after a sign-out or account change is dropped.
An answer for another company is ignored.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 14:26:27 -06:00
DottaandClaude Opus 5.5 28063474d2 fix(ui): show why a queued message edit was not saved (#20583)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - In task chat, a person can edit a queued message before the agent
receives it
> - The server rejects an edit with 409 when the queue changed in
another session
> - The composer kept the text, but it showed no reason, so the edit
looked stuck
> - This pull request shows the reason under the editor
> - The benefit is that the person knows the edit was not saved and what
to do next

## Linked Issues or Issue Description

**What happened**
Two sessions edited the same queued message. The older save got `409
queued_comment_revision_conflict`. The composer still said "Editing
queued message" and showed no error.

**Expected behavior**
The composer tells the person that the queue changed in another session,
keeps the text, and lets them save again.

**Steps to reproduce**
1. Queue a message on a task with a running agent.
2. Open the task in two tabs and start editing the queued message in
both.
3. Save in the second tab, then save in the first tab.
4. See no error in the first tab.

## What Changed

- `TaskChatComposer` shows a save failure for a queued message edit in
its existing `role="alert"` line
(`data-testid="task-chat-queued-edit-error"`). The error clears when the
edit ends or a new save starts.
- `api/errors.ts` has copy for `queued_comment_revision_conflict`,
`queued_comment_stale_queue`, `queued_comment_stale_target` and
`queued_comment_not_pending`.
- The existing queue refresh after a 409 updates the revision, so a
second save works.

## Verification

- Red then green: `cd ui && npx vitest run
src/components/task-chat/TaskChatComposer.test.tsx` (new test `shows why
a concurrent queue change rejected the save and keeps the edit` fails on
`master`; all 146 pass with the change).
- `npx vitest run src/components/TaskChatThread.test.tsx
src/api/errors.test.ts` (479 passed).
- `pnpm check:token-gates`: all gates clean.
- `tsc --noEmit -p ui`: no errors outside unrelated Storybook files.

## Risks

- Low risk. UI copy only, no new API. A stale edit now shows a short
error line in addition to the "Queued message changed" label.

## 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-10 13:37:31 -06:00
DottaandPaperclip 89ec4e7614 feat(links): add cached GitHub pull request previews (#20593)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Task conversations contain links to pull requests and other work.
> - A URL alone cannot tell the operator whether a PR is open, merged,
or blocked by conflicts.
> - Fetching every link at render time wastes API calls and can expose
private data through a shared cache.
> - This pull request adds a demand-driven links service and a GitHub PR
adapter.
> - Each viewer and company has a separate cache. Repeated links share
one request and result.
> - Operators get context when they hover or focus a link. Normal links
stay useful when a provider is unavailable.

## Linked Issues or Issue Description

**Subsystem affected**

Task Markdown links, shared link metadata, and the server GitHub API
boundary.

**Problem or motivation**

PR links have compact labels and neutral icons. They do not provide a
rich hover card or a common status source across repeated mentions. A
background resolver cannot borrow a person's GitHub connection because
it has no viewer identity.

**Proposed solution**

Recognize supported links locally. Request metadata after a delayed
hover or keyboard focus. Show a card at the first line of the link,
aligned with its icon. Share the result across all mentions in the
current viewer and company. Keep the exact original navigation URL.

The first adapter supports GitHub pull requests. Open is green, merged
is purple, and merge conflicts are orange. Draft, closed, unknown,
loading and unavailable states remain quiet.

**Alternatives considered**

Eager page scans would spend API quota on links the operator does not
inspect. Company-wide caching of personal GitHub results would cross the
credential boundary. This change uses neither approach.

**Roadmap alignment**

ROADMAP.md was checked. This extends existing task links and the GitHub
resolver. It adds no deployment adapter or new background worker.
Searches of open and closed PRs for link previews and PR hover cards
found no matching implementation.

## What Changed

- Add host-owned link recognition and a normalized preview contract. The
service selects an adapter by provider.
- Add a board-only company read endpoint. It checks company access
before cache reads and sends a private, no-store response.
- Use the current viewer's eligible GitHub connection. A configured
connection that cannot supply credentials fails closed. With no managed
connection, reuse existing company credentials or public access.
Instance credentials retain the public-repository check.
- Reuse the GitHub external-object resolver for API transport and
response parsing. Add title and mergeability to its normalized snapshot
without changing its existing state contract.
- Coalesce requests by company, viewer and canonical PR URL. Bound
cached entries and concurrent work. Cache success for one minute and
failures for a short period or the provider retry window.
- Add an abortable request deadline. Timed-out work stays in the
concurrency budget until it settles. No periodic worker or idle polling
is added.
- Add a shared browser query and anchored preview card. Repeated
Markdown links observe the same result. There are no automatic retries,
window-focus refreshes, toasts or connection prompts.
- Add ten production-component Storybook examples and real
pointer/keyboard browser checks. Document the API and verification
route.

## Verification

- Linux source checkout: 233 focused tests passed in nine suites. They
cover recognition, fixed API destinations, provider states, access
failures, credential selection, viewer/company isolation, request
coalescing, expiry, cache bounds, concurrency bounds and mounted OpenAPI
coverage.
- A live public GitHub smoke returned this PR's correct title and open
state. Two equivalent URLs shared one real network request. Personal
connection access was not part of that smoke.
- A controlled HTTP server stalls a real response body. The test proves
cancellation while another lookup completes.
- UI typecheck and the server layered typecheck passed. The UI
production build passed.
- Token gates and query policy gates passed. All 30 map/catalog support
checks passed. The three new server test files also passed a focused
TypeScript check.
- All 27 focused browser checks passed on the reviewed source tree. They
cover light/dark themes, narrow layouts,
open/merged/conflict/draft/closed/loading/unavailable/failed states,
fixed positioning during pointer movement, exact navigation and keyboard
access. A real browser HTTP interception counts one deferred lookup for
two equivalent PR links.
- The first browser attempt lacked Chromium. A later combined build/test
attempt exhausted the sandbox's 8 GiB memory limit and killed Storybook.
Those attempts supply no pass evidence. Chromium is installed, and the
final browser checks run with the build stopped. One initial Storybook
dependency reload interrupted a streaming check. All eight existing
inline-link cases passed on the warmed server, followed by a complete
27-case pass.
- Synthetic Storybook pointer events did not exercise the native hover
boundary. The assertions now run through real Playwright pointer and
keyboard actions. The fixture stories retain the production components.
- Review fixes preserve the Draft label when GitHub reports both draft
and conflicts. They also preserve neutral link colors in dark mode. The
25 affected service checks pass, including the added combined-state
regression. Browser checks compare unknown, draft and closed links with
the muted theme color.
- The first CI policy run rejected a literal retry opt-out. The code now
uses the existing explicit zero-retry helper. A new test-spy type error
was also corrected. These failures are fixed; their earlier runs remain
failed.
- `pnpm verify --explain` returned exit 2 because the browser test path
selects broad CI. No full local suite is claimed. The final CI run
supplies that coverage.
- The previous head `437913c797963aa113bb82e2ae8b836969317ef9` passed CI
and received a 5/5 Greptile score. All four review threads are resolved.
Master then advanced. The current head
`ae2a1c2adbd61a621cca9bdd160561952613df6e` resolves the source-manifest
conflict against master `9b21dcbdda01296349f3cfe0a3a6891e79df6130`.
Fresh CI and review are running.
- The previous CI run found a lost class on decorated PR links and a
stale SDK source manifest. Both are fixed. The existing CommentThread
integration test and 26 focused link service tests pass. All 45 SDK
source/privacy tests pass. All eight source-manifest helper tests pass.
The manifest helper changed only source identities and added the two new
shared files. Runtime, compiler, parser and library pins stay unchanged.
- A later review found that expired timeouts could reuse a settled
promise. The service now removes settled requests from its
shared-request map and counts unfinished provider work separately. The
new regression failed before the fix and passes after it. The
concurrency-bound and real stalled-HTTP tests also pass.
- UI typecheck and token gates pass after these fixes. The browser rerun
needed Chromium in the new session. That first attempt did not exercise
the UI. The complete 27-case browser rerun passed before the master
merge with retries disabled. All 27 browser cases also passed on the
merge revision with retries disabled.

- After the master merge, all 84 focused link, OpenAPI and integration
tests pass. All 55 SDK source/privacy/manifest tests pass. UI typecheck
and token gates pass again. The source manifest retains master's
reviewed runtime pins and includes both sets of source changes. GitHub
confirms the PR can merge cleanly. The preflight helper rebuilt the
shared package and plugin SDK; Cargo is unavailable locally. Final CI
supplies Rust coverage.

- The latest master update changed the reviewed report-lock policy but
left the SDK lock hash at its previous value. The SDK source/evidence
checks reproduced the mismatch. This change sets only the SDK lock hash
to the authenticated output of the existing master policy and patch.
Compiler, parser and Zod pins stay unchanged. All 56 SDK
source/evidence/manifest tests now pass. All 84 focused
link/API/integration tests, UI typecheck and token gates also pass on
the updated source.

## Risks

- This cache is process-local. Separate server replicas can make
separate GitHub requests.
- A cached result can remain visible for its short lifetime after GitHub
changes. A later hover refreshes expired data.
- GitHub can report unknown mergeability. The UI shows conflicts only
when GitHub explicitly reports them.
- Personal preview data stays in viewer/company caches. It is not
written to company external-object records or persisted browser query
storage.
- Stories simulate the API. They do not prove a live GitHub login or
private-repository access.
- No schema migration is required.

## Model Used

OpenAI Codex, based on GPT-6. The runtime does not expose the exact
serving model ID or context-window limit. Used reasoning, repository
inspection, source editing, shell execution and browser tools. No
subagents were used.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-10 19:32:51 +00:00
DottaandClaude Opus 5.5 9b21dcbdda refactor: remove SPEC docs and repoint spec citations (#20565)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Contributors and agents read the docs under `doc/` before they
change code
> - `doc/SPEC.md` and `doc/SPEC-implementation.md` were out of date.
`doc/SPEC-implementation.md` became an append-only log: in 60 days it
got about 1,645 new lines and lost 181, across 140 commits
> - #20557 changed `AGENTS.md` so the code and its tests are the source
of truth, and the specs are only background
> - This pull request removes the two spec docs. It moves only the
sections that live code comments cite into focused docs, and it repoints
all other references
> - The benefit is one less stale document that agents read and append
to

## Linked Issues or Issue Description

Refs #20557 (merged). This PR is the follow-up that removes the docs. It
is based on `master`, which already contains #20557.

**Issue type**
Outdated content. The docs are out of date and conflict with the code.

**Where is the issue?**
`doc/SPEC.md`, `doc/SPEC-implementation.md`, and the code comments and
docs that cite them.

**What's wrong?**
`doc/SPEC.md` and `doc/SPEC-implementation.md` are out of date, but
agents still read them and append a paragraph with each fix.

**Suggested fix**
Delete both files. Move the sections that code cites into focused docs.
Repoint the references.

## What Changed

- Deleted `doc/SPEC.md` and `doc/SPEC-implementation.md`.
- Moved these spec sections, because live code comments cite them:
- §9.3.1 "Shared default-open issue writes" → new section in
`doc/agent-permission-defaults.md`.
- §9.8.1 "Issue-thread interaction resolver contract" →
`doc/execution-semantics.md`, "Issue-thread interaction resolution".
That section already had most of the contract. I added only the missing
parts: the `supersedeOnUserComment` default, the canonical policy list,
the addressee and cap rules, the rule that surfaces show the server's
effective audience, and the cross-issue budget charge. The short policy
summary at the top of the section now points to the list, so the
definitions occur one time only.
- §9.9 "Task Watchdog Authority Contract" → new "Authority contract"
section in `doc/TASK-WATCHDOG.md`. It holds the allowed and disallowed
mutation lists and the deferred capabilities. The atomic recovery batch,
restoration verification, and interaction rules were already in
`doc/execution-semantics.md` §11, so the new section links there and
does not repeat them. I did not move the "Downstream acceptance
criteria" checklist or the sentence about the CTO and Security sign-off.
These were implementation-time notes; the tests now hold that coverage.
- §9.10 "Company Skill Policy Contract" → new
`doc/company-skill-policy.md`. No focused doc had this contract. I did
not move the "Required regression tests" phase checklist.
- Did not move any other part of the specs.
- Repointed code comments that cited spec sections:
- `packages/shared/src/issue-write-denial.ts` (§9.3) →
`doc/agent-permission-defaults.md`
- `server/src/routes/issues.ts` (§9.9) → `doc/TASK-WATCHDOG.md`,
"Authority contract"
- `server/src/routes/issues.ts` (§9.3, §9.8.1 cross-issue budget) →
`doc/agent-permission-defaults.md`
- `ui/src/lib/interaction-audience.ts` and
`ui/src/components/InteractionGovernancePanel.tsx` (§9.8.1) →
`doc/execution-semantics.md`
- `ui/src/lib/skill-policy-denial.ts`, `ui/src/pages/CompanySkills.tsx`,
`ui/src/pages/CompanySkills.production.tsx`,
`ui/src/pages/SkillStudio.tsx` (§9.10) → `doc/company-skill-policy.md`
- `server/src/services/recovery/ownership-invariant-report.ts` (monitor
deadline wait path) → `doc/execution-semantics.md` §8. That section
already lists the one-shot monitor as a valid wait path.
- Resynced the git blob pin for
`packages/shared/src/issue-write-denial.ts` in
`tests/runner-e2e/source-sdk-public-inputs.json`. The comment edit
changed the file's hash, and the Runner support suite checks that pin. I
used `node tests/runner-e2e/source-sdk-public-inputs.mjs <HEAD>
--write`; only that one pin changed.
- Updated `AGENTS.md` §1 so it does not name the deleted files.
- Removed the links to the deleted files from
`doc/plugins/PLUGIN_SPEC.md`.
- Updated the `doc-maintenance` agent skill
(`.agents/skills/doc-maintenance/`) so it no longer audits or stages
`doc/SPEC.md`.
- Removed or replaced references in `maps/README.md`,
`maps/feature-map/README.md`, `maps/storybook-map/README.md`,
`doc/PRODUCT.md`, `doc/CLIPHUB.md`, `doc/DEPLOYMENT-MODES.md`,
`doc/DEVELOPING.md`, `doc/evals.md`, `doc/spec/agent-runs.md`,
`doc/AGENTCOMPANIES_SPEC_INVENTORY.md`,
`doc/connections/CONNECTOR-PLAYBOOK.md`,
`doc/plugins/ideas-from-opencode.md`, `doc/execution-semantics.md`,
`doc/TASK-WATCHDOG.md`, and `.devin/wiki.json`.
- In `doc/execution-semantics.md`, the "admission is independent of
cleanup and accounting" reference now states the rule inline: only a
hard stop, an explicit pause, or an inactive company refuses admission.
- Kept these references as they are: dated plans in `doc/plans/` and
`docs/plans/`, the dated log
`doc/logs/2026-05-24-cli-api-parity-e2e-log.md`, `releases/`, the
transcript fixture `ui/src/fixtures/runTranscriptFixtures.ts` (it uses
the filename only as sample text), and
`docs/specs/external-task-protocol.md` (it refers to the Symphony
project's own `SPEC.md`).

## Verification

- `git grep -n "SPEC-implementation\|SPEC\.md"` (without `releases/`,
and ignoring `PLUGIN_SPEC.md` citations) finds only the kept references
listed above.
- `grep -rn "§9\.\(3\|8\|9\|10\)" ui/src server/src packages/shared/src`
finds no results.
- `node scripts/check-maps.mjs`: "Maps aligned: 35 areas, 192 capability
IDs".
- `pnpm check:token-gates`: "All gates clean".
- `pnpm exec vitest run --config tests/runner-e2e/vitest.config.ts
tests/runner-e2e/source-sdk-public-type.test.ts
tests/runner-e2e/source-sdk-evidence.test.ts`: 45 passed. These failed
in CI before the pin resync.
- `pnpm verify`: exit 2 ("Broader verification required") because
`AGENTS.md` is a global CI input. Required CI supplies the broad
coverage for this revision.
- All code changes are comments only. No runtime behavior changes. The
title uses `refactor:` because comments in source files changed.

## Risks

- Low risk. The change removes docs and edits comments only.
- A reader can lose context from spec sections that I did not move. The
text stays in git history. The code and its tests are the source of
truth.
- External links to the two deleted files will return 404.

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

## Model Used

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

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass (focused checks above; CI
supplies broad coverage)
- [x] I have added or updated tests where applicable (not applicable:
docs and comments only)
- [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-10 13:07:59 -06:00
DottaandClaude Opus 5.5 ed3ffc3bc7 fix(ui): let chat review requests open the document they ask about (#20590)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - In chat, an agent can end its turn on a confirmation card that asks
the person to review a saved document
> - The full task view links the card's document, but the compact chat
card showed the document key as plain text unless it was the plan
> - A person who is asked to review a draft in chat could not open the
draft from the card
> - This pull request links a document on the same chat to its
`#document-<key>` panel, the same link a reply uses
> - The benefit is that a review request in chat lets the person read
what they are asked to approve

## Linked Issues or Issue Description

**What happened?**
In a chat eval run on master `f4337bb5fb`
(`questions-and-approvals.reject-and-revise`), the agent saved a `draft`
document and ended its turn on a confirmation card "Review research
notes introduction draft" with `target: { type: "issue_document", key:
"draft", revisionId }`. In the chat composer takeover, the card showed
`draft` as a plain chip with no link.

**Expected behavior**
A confirmation card in chat opens the document it asks the person to
review, like the full card in the task view does.

**Steps to reproduce**
Create a pending `request_confirmation` on a chat task with
`payload.target = { type: "issue_document", key: "draft", revisionId }`.
Open the chat. The takeover shows the `draft` chip without a link.

## What Changed

- `CompactTarget` in `TaskChatCompactInteractionCard.tsx` gives an
`issue_document` target on the same chat the href `#document-<key>`.
Plan targets, targets with an explicit `href`, attachments and documents
on other tasks keep their current behavior.
- Added a `TaskChatThread` test: a pending draft review request in the
takeover links `#document-draft`.

## Verification

- `npx vitest run src/components/TaskChatThread.test.tsx -t "links a
review request"` in `ui`: red before the fix, green after.
- `npx vitest run src/components/TaskChatThread.test.tsx
src/components/coordinator/CoordinatorTaskCommitment.test.tsx
src/lib/issue-thread-interactions.test.ts`: 457 passed.
- `node scripts/check-token-gates.mjs`: all gates clean.
- Paid chat eval on a temporary branch with this change: see the comment
on this PR.

## Risks

- Low risk. Plan review cards are unchanged; their plan preview already
opens the plan.
- No API, schema or stored state changes.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), agentic coding with
tool use 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-10 12:28:37 -06:00
Nicky LeachandPaperclip 98ce93dd4e Choose project storage from compatible environments (#20587)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Projects supply repository files to agent runs through core staging
or a storage plugin.
> - A storage plugin's default claim silently attached new projects even
when their agents used a different environment.
> - An environment name or driver type does not establish shared storage
access. The existing binding and task-driver contracts do.
> - This pull request groups creation choices by that contract and
selects storage from the creator's effective environment.
> - The benefit is a visible, validated choice and a clear error before
an incompatible managed workspace operation.

## Linked Issues or Issue Description

Fixes #20459.

Related: #20156 covers a separate provider-failure diagnostic. The PR
search found no other fix for #20459. ROADMAP.md has no duplicate
project-creation change.

## What Changed

- Show **Available in** when configured environments require different
project storage. Each option names the environments that can use the
same storage owner. Hide the selector when all choices are compatible.
- Derive compatibility from repository staging and managed binding
ownership. Local, SSH, and sandbox environments use core staging.
Managed storage requires a registered task driver from the same
installed storage plugin. Separate storage plugins remain incompatible.
Unknown plugin contracts and the fake sandbox are excluded.
- Follow the creating agent's environment, or the person's primary
agent, then the instance default and local fallback. Apply managed-only
and forced Kubernetes policy. The product has no separate user or
company environment-default field today.
- Add a company-authorized discovery endpoint and an optional creation
`environmentId`. Validate the choice again on save. Store the storage
owner without overriding agent run environments. Preserve explicit API
storage choices and existing project ownership.
- Keep the hidden repository-free private-task project independent of
environment availability. Submit the observed storage owner as
`expectedStorageProviderPluginId` so a changed contract fails at save
without setting a workspace policy. Instance task-isolation defaults
stay inherited.
- Reject an incompatible managed workspace before a provider operation.
Name the storage and selected environment in the error. Add regression
tests, API documentation, creation-story fixtures, and minimal
corrections to obsolete map text.

## Verification

- **102 tests passed across 7 files:** project environment integration,
installed storage policy, repository persistence, project environment
routes, project creation telemetry routes, OpenAPI, and
NewProjectDialog. Run with `node node_modules/vitest/vitest.mjs run
server/src/__tests__/project-environments.test.ts
server/src/__tests__/project-workspace-provider.integration.test.ts
server/src/__tests__/project-repositories-persistence.test.ts
server/src/__tests__/project-routes-env.test.ts
server/src/__tests__/project-goal-telemetry-routes.test.ts
server/src/__tests__/openapi-routes.test.ts
ui/src/components/NewProjectDialog.test.tsx --silent`.
- The review fixes cover both archived-default private-task cases and
changed-storage rejection. With both instance isolation flags enabled, a
new core repository project keeps a null policy and resolves to an
isolated Git worktree. The complete affected set passes all 102 tests.
- The CI query-error policy failure was fixed by using the shared query
view. The form and shared query-view suites pass **24 tests**, including
cached choices during a transient refetch failure. `node
scripts/check-query-error-rendering.mjs --enforce --summary` reports
zero findings.
- CI exposed stale SDK source hashes and heartbeat fixtures that paired
plugin storage with Local. Refreshed exactly four shared-source hashes
with the maintained generator (runtime pins unchanged). All **53 SDK
privacy/source-proof tests**, including real built SDK output, pass. All
**7 corrected managed-storage heartbeat scenarios** pass; the fixtures
declare the owning plugin's task environment and substitute transport at
the existing execution seam.
- Shared and UI TypeScript checks pass. The UI production build passes.
`pnpm check:token-gates`, `node scripts/check-maps.mjs`, `node
scripts/check-module-boundaries.mjs`, and diff checks pass.
- Tested UI states: compatible choices, incompatible groups, default
within a group, manual override, create failure, loading, discovery
failure and retry, and an unavailable default. Tests use the production
form and existing design tokens.
- The integration tests use real isolated PostgreSQL on macOS arm64 with
Node 26.12.0. They cover user/agent defaults, storage persistence,
policy restrictions, company and authentication denials, and rejection
before a provider call. Discovery uses saved configuration and makes no
worker or provider requests.
- Local verification limits: `pnpm -r typecheck` and `pnpm build` stop
in unchanged adapter-utils code because the installed ACPX types lack
`inheritProcessEnv`. A direct server check also finds an unchanged AI
SDK type-version mismatch. The existing heartbeat test `inherits the
instance default environment across companies while preserving explicit
agent overrides` times out at line 518 on both this change and
unmodified master with the same dependencies. The full `pnpm test:run`
attempt passed all six chat shards (1,948 tests), then the Git workspace
expiration suite reported 14 failures. I stopped that broad run. Two
sampled failures reproduce on unchanged master with the same
`unknown_worktree_git_files` refusal; the other expiration failures were
not independently compared. A later broad workspace-preparation run also
hit workspace validation failures and was stopped; the seven corrected
fixture scenarios pass independently. These limits do not establish a
full local suite pass. Hosted CI remains required.
- Initial missing-package and loopback-denial failures were resolved by
repairing isolated dependency links, building the SDK, and running
database tests with loopback access. No deployed or live-provider
execution is claimed.

## Risks

- Creation defaults now follow the effective environment. A plugin's
`defaultForProjects` claim alone no longer selects storage. This is the
intended behavior change.
- Compatibility covers repository project storage. It does not guarantee
arbitrary host paths, retained-workspace moves, adapter/image support,
credentials, or worker readiness. Matching provider configs cannot prove
a cross-plugin storage contract.
- A person's primary agent supplies the personal default because there
is no separate personal environment setting. An unavailable configured
default requires a valid explicit choice or a configuration repair.
- No migration runs. Existing projects keep their storage owner. This
change does not detach existing managed data or provide a storage
migration.
- Full local verification has the limits listed above. CI and standard
Greptile review are pending.

## Model Used

OpenAI Codex, based on GPT-6, with reasoning, tool use, and code
execution. The exact serving snapshot and context-window size were 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-10 17:38:31 +00:00
DottaandClaude Opus 5.5 d82e5efeb9 perf(ui): cut repeated reads while a task is open (#20563)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The board UI keeps an open task current with a shared company
websocket and with polling fallbacks
> - In a busy company, one open task sent 35 to 60 API requests in 17
seconds, and each request waits on the database
> - Most of these requests were repeated reads: task summaries 22 times,
the task run list 11 times, task browsers 6 times, and the 60 KB agent
list 6 times
> - The cause is not the open task. Other agents' run and task events
invalidate company-wide lists on every 300 ms batch, and the task view
polls runs and browsers on fixed intervals even while the websocket is
open
> - This pull request spaces company-wide refreshes, and it lets the
websocket replace run and browser polling while it is connected
> - The benefit is about 79% fewer reads from an open task in a busy
company, with the same polling fallback when the connection is down

## Linked Issues or Issue Description

Refs #17608
Refs #9627

**What happened?**

An open task in a busy company sent the same reads again and again. In
one 17-second visit, the UI requested `issues/summaries` 22 times, the
task live runs 11 times, the task browsers 6 times, and the company
agent list 6 times. The company live runs and the task's child lists
(`parentId`, `createdFromIssueId`, `descendantOf`) were requested 2 to 3
times each.

**Expected behavior**

A busy company must not make an open task refetch company-wide lists on
every event batch. While the live connection is open, the UI must use
events, not fixed fast intervals, for task runs and browsers. The UI
must keep its polling fallback when the connection is closed.

**Steps to reproduce**

1. Open a task in a company where other agents run work.
2. Keep the task open for 17 seconds.
3. Count the API requests in the browser network panel.

**Paperclip version or commit**

Based on master at bd30ef8510.

## What Changed

- `ui/src/lib/query-invalidation-batcher.ts`: Add a minimum spacing
option to the invalidation batcher. A quiet lane flushes after the
normal batch interval. A busy lane flushes at most once per spacing
window. Add `createLaneRoutingBatcher`, which sends each invalidation to
one of two batchers by query key.
- `ui/src/context/LiveUpdatesProvider.tsx`: Route company-wide keys to a
5-second ambient lane. These keys are task lists and summaries, the
agent list, heartbeats, company live runs, sidebar badges, the
dashboard, costs and the company activity feed. All other keys, such as
task detail, comments, task runs and browsers, keep the 300 ms lane.
- `ui/src/lib/live-runs-cache.ts`: While the company connection is open,
a successful active run list or active run polls every 15 seconds
instead of every 1 or 3 seconds. Run lifecycle events already refresh
these queries, and progress events patch them.
- `ui/src/hooks/useTaskBrowsers.ts`: While the company connection is
open, stop browser polling when no browser can change state. Keep the
3-second poll for a starting, running, idle or stopping browser, and
when the connection is closed. The two callers pass the task's company.
Socket open and reconnect already reconcile this active query.
- `ui/src/lib/task-browsers-refresh.ts` and `LiveUpdatesProvider.tsx`:
On `browser.*` activity for a task, refresh that task's browser list. If
a read is in flight, one shared follow-up read runs after it settles, so
an older empty result cannot stop polling. Two views of the same task
share that follow-up. A list that no view observes becomes stale, so a
returning view reads it. When the connection opens or resyncs, each
browser list with a read in flight also gets one follow-up read, because
missed activity is not replayed.
- `doc/DATABASE.md`: Document the ambient lane and the new polling
contract.
- Tests: Add a fan-out test that replays a busy-company event stream for
17 seconds against an open task, plus unit tests for spacing, lane
routing, key classification, the run interval and the browser interval.
Update the recovery test for the connected active-run interval.

## Verification

Request count over a fixed 17-second open window
(`ui/src/context/LiveUpdatesProvider.fanout.test.ts`). The test mounts
the reads that an open, running task keeps active. It replays a busy
company: one run status event every second and one task activity event
every 0.7 seconds, none for the open task. Fake timers make each run
deterministic, so the spread is zero. The baseline used the same test
with master's batcher, run interval and browser interval.

| Read | Before | After |
| --- | ---: | ---: |
| Task summaries (sidebar) | 24 | 5 |
| Company agent list | 17 | 5 |
| Company live runs | 17 | 5 |
| Child list: parent | 24 | 5 |
| Child list: created from | 24 | 5 |
| Child list: descendants | 24 | 5 |
| Task live runs | 18 | 2 |
| Task browsers | 6 | 1 |
| **Total** | **154** | **33** |

The baseline numbers match the staging capture well (summaries 22,
browsers 6).

- `pnpm exec vitest run src/context/LiveUpdatesProvider*.test.*
src/lib/live-runs-cache.test.ts
src/lib/query-invalidation-batcher.test.ts
src/hooks/useTaskBrowsers.test.ts src/lib/task-browsers-refresh.test.ts
src/hooks/useTaskPluginPanels.test.tsx
src/components/task-side-panel/TaskSidePanel.test.tsx
src/pages/IssueDetail.test.tsx` (in `ui/`): all pass.
- `pnpm exec tsc --noEmit -p ui`: no errors in `ui/src`.
- `pnpm check:token-gates`: clean.
- Tests also prove these cases:
- With the connection closed, task runs still poll every second and
browsers every 3 seconds.
- With a quiet company, one event refreshes the summaries and child
lists after one normal batch.
- `browser.started` on the open task refreshes its browsers. The same
event on another task does not.
- A browser that starts during a slow empty read appears after that read
settles (`ui/src/lib/task-browsers-refresh.test.ts`). This test fails
when the refresh joins the older read.
- Two views and repeated events during one read cause one follow-up
read. This test fails without the shared queue.
- A list cached while the task was closed is read again when a view
returns.
- A connection that opens during an older empty browser read causes one
more read, so a browser that started while the socket was closed
appears. This test fails without the follow-up.
- Task detail, comments, task runs, browsers, attention and another
company's keys stay on the fast lane.
  - An unrecognized active result keeps the fallback interval.

This is a local request-count proof. It does not include a timed browser
journey on a deployed instance.

## Risks

- Company-wide lists refresh at most once every 5 seconds from live
events, so they can be up to 5 seconds late. After 5 quiet seconds, an
event refreshes them in about 300 ms. Own mutations invalidate their
queries directly, so they do not wait.
- An active task run refreshes from events. Fields without an event,
such as output silence, can be up to 15 seconds late while the
connection is open. Reconnect still reconciles missed events.
- A browser that appears without activity is not shown until the next
activity or reconnect. Every browser lifecycle step logs activity on the
task, so this case is not expected.
- No server, schema or 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
- [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-10 11:01:53 -06:00
DottaandPaperclip dff8bed007 fix(ui): show chat activity immediately during admission (#20559)
## Thinking Path

> - Paperclip helps people manage AI agents and their work.
> - Chat uses a shared runner turn to show activity after Send.
> - The turn currently shows Queued while the server admits the request.
> - People need immediate activity feedback during this short wait.
> - This change uses the existing Working header and Thinking activity
during ordinary admission.
> - Specific server holds stay visible so people can see setup or
maintenance delays.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

The activity indicator in task chat and agent chat after Send.

**Current behavior**

Ordinary admission shows Queued in the header and activity row. It
changes to Working and Thinking when execution starts.

**Proposed behavior**

Show Working and Thinking from message submission through ordinary
queued admission. Keep the same avatar, name, and activity row in place.
Preserve specific server waits and the existing queue controls.

**Subsystem affected**

ui/ — React board UI.

## What Changed

- Use the default activity presentation when a queued turn has no
specific server hold.
- Retain maintenance, agent setup, server restart, previous-run,
provider, and preparation labels.
- Omit the elapsed-time suffix until a start time is known. This
prevents a frozen Working for 0s header before a run exists.
- Update component tests for submission, acknowledgement, reassignment,
and runtime discovery on desktop and mobile.
- Update the activity guidance in DESIGN.md.
- Keep this behavior in the shared runner turn. No API, queue, or
execution contract changes are needed.

## Verification

- Chat component suites: 582 passed, 1 existing skip.
- Command: `pnpm exec vitest run
ui/src/components/task-chat/TaskChatRunnerTurn.test.tsx
ui/src/components/TaskChatThread.test.tsx
ui/src/components/IssueChatThread.test.tsx`.
- `pnpm check:token-gates`: passed.
- `pnpm verify`: exit 2. DESIGN.md is a global CI input, so the selector
requires broader CI coverage. The three focused chat suites passed again
on final commit `e85b8e43b007288f3ef41e0a42e801d5d2c41e54`. Required CI
passed on this revision. See the CI result below.
- The earlier CI attempt on `796fac80a06769d67af5b899fb186a875bfb228d`
failed the unchanged Rust test
`process_lineage::journal::tests::reaping_stops_a_dead_runners_provider_and_its_escaped_tool_job`
with `unattributed_orphan`. This UI change does not alter its compiled
inputs. The cause is not yet established. The current revision passed
the Rust check. The earlier failure cause remains unknown. [Failed
job](https://github.com/paperclipai/paperclip/actions/runs/42843806417/job/129926132490).
- The workspace preflight installed dependencies and built shared and
plugin SDK outputs. Its unrelated Runner build could not start because
cargo is unavailable. The current revision passed CI typecheck and
build.

- Both current-head reviews are 5/5 with no remaining findings. There
are no unresolved review threads.
- Current-head typecheck, build, general tests, serialized server tests,
all eight E2E shards, and Rust checks passed. [CI
run](https://github.com/paperclipai/paperclip/actions/runs/42845383745).
- The Runner support suite reported one 30-second timeout in
`private-reporter-artifacts.test.ts`; 11,291 other tests passed. Both
tests in that file passed locally in isolation (1.64 seconds for the
full command). The one rerun passed in CI attempt 2. The timeout cause
remains unknown. [Failed
job](https://github.com/paperclipai/paperclip/actions/runs/42845383745/job/129932351496).

- Final CI result: attempt 2 passed on
`e85b8e43b007288f3ef41e0a42e801d5d2c41e54`. Typecheck, build, tests,
Rust checks, and all eight E2E shards passed. [CI attempt
2](https://github.com/paperclipai/paperclip/actions/runs/42845383745/attempts/2).

## Risks

- Low risk. This changes presentation only. A queued turn can show
Thinking before provider execution starts.
- Specific server holds remain visible. Scheduling, permissions, and
saved message controls do not change.

## Model Used

- OpenAI GPT-6 through Codex, with reasoning, tool use, and code
execution. The runtime does not expose a more specific deployment 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-10 10:04:52 -06:00
DottaandPaperclip ce1ee9e89f fix: stop prompts from encouraging subtasks (#20362)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Agents read runtime prompts, tool help, skills, and role
instructions.
> - Several of these texts tell agents to create child tasks as a normal
next step.
> - One accepted-plan prompt even forbids work on the current task.
> - Task parentage is useful, but it does not need a prompt that
encourages task creation.
> - This pull request removes those prompts and keeps the task APIs
available.

## Linked Issues or Issue Description

Refs #10642. This change adjusts prompt wording; it does not add
delegation-cycle guards.

**Current behavior**
Prompts tell agents to create children after plan approval, use children
for long work, and create a task just to pass context. Onboarding
requires a child even for a single request.

**Proposed behavior**
Continue work on the current task. Keep explicit assignments, optional
task creation, parentage, dependencies, and approval gates. Use the
separate-task recipes when the user requests that structure or when
continuing existing work.

**Reason and benefit**
Reduce needless task trees without removing subtask support. Keep the
patch limited to wording, existing test expectations, and generated
catalog data.

## What Changed

- Align accepted-plan prompts and remove direct child-creation advice
from runtime and error text.
- Make task tool descriptions neutral. Keep their fields, defaults, and
handlers.
- Update the main skill, onboarding, CEO instructions, company
templates, specialist workflows, and matching guides.
- Keep historical release snapshots intact. Refresh the explicitly
seeded schema fixture's catalog digest; the server still verifies
historical release snapshot hashes.
- Refresh the affected generated catalogs, capability inventory,
semantic contracts, and fixture manifest.

## Verification

- Final tested head: `c6e304d0c105cdbfbe0b2ab0a233307fcd91a04b`.
- All 66 check runs are complete: 60 pass, one is neutral, and five are
skipped by CI conditions. Snyk also passes. No checks fail or remain
pending.
- [PR
CI](https://github.com/paperclipai/paperclip/actions/runs/42846226554)
passes typecheck, build, server/general tests, all eight E2E shards,
Runner support, Runner Rust, and the aggregate verify gate.
[Windows](https://github.com/paperclipai/paperclip/actions/runs/42846226546)
and [Docker
Runner](https://github.com/paperclipai/paperclip/actions/runs/42846226108)
also pass.
-
[Greptile](https://github.com/paperclipai/paperclip/pull/20362#issuecomment-6751419835)
and [Paperclip
review](https://github.com/paperclipai/paperclip/pull/20362#issuecomment-6751454010)
give this exact head 5/5. All six inline threads are resolved. Neither
current summary has findings.
- Merged master `bf0220d34a156c81853755c3d1365ddc527037de`. Resolved
three generated-file conflicts with the canonical generators.
- Local verification passes: 473 focused prompt, continuation, UI,
catalog, runtime and native-execution tests; Runner TypeScript build;
generated metadata; UI token gates; and diff checks.
- The prior head `c22018cb0f` failed three SDK evidence tests because
two source hashes were stale. Reproduced all three failures locally. The
existing generator refreshed only the hashes for the denial-copy file
and its test. All 53 affected evidence/source-pin tests then passed,
including private-value rejection. No scanner logic, assertions, or
runtime dependency pins changed. Current CI confirms the support suite
passes.
- A prior commit failed the Rust process reaping test. Its cause remains
unproven. No Rust code or assertions changed. Later Rust runs pass; this
does not establish the cause of the earlier failure.
- Local preflight built shared/plugin SDK packages, then stopped because
`cargo` is absent. Current-head CI supplies Rust and full build
coverage. `pnpm verify --explain` selects broad required CI; full local
suites were not repeated.

## Risks

- Wording changes guide the model; they do not enforce a ban on
subtasks.
- Existing installed skill versions and saved custom instructions need
their normal refresh process. This PR does not migrate company
configuration.
- Existing approved task assignments and approval gates remain in
effect.

## Model Used

- OpenAI GPT-6, through Codex, with reasoning, tool use, and code
execution. The exact routed model ID and context-window size were 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-10 09:58:54 -06:00
Dotta 38cc6b2a74 fix(ui): restore pinned sidebar links above More (#20369)
Restore the mixed pinned navigation list after Inbox and before More, retaining saved order, agent pins, drag controls, and unpin actions.

Verified with focused tests, UI checks, browser interaction checks, and passing PR CI. Greptile: 5/5.
2026-11-10 09:18:37 -06:00
DottaandPaperclip cac82ed4d9 fix(ui): show parent tasks in responsive breadcrumbs (#20366)
## Thinking Path

> - Paperclip helps people manage AI agents and their work.
> - Tasks can have a parent task.
> - The task header does not show that parent in its navigation path.
> - On a phone, the header removes the task path and shows only the
current title.
> - Long parent and current titles must share the available space.
> - This pull request adds a parent link and limits each displayed task
title to 48 characters.
> - People can move to the parent and keep the current task number in
view.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

The navigation path in a task header.

**Current behavior**

The global path omits the parent. The phone header shows only the
current task. Long parent titles cannot share the row safely.

**Proposed behavior**

Show the immediate parent before the current task. Limit each displayed
title to 48 Unicode code points, with an ellipsis. Limit the parent to
40% of the row. On phones, remove the collection link and parent number
so both titles and the current task number fit.

**Reason and benefit**

People can see the task context and open its parent. Full titles stay in
hover text and accessible names. Private parents have no link.

Related: #9302 adds project navigation. This change adds task parent
navigation.

## What Changed

- Add the immediate parent from the existing authorized ancestor
response to the global task path.
- Make parent and current titles shrink. Apply a 48-character display
limit without changing stored titles.
- Keep the current task number visible on phones. Keep private parent
labels noninteractive.
- Add ten Storybook scenarios with the production header. Cover linked
parents, long titles, phone widths, tablet width, dark theme, standalone
tasks, Mine source, and private parents on desktop and phones.
- Add regression tests and document the layout rules in DESIGN.md.
- Keep the source collection and filters when people follow the parent
link. Cover route state, stored state and legacy query links.
- Use the existing noninteractive lock chip for private parents. Hide
its parent number on phones. Refresh its control key when an open task
changes screen size. Both regressions failed before their fixes and pass
after them.
- Refresh the SDK public source registry after the master update. The
existing helper keeps all runtime and dependency pins unchanged. This
repairs three support tests that failed on stale master source
identities.

## Verification

- Focused UI tests: `pnpm exec vitest run
ui/src/components/BreadcrumbBar.test.tsx
ui/src/pages/IssueDetail.test.tsx` — 280 passed.
- UI typecheck: `pnpm --filter @paperclipai/ui typecheck` — passed.
- `pnpm build-storybook` — passed.
- `pnpm check:token-gates` — passed.
- Catalog checks and 30 map/catalog tests — passed.
- Chromium: all ten scenarios rendered with no page errors. Story play
functions finished. Checked 1440, 768, 390 and 320 pixel widths,
keyboard parent navigation, private parent links, title limits and
horizontal overflow.
- Private-parent browser check: resize an open task from 1440 to 320
pixels and back. The chip changes from the parent number to Private and
back. It remains noninteractive.
- Screenshots were captured in light and dark themes. They are task
evidence and are not committed.
- This verifies the header with fixture data and the page wiring with
mocked APIs. It does not claim live backend authorization or a pixel
baseline comparison. The static server returns 404 for optional shell
context reads. After keyboard navigation, destination task reads also
return 404 because the preview has no task backend. The header renders
and all play checks pass.
- Case discovery and OpenAPI tests: 47 passed.
- Current head: `e7a1ada02e75e9b2cbd5f6076ad6c113954f3a4b`. [CI run
42834603680](https://github.com/paperclipai/paperclip/actions/runs/42834603680)
passed. All 61 head check runs are complete: 58 passed and three
conditional jobs were skipped. The legacy Snyk status passed.
[Greptile](https://github.com/paperclipai/paperclip/pull/20366#issuecomment-6751938924)
reviewed this head at 5/5 with no actionable findings. All four inline
review threads are resolved.
- After the master update: all 280 UI tests, UI typecheck, token gates,
Storybook build, 30 catalog tests and ten Chromium stories pass. All six
screenshots match the saved images byte for byte.
- SDK and process support checks: 105 tests pass. The source pin helper
has eight passing tests. The three SDK failures reproduced before the
registry refresh and passed after it. The helper check reports no
remaining source drift and no runtime pin changes.
- The earlier case discovery failure reproduced before the summary
correction and passed after it. All server shards passed on the next
head. That CI run failed one unchanged Rust process-reaping test: its
owned tool stopped, but two unattributed orphan processes blocked
verification. The earlier CI run passed the same Rust inputs. Cross-test
process interference is possible; the historical log does not prove the
cause. The next CI run passed Rust. That run failed three SDK checks on
stale master source pins and one process inspection check. The source
pin repair is in this head. The process check passes locally, but the
earlier process inspection failure remains unexplained.
- The same prior run failed the cohort migration resume test. Its upload
barrier releases after five seconds; maximum uploads were seven and
three in the two attempts, below eight. Three server files also exceeded
the 180-second file budget. These test inputs come from master. A local
attempt could not reproduce that CI assertion because its child Git
command failed in this execution environment. Fresh CI 42834603680
passed these paths, including every server shard and Runner support. No
test or process safety check was weakened.

## Risks

- Breadcrumb titles display less text. Hover text and accessible names
retain the full title.
- The global path shows the immediate parent. People can follow parent
links through a deeper task tree.
- The shared breadcrumb component also serves other pages. Existing
header and agent-switcher tests pass.
- No schema, API behavior or stored title change.
- SDK source identities must match the integrated source. The registry
change uses the existing helper and preserves all runtime and dependency
pins.

## Model Used

- OpenAI Codex, based on GPT-6. The exact runtime model ID and context
window size are not exposed. Used reasoning, repository tools and
Chromium verification.

## 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-10 08:54:39 -06:00
f4337bb5fb test(evals): land the chat catalog eval harness on master (#19614)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The chat-feature evals measure whether agents complete real chat
requests through the product
> - The chat catalog eval harness and its repairs live on the
`chat-eval/master-candidate` branch, not on master
> - Today's work made 139 of 242 chat evals pass in verified single-case
live runs. Each product fix landed on master as its own pull request
> - The eval-side changes (setups, graders, fault seams, evidence
capture) still exist only on the candidate
> - This pull request lands the harness on master. Master stays the
source of truth for product behavior
> - The benefit is one tree: evals run against master commits, and
product fixes reach the evals as soon as they merge

## Linked Issues or Issue Description

Refs #19441, Refs #19599, Refs #20270, Refs #20334, Refs #20342, Refs
#20344, Refs #20354, Refs #20360, Refs #20373, Refs #20382, Refs #20409

**What existing behavior does this improve?**
The chat catalog eval harness and the product it evaluates live on a
long-running branch outside master. Eval fixes do not reach master, so
master-based eval runs use old setups and graders.

**Subsystem affected**
`tests/runner-e2e` chat catalog harness, plus a small set of product
changes that exist only on the candidate.

**Proposed change**
Land the harness on master. Every product fix from this eval work
already merged to master as its own pull request. Conflicts keep
master's product behavior.

## What Changed

- Most of the diff (808 of 851 files) is under `tests/runner-e2e/`:
chat-catalog domains, scenarios, descriptors, the TestOps manifest,
private evidence tooling, and their tests.
- Graders now check saved state, setup literals and rendered or clicked
links. Many keyword and regex checks on free-form replies are removed.
Each changed grader has a bumped `oracleVersion` and calibration tests
(a valid reply, a reworded valid reply, and broken variants that must
fail).
- The shared link helper `hasLink` no longer treats a list link (for
example `/COM/issues?view=mine`) as a link to every task without a URL
key.
- Setup fixes build real state through supported product routes: routine
history, agent grants through the board grant route, budget holds with a
real cap, and a storage-layer archive fault for
`inbox-search.archive-partial-failure`.
- Browser boundary journeys (sign-in, chat-disabled, onboarding) now
write the standard pass evidence files.
- Retargeted cases, approved by the maintainer:
`agent-chat.chat-disabled` (Conference Room gate),
`agents.restore-instructions` (no saved instruction history),
`access.manage-membership` (member permission edits stay board-only).
- The private evidence trusted files (`manual-private-handoff.ts`,
`manual-private-retention.ts`, `manual-private-report-assets.ts`) are
byte-identical to master.
- Product files outside `tests/` (43 files, +1,328 −49) carry
candidate-only changes that master does not yet have. Most are tests.
The main code changes are in `request-human-input.ts`,
`heartbeat-run-summary.ts`, `native-completion-feedback.ts`,
`native-run-finalizer.ts`, `openapi.ts`, `secrets.ts` and
`IssueChatLongThreadPerf.tsx`.

## Verification

- `pnpm vitest run chat-catalog-domains chat-catalog-contract.test.ts
chat-catalog-consistency.test.ts unit-launcher.test.mjs
--testTimeout=180000` in `tests/runner-e2e`: 269 files pass, 3,550 tests
pass.
- `node --test chat-full-catalog-testops-manifest.test.mjs`: 10 pass.
- `node tests/runner-e2e/source-sdk-public-inputs.mjs <HEAD> --check`:
no change.
- `tsc` for `server` and `tests/runner-e2e`: clean.
- Live single-case verification: 139 of 242 cases passed on paid runs,
and a person read the saved evidence for each. These are single-case
runs, not one full-cohort run.

## Risks

- Large diff. Most of it is test-only, but the 43 non-test files change
product code paths. Review those files first.
- Some graders are now less strict on reply wording. A person must read
replies for meaning on those cases.
- 42 cases need disposable outside accounts (Notion, Slack, GitHub,
Daytona and others) and stay blocked. `access.invite-teammate` stays
failing until local agent isolation lands, because the runner's durable
state keeps a live invite token.
- The paid workflow compares the trusted private-evidence files with
master. They are unchanged, so this risk is low.

## Model Used

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

## 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>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-10 08:02:11 -06:00
DottaandClaude Opus 5.5 7329eaf353 perf(ui): open sidebar Recents tasks from cache (#20450)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The board UI sidebar has a Recents list of the tasks a person opened
last
> - The Recents list renders from a local snapshot, but its links had no
warm-up. A click waited for the task-detail code and about nine
first-frame reads
> - Task lists (Inbox, Tasks) already warm a task on hover, press and
click, and #20355 added a batched `chat-bootstrap` read that had no UI
consumer
> - The hosted database is CPU-bound, so a fix must add at most one read
per page load and must not compete with the page's own reads
> - This pull request gives Recents the same open-intent prefetch as
task lists, warms the task-detail code, and preloads the top ten recent
tasks with one `chat-bootstrap` request after the page is idle
> - The benefit is that a click on a recent task shows its chat from
cache, without a wait for the API

## Linked Issues or Issue Description

Refs #20355 (server read this PR consumes). Builds on #20389, #20398 and
#20419.

**Is your feature request related to a problem?**
Opening a task from the sidebar's Recents was slower than opening it
from a task list. The Recents links had no prefetch. The task page
waited for its first-frame reads after the click.

**Describe the solution you'd like**
Recents links use the task lists' open-intent prefetch. When the page is
idle, one batched request loads the first frame of the most recent tasks
into the task page's caches.

**Describe alternatives you've considered**
Per-task prefetch of every recent task. That makes up to ten times nine
requests per page load, which the hosted database cannot afford.

**Additional context**
I searched open PRs and remote branches for "chat-bootstrap" and
"instant task opens". The only related work is the server-side branches
that merged as #20355. There is no other in-flight UI consumer of
`chat-bootstrap`.

## What Changed

- `ui/src/hooks/useIssueNavigationPrefetch.ts`: the open-intent handlers
from `IssueLinkQuicklook` move into one hook (light warm on pass-over;
`history: true` on a resting hover or focus, a mouse or pen press, and a
click). `IssueLinkQuicklook` now uses the hook. Its behavior does not
change.
- `ui/src/components/RecentTaskNavItem.tsx`: one Recents link wrapper
that applies the hook. The classic list (`SidebarRecentTasks`), the
Coordinator task rows and `DesignSidebar` use it. `SidebarNavItem`
accepts the handlers through a new optional `linkHandlers` prop.
- Classic Recents links now open `/issues/<identifier>` (Coordinator
already did). The old UUID link remounted the task page after a redirect
to the identifier route.
- `ui/src/hooks/useRecentTaskOpenWarmup.ts`: when Recents shows a task,
it fires the task-detail code warm-up (`useWarmTaskDetailCode`). After
two frames, a visible tab, no fetching queries and an idle callback, it
sends one `issues/chat-bootstrap?ids=` request for the ten most recent
tasks. It skips tasks whose first frame is cached and fresh. It sends no
request for an empty list or a hidden tab. It sends at most one request
per account per page load. Unmount or an account change aborts the
request.
- `ui/src/lib/issueChatBootstrapCache.ts`: `seedIssueChatBootstrap`
writes each field under the keys the task page reads, for the requested
ref, the id and the identifier: core issue, recent comments, comment
input facts (one batch, through the new `seedIssueIdBatch`), recent
activity, the recent run window (through the new shared
`buildRecentIssueRunWindow`), interactions, attachments, live runs and
active run. An explicit `null` active run is stored. A missing field is
not stored. It does not replace data that is as new or newer, data with
a read in flight, or data with a pending invalidation. Keys whose page
merges observed history (comments, activity, runs, input facts) are
filled only when empty. Items for another company are ignored.
- The seed fills the `?view=core` key, not the full detail key.
`seedIssueDetailCache` is not used: the core payload cannot stand in for
the detail read's related data, and seeding it there would suppress that
read.
- `issuesApi.chatBootstrap` client method.
- `tests/perf/collections/recent-task-open.spec.ts` and its README
section.

## Verification

Local disposable instance (`local_trusted`, production UI build,
embedded PostgreSQL, collection fixture plus 12 comments on each of 8
recent tasks, streamlined UI with the Coordinator sidebar). Journey:
open Projects with 8 Recents, wait for network idle plus 3 s, click a
recent task, measure until its latest comment is on screen. Each run
uses a fresh browser context (empty HTTP cache and storage apart from
Recents). Median of 5 runs, same server and fixture for both arms:

| Arm | Click to first chat frame | Idle page API requests |
| --- | --- | --- |
| `master` (b6f4008f9b), loopback | 227 ms (first run: 249 ms) | 24
(range 21–25) |
| This PR, loopback | 134 ms | 22 (range 21–27) |
| `master`, +150 ms on each API response | 455 ms | 19 |
| This PR, +150 ms on each API response | 135 ms | 20 (+1
`chat-bootstrap`) |

With added latency, the first frame no longer depends on API latency.

- `PAPERCLIP_COLLECTION_PERF_BASE_URL=http://127.0.0.1:<port> pnpm exec
playwright test --config tests/perf/collections/playwright.config.ts
recent-task-open.spec.ts recent-task-summaries.spec.ts`: 5 passed. On
`master` the two assertion cases fail (0 `chat-bootstrap` requests; 10
first-frame reads after the click).
- Reads that still follow a click on a preloaded task: full detail (`GET
/issues/:id`), `work-products`, `documents`, `feedback-votes`,
`tree-control/state`, `tree-holds`, `runner-goal`, `browsers`, and the
`read` mark. The `chat-bootstrap` payload does not cover them. None
gates the chat's first frame (the frame shows at 135 ms while every API
response waits 150 ms).
- Unit tests: `RecentTaskNavItem.test.tsx` (intent levels, href),
`useRecentTaskOpenWarmup.test.tsx` (one request for the top ten, code
warm-up, wait for page reads, hidden tab, fresh entries skipped, once
per account, user isolation, abort on unmount),
`issueChatBootstrapCache.test.ts` (each key in the endpoint shape,
`null` versus missing fields, company isolation, no downgrade of newer,
in-flight or invalidated data, merge keys filled only when empty).
- `ui` Vitest: `src/components src/hooks src/lib src/api` 694 files
passed; `src/pages src/App src/context` 188 files passed. `pnpm
check:token-gates`: clean. `pnpm verify` exits 2 because a `tests/` file
is a global CI input; required CI supplies the broad coverage.

## Risks

- Low to medium. One extra read per page load when Recents has tasks
without a fresh cached first frame. The read starts only after the
page's own queries settle, and the server reads at most two tasks at a
time.
- A live event that arrives while the preload is in flight, for a task
with no cached keys, can leave seeded data up to 30 s old until the next
live event or the next open after the stale time. Inbox hover prefetch
has the same window.
- Classic Recents links now use the identifier route instead of the UUID
route. The task page already redirected UUID links to this route.
- `tests/perf/collections` is a manual suite. CI does not run it.

## Model Used

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

## Checklist

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

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runner agents run on a provider (here Codex) with a model the
user saves on the agent
> - When the provider rejects that model, Paperclip should classify the
run as `native_provider_model_rejected`, ask the user to choose a model,
and let Retry finish the task
> - Paperclip recognized only the ChatGPT-account rejection (a 400 JSON
envelope). With API-key auth, Codex returns a different terminal error
for an unknown model
> - So the run failed as a generic `adapter_failed` ("Native session
failed"), and recovery opened a reconciliation incident ("Recovery
needed") for a turn that never did any work
> - This pull request recognizes the real API-key shape, bound to the
configured model, and names the rejected model on the task
> - The benefit is that the task asks for a model choice instead of a
reconciliation, and Retry completes the task after the user picks a
valid model

## Linked Issues or Issue Description

**What happened?**
The restart-chaos config fault CF3 `model_id` saves a Codex agent with
the model `chaos-missing-model-<nonce>`. The run stays "running" for
about 60 seconds, then fails with `errorCode: adapter_failed`, "Native
session failed". The task shows "Recovery needed", cause
`native_continuation_requires_reconciliation`. The grader fails as
`CF3/model_id:actionable_message` (waves `w-20261110T061914-s206-cf` t01
and `w-20261110T053831-s205-cf` t02).

Root cause: the wave runs Codex with an OpenAI API key. I drove the
bundled `codex app-server` (0.160.0) directly with an unknown model:
- With API-key auth, Codex retries the Responses API 404 ten times
(about 59 s), then fails the turn with `{"message":"unexpected status
404 Not Found: The model \`<id>\` does not exist or you do not have
access to it., url: https://api.openai.com/v1/responses, cf-ray: …,
request id:
req_…","codexErrorInfo":{"httpConnectionFailed":{"httpStatusCode":404}}}`.
- With ChatGPT auth, the turn fails in about 3 s with `message` =
`{"type":"error","status":400,"error":{"type":"invalid_request_error","message":"The
'<id>' model is not supported when using Codex with a ChatGPT
account.","code":"model_not_found"}}`. Paperclip already classified this
one.

`createNativeProviderFailureObservation` parsed only the JSON envelope,
and the runner's provider-failure category for an HTTP 404 is `unknown`.
The terminal therefore stayed unclassified.

**Expected behavior**
The run is `native_provider_model_rejected`, the task shows `userAction:
"choose_model"` and names the model, no reconciliation incident opens,
and Retry completes the task after the user picks a valid model.

**Steps to reproduce**
`node cli/node_modules/tsx/dist/cli.mjs
tests/restart-chaos/config-wave.ts --faults CF3 --adapters codex
--references model_id --tasks 1 --concurrency 1 --restarts K2 --seed
206` on `master` (with `OPENAI_API_KEY` set).

**Paperclip version or commit**
`master` at 5ab98b0.

## What Changed

- `server/src/services/native-runtime/native-provider-failure.ts`: the
Codex observation recognizes the API-key terminal shape
(`codexErrorInfo` `httpConnectionFailed` or `responseStreamDisconnected`
with `httpStatusCode: 404`, and the message prefix for the configured
model only). It classifies it as `native_provider_model_rejected` with a
new closed diagnostic `NATIVE_MODEL_NOT_FOUND_DIAGNOSTIC` (`codex`,
`model_not_found`, `404`, `api_key`). No provider text or model name is
stored in the diagnostic. A recoverable turn, another model's 404, or
another status is not classified.
- `readNativeModelRejectionDiagnostic` accepts both closed diagnostics,
so run failure diagnostics show it.
- `TaskInterruptionPresentation.rejectedModel` (shared type): with
`userAction: "choose_model"`, the task query carries the model id from
the run's native execution input. Only a
`native_provider_model_rejected` run discloses it.
- `TaskChatThread` names the model in the "Model unavailable" marker:
`The provider rejected the model "<id>".`
- The restart-chaos config wave reads `interruption.rejectedModel` as
task-level text. For `choose_model`, its repair step uses the Retry that
the "Model unavailable" marker offers (`POST /agents/{id}/wakeup` with
`reason: "retry_failed_run"`, the same request as
`agentsApi.retryFailedRun`), because a model rejection does not offer
the task-level Retry.

## Verification

- Red before, green after (the classification line disabled makes both
fail):
- `native-provider-failure.test.ts` — "classifies the API-key 404 for
the configured model as a model rejection (codex 0.160.0 shape)", with
the exact terminal error captured from `codex app-server` 0.160.0, plus
negative cases.
- `native-provider-failure.integration.test.ts` — new `api-key` scenario
through `executePaperclipNativeSession`, the persisted evidence reader
and `finalizeNativeRun`: the run ends `native_provider_model_rejected`,
the issue is `blocked` with that decision, and no issue recovery action
is created.
- `task-interruption-query.test.ts` — the real task query carries
`rejectedModel` only for a model rejection. `TaskChatThread.test.tsx` —
the marker names the model.
- `pnpm --filter @paperclipai/shared typecheck`, `pnpm --filter
@paperclipai/server typecheck`, `pnpm --filter @paperclipai/ui
typecheck` pass.
- Live proof: config wave `w-20261110T090801-s206-cf` (CF3 `model_id`,
codex with an OpenAI API key, K2, seed 206) on commit 05e8938: **1/1
pass**. The run ends `native_provider_model_rejected`; the task rests as
`waiting_for_user` / `choose_model` with `rejectedModel:
"chaos-missing-model-472e1851"` and execution cause
`native_provider_model_rejected` (no reconciliation incident), rests
again after the K2 restart, and finishes 27 s after the model is
restored and the marker's Retry is used. On `master` the same wave
failed as `actionable_message` with "Recovery needed" /
`native_continuation_requires_reconciliation`
(`w-20261110T061914-s206-cf`).

## Risks

- Low risk. The match is bound to the configured model, a 404 status and
a non-recoverable turn, so other provider failures keep their current
behavior.
- The match depends on the Codex terminal text. If a later Codex release
changes it, the run falls back to the old generic failure, not to a
wrong classification.
- Claude (ACPX) unknown models are not changed here. Claude Code reports
an unknown model as a synthetic assistant message ("There's an issue
with the selected model …") rather than a provider error; this PR does
not change or live-verify that path.

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

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code with tool use (shell, file edits, test runs, a direct `codex
app-server` probe).

## 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 <noreply@anthropic.com>
2026-11-10 03:11:10 -06:00
DottaandClaude a14ae9b5c9 fix(workspaces): name a failed workspace setup command on the task page (#20478)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Before an agent starts, Paperclip prepares the task workspace and
runs the project's configured setup commands (for example the workspace
provision command)
> - When that command fails, the run fails as `setup_failed`, and the
task stops with a Retry
> - The task page shows only "Failed" and a generic recovery notice. The
real error (the command, its exit status and output) is visible only on
the run page
> - The user cannot see from the task which setting is broken or what to
change
> - This pull request records a bounded, named cause for a failed setup
command and shows it on the task page
> - The benefit is that the task names the broken setting and what to
change, and classification, retry and recovery stay the same

## Linked Issues or Issue Description

**What happened?**
The restart-chaos config fault wave CF1 sets a project
`workspaceStrategy.provisionCommand` to `bash
./scripts/provision-worktree.sh`, a script that does not exist. The run
fails as `setup_failed` with `Execution workspace provision command
"bash ./scripts/provision-worktree.sh" failed: bash:
./scripts/provision-worktree.sh: No such file or directory`. The task
page shows "Failed" and "Paperclip automatically retried continuation …
still has no live execution path". The grader fails the task as
`CF1/provision_command:message_only_in_run_detail`.

**Expected behavior**
The task page names the failed setup step, the configured command, its
exit status and a short tail of its output, and says what to change.
Retry behavior stays the same.

**Steps to reproduce**
`node cli/node_modules/tsx/dist/cli.mjs
tests/restart-chaos/config-wave.ts --faults CF1 --adapters codex --tasks
2 --concurrency 2 --restarts K1,K2 --seed 204` on `master`.

**Paperclip version or commit**
`master` at 5ab98b0 (wave `w-20261110T043929-s204-cf`, tasks t01 and
t03).

## What Changed

- `WorkspaceCommandExitError` now carries the setup step
(`workspace_provision`, `workspace_seed`,
`workspace_runtime_provision`), the configured command and the command
output.
- New `server/src/services/workspace-setup-command-failure.ts`: it
builds a bounded, redacted `setupCommandFailure` record (step, command
up to 200 characters, exit code, last 300 characters of output). It also
recognizes the older message format, so runs recorded before this change
still get a name.
- `run-completion.ts` adds `setupCommandFailure` to the setup failure
`resultJson`. The error code (`setup_failed`) and all retry and recovery
fields stay the same.
- `TaskInterruptionPresentation.setupFailure` (shared type) is set for
`setup_failed` runs. It goes through the same per-run secret redaction
and user-name redaction as `workspaceFailure`.
- `ExecutionBlockerNotice` shows "Couldn’t set up the task workspace.
<command> failed with exit code N: <output>. Fix the command in the
project’s workspace settings, then retry." A scheduled transient retry
keeps its countdown and only adds the name.
- The restart-chaos config wave reads `interruption.setupFailure` as a
task-level text for its actionable-message check.

## Verification

- Red before, green after (the tests fail with the source changes
reverted):
- `server/src/__tests__/workspace-runtime.test.ts` — "names a failed
project provision command with its exit status and output (CF1)" runs a
real git worktree with a missing provision script (exit 127).
- `server/src/services/task-interruption-presentation.test.ts` — "setup
command failure presentation (CF1)": names the command, keeps
`state`/`canRetry`/`userAction` identical, recognizes the older message,
keeps a scheduled retry, bounds output and strips credentials from the
command.
- `ui/src/components/ExecutionBlockerNotice.test.tsx` and
`tests/restart-chaos/config-faults.test.ts` cover the notice text and
the harness check.
- `pnpm --filter @paperclipai/shared typecheck`, `pnpm --filter
@paperclipai/server typecheck`, `pnpm --filter @paperclipai/ui
typecheck` pass.
- Live proof: config wave `w-20261110T083227-s204-cf` (CF1, codex, 2
tasks, K1 and K2, seed 204) on commit ae9a4b7: **2/2 pass**. Both tasks
show `The project's workspace provision command "bash
./scripts/provision-worktree.sh" failed with exit code 127: bash:
./scripts/provision-worktree.sh: No such file or directory` on the task,
rest again after the restart, and finish after the repair and Retry. On
`master` the same wave failed both tasks as `message_only_in_run_detail`
(`w-20261110T043929-s204-cf`).

## Risks

- Low risk. The change adds a presentation field. It does not change the
error code, retry, recovery or blocking behavior.
- The command and output are user-configured text. They are bounded,
passed through `redactSensitiveText`, the run's secret registry and
user-name redaction. If redaction fails, the field is omitted, like
`workspaceFailure`.

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

## Model Used

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

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] 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 <noreply@anthropic.com>
2026-11-10 02:35:18 -06:00
DottaandClaude 1e13c58a4e fix(heartbeat): name the cause of generic adapter failures and keep SQL text off the board (#20483)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Heartbeat run settlement records why a run failed, and the board
shows that record.
> - Runs that fail without a named code settle as `adapter_failed` (or
`setup_failed`) with no cause.
> - A user, an operator, and the restart chaos harness cannot tell what
went wrong from such a run.
> - Some run errors also show raw SQL statement text to users.
> - This pull request records a bounded `failureCause` code for generic
failures, shows it on the board, and keeps SQL text out of run errors.
> - The benefit is that a generic failure now names its cause, for
example "adapter failed (40001)", without leaking messages, SQL, or
secrets.

## Linked Issues or Issue Description

Refs #20385 (the nested-cause extraction this change reuses). Related:
#20421 (restart chaos universal checks U1-U4).

**What happened?**

The restart chaos audit of 79 saved waves found 30 failed runs with
`errorCode: "adapter_failed"` and a null or generic error. Every U3
("diagnosable") warning was `adapter_failed`. Nobody could tell what
failed. Some board run errors also showed raw SQL statement text from
wrapped database errors (Drizzle `Failed query: ... params: ...`).

**Expected behavior**

A run that fails with a generic code names a short, safe cause. The
board shows the cause next to the stop reason. Run error text never
shows SQL statements or their parameters.

**Steps to reproduce**

Make an adapter throw a wrapped database error (an `Error` whose nested
`cause` has `code: "40001"` and whose message has a Drizzle `Failed
query:` statement). Before this change, the run settles as
`adapter_failed` with no cause, and `heartbeat_runs.error` contains the
SQL statement and parameters. The new tests in
`server/src/services/heartbeat/run-completion.test.ts` reproduce this.

## What Changed

- Runner: move the #20385 event-commit cause extraction into
`safeErrorCauseCode`
(`packages/paperclip-runner/src/control-plane/safe-error-cause.ts`) and
export it. The Runner's own diagnostics use the same function, unchanged
in behavior.
- Shared: add the `failureCause` contract
(`packages/shared/src/run-failure-cause.ts`): one `[A-Za-z0-9_.:-]`
token of at most 80 characters, `normalizeRunFailureCause`,
`readRunFailureCause`, and `withoutSqlStatementText`.
- Server: add `server/src/services/heartbeat/run-failure-cause.ts`.
Settlement records `resultJson.failureCause` only when the code is
generic (`adapter_failed`, `setup_failed`). `errorCode` stays the same.
- Thrown errors (`failRun`, `failRunSetup`): `provider_http_<status>`,
else `provider_<category>`, else the first safe code in the error and up
to three nested causes (`40001`, `ECONNRESET`), else the error class
name, else `unknown_error`.
- Adapter results without a code (`completeRun`): the error family,
`signal_<name>`, `process_cancellation_failed`, `exit_code_<n>`,
`adapter_reported_error`, or `unknown_error`.
- The failure log lines (`heartbeat execution failed`, `heartbeat
execution setup failed`, and a new `heartbeat run failed` warning for
generic adapter results) include `failureCause` and `errorCode`.
- Server: replace SQL statement text in run error text with "Database
query failed (<cause>)" at all three settlement paths, including the
cancelled branch. The execution blocker's `runError`, the attention
inbox (failed-run excerpt and metadata), and continuation summaries
apply the same replacement on read, so older rows also stop showing SQL
there.
- Server: Retry admission and failed-preparation replay accept only a
closed set of `resultJson` keys. `failureCause` is added to each of
those lists (`task-retry.ts`, `chat-channels/failed-run-retry.ts`,
`cancelled-native-startup.ts`, `fresh-workspace-continuation.ts`). It is
a diagnostic code with no execution or replay authority, so a
failed-preparation run stays retryable.
- Server: `runsForIssue` returns a validated top-level `failureCause`.
It is null for older rows, non-string JSON values, and malformed values.
- UI: `RunForIssue.failureCause`; `IssueRunLedger` shows "adapter failed
(40001)". Older runs keep "adapter failed". The Storybook run-ledger
fixture has a failed run with a cause.
- Restart chaos harness: run samples keep `failureCause`. U3 treats a
generic code with a specific `failureCause` as diagnosable. A bare
`Error`, `unknown_error`, or the no-code fallback
`adapter_reported_error` still counts as vague. U3 stays a warning.
- Docs: `doc/run-log-events.md` and the harness README describe the
field and the SQL replacement.

## Verification

- Red before the fix: the four new `run-completion.test.ts` cases failed
against `origin/master`'s `run-completion.ts` (no `failureCause`, SQL
text in `error`).
- Green:
- `server/src/services/heartbeat/run-completion.test.ts`: all tests
passed (real embedded PostgreSQL). New cases cover a nested `40001`
three levels deep for execution and setup failures, SQL and a secret
parameter absent from `error` and `resultJson`, the bound (an
81-character code and a SQL "code" fall back to the class name),
`exit_code_137` for a code-less adapter result, a cancelled result with
SQL text, no cause for a named code, and the log line. On this loaded
host the suite's `afterAll` database teardown hit its 30 s hook limit
after every test passed (the known local teardown limit, also seen in
#20385). CI runs the suite.
- `server/src/services/heartbeat/run-failure-cause.test.ts` (4),
`server/src/__tests__/activity-service.test.ts` (recorded, legacy,
malformed, overlong, boolean, and number cause projections),
`server/src/__tests__/issue-continuation-summary.test.ts` (legacy SQL
row).
- `packages/shared/src/run-failure-cause.test.ts` (6),
`ui/src/components/IssueRunLedger.test.tsx` (21), restart chaos
`universal.test.ts` and `observer.test.ts`.
-
`packages/paperclip-runner/src/control-plane/durable-prp-control-plane.test.ts`:
207 passed (the stall-diagnostic cases still read `40001` and
`ECONNRESET`).
- Typecheck: shared, runner, server (all layers), UI, restart chaos
harness. `pnpm check:token-gates`: all gates clean.
- `pnpm verify` returned exit 2 (global doc input changed); required CI
supplies broad coverage.
- CI on `6295f8d` found that the new key made failed-preparation runs
ineligible for Retry (`chat-channels.integration.test.ts` saved
board-message Retry got 409;
`workspace-recovery-preparation-dispatch.test.ts` dispatched 0 Retries).
Both reproduced locally and pass after the allowlist fix, with the other
459 Retry cases in `chat-channels.integration.test.ts` and all 25 in
`workspace-recovery-preparation-dispatch.test.ts`.
- No chaos waves were run. Storybook was not opened in a browser; the
fixture typechecks and the label is covered by the UI test.

## Risks

- Low risk. `errorCode` values do not change, so retry, recovery, and
comparison logic is unaffected.
- No schema change and no migration. `failureCause` is a new optional
`resultJson` key. Rows saved by the previous release have no key and
render as before; a new row read by an old server is ignored.
- The SQL replacement only acts on a Drizzle `Failed query:` marker, or
on `$n` placeholder text with a statement shape. Ordinary error text
does not change. When it acts, it also drops the text after the
statement.
- Code that checks `resultJson` against a closed key list must allow
`failureCause`. I found and updated the four lists on the Retry path; a
list I missed would refuse a Retry for a run with a generic failure.
- This is the run log / instance database path. It adds no Telemetry
event.
- The server failure log line still logs the raw error message
(unchanged behavior); only the board-facing text is changed.

## Model Used

- Claude Opus 5.5 (Anthropic), model ID `claude-opus-5-5`, used through
Claude Code with tool use (shell, file editing) and code execution for
tests.

## Checklist

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-11-10 02:14:53 -06:00
Nicky Leach 8809f44b5e Connect Codex and Claude login to agent computers (#20417) 2026-11-09 22:12:49 -08:00
a5e01634f6 feat(ui): ask self-hosted users to star Paperclip on GitHub after 5 done tasks (#20453)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Many teams run Paperclip self-hosted, so the project grows when
those users tell others about it
> - A GitHub star is the cheapest way for a user to do that, but the app
never asks
> - Asking too early is noise. A user who has finished a few tasks knows
whether Paperclip helps them
> - This pull request adds a one-time, dismissable modal that asks
self-hosted users for a star after their company has 5 done tasks
> - The benefit is more visibility for the project, and no repeated
prompt for the user

## Linked Issues or Issue Description

**Subsystem affected**

Board UI (dialogs shown in the main app shell).

**Problem or motivation**

Self-hosted users have no prompt to star the repository. Users who like
Paperclip often do not think of it. A prompt at the right moment helps
other teams find the project.

**Proposed solution**

Show a small modal once the selected company has 5 tasks with status
"done". The modal shows the Paperclip mark, the headline "You completed
5 tasks!", a short body, a "Star on GitHub" button and a "Not now"
button. It shows only once per browser. Closing it in any way counts as
seen.

**Alternatives considered**

A permanent banner or sidebar link. Both stay on screen after the user
decides, so they cost attention every day. A one-time modal costs
attention once.

**Roadmap alignment**

This is a small UI addition. It does not overlap with planned core work
in ROADMAP.md.

No related public issues or PRs found (searched for "star", "github
star").

## What Changed

- Added `GitHubStarPrompt` (`ui/src/components/GitHubStarPrompt.tsx`).
It reads the health endpoint and shows only on self-hosted instances
(never on Cloud), never during onboarding, and only after 5 done tasks
in the selected company. It waits for any other open dialog to close. It
stores a "seen" flag in `localStorage` when it opens, so a reload does
not show it again.
- Mounted the prompt in `BoardDialogs`.
- Exported the modal-detection helpers (`MODAL_SELECTOR`,
`useModalOpen`) from `AnnouncementWell` so both components use the same
rule.
- Added `PaperclipMark`, the mark alone from the existing lockup
geometry. The lockup and the mark now share one path constant.
- Added the `issues.starPromptDone` query key.
- Added tests for when the prompt opens and when it stays hidden.

## Verification

- `pnpm --filter @paperclipai/ui exec vitest run
src/components/GitHubStarPrompt.test.tsx
src/components/AnnouncementWell.test.tsx`
- `pnpm check:token-gates` (all gates clean)
- `pnpm typecheck`
- Manual: on a self-hosted dev instance, mark 5 tasks in one company as
done. The modal opens. "Star on GitHub" opens
github.com/paperclipai/paperclip in a new tab. Reload: the modal does
not open again. To see it again, delete the
`paperclip.github-star-prompt.seen` key from `localStorage`.

## Risks

- Low risk. The change is UI only and additive.
- The prompt runs one small issues query (limit 5) while it is still
eligible. After it is seen, the query is disabled.
- If `localStorage` is not available, the prompt treats itself as
already seen and does not show.

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

## Model Used

- Claude Opus 5.5 (Anthropic), model id `claude-opus-5-5`, run through
Claude Code with tool use (shell, file edits). Standard context window;
extended thinking at low effort.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] 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: squadbot <squadbot@squadbots-Mac-mini.local>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 21:11:29 -08:00
DottaandClaude Opus 5.5 9a4a5066fa perf(inbox): stop optional reads from holding Mine rows hidden (#20407)
> Follows #20388, which merged to `master` as `50df9a19b9`. This branch
now includes `origin/master`, so its diff carries only this PR's change:
`ui/src/pages/Inbox.tsx`, `ui/src/pages/Inbox.test.tsx`,
`ui/src/api/approvals.ts` and `ui/src/api/access.ts`.

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The Mine inbox is the first page a board user opens to see the tasks
that need them
> - The page hides task rows until every inbox read finishes, including
reads that do not change the rows
> - A slow user directory, approvals or join-request read therefore
delays every task row, and a stalled read keeps the skeleton up
> - Loading is also a layout contract: content that arrives late must
not push visible rows down or reset keyboard selection
> - This pull request removes the user-directory gate where names only
fill row text, and bounds the approvals and join-request gates so a
failure or stall cannot hold rows hidden
> - The benefit is that a slow optional read no longer hides task rows,
and the rows still do not move when late data arrives

## Linked Issues or Issue Description

Refs #20388

**What existing behavior does this improve?**
The Mine inbox reveal gate waits for every source, including the user
directory, approvals and join requests.

**Subsystem affected**
Board UI inbox (`ui/src/pages/Inbox.tsx`).

**Current behavior**
If the user directory is slow, every task row waits for it, although
user names only fill text in the rows. If approvals or join requests
stall, the skeleton stays up until the request ends.

**Proposed behavior**
Rows reveal on session, experimental settings, `inbox/mine`, dismissals,
agents and projects. The user directory fills names in place. Approvals
and join requests can delay rows for one attempt of at most 10 s.

**Reason and benefit**
A slow optional read no longer hides the task list. A failed read shows
the existing "Some inbox items couldn’t load" banner with a retry
instead of a skeleton.

**Breaking changes**
None.

## What Changed

- User directory: it no longer gates task rows. It still gates rows when
they are grouped by assignee, because user names name and order those
groups.
- Approvals and join requests: they still gate rows (see "Kept gates"
below). The gate now covers only the first attempt (`isLoading &&
failureCount === 0`). That attempt uses a 10 s request timeout
(`INBOX_OPTIONAL_READ_TIMEOUT_MS`). After a failure or timeout, rows
reveal and the query retries in the background.
- Timeout notice: a timed-out approvals or join-request read is a
transient error, which the inbox hides. Once the app's retries end, the
inbox now shows the "Some inbox items couldn’t load" notice and "Retry
missing items" button for a `TIMEOUT` on those reads. Other transient
errors stay hidden as before.
- `approvalsApi.list`, `accessApi.listJoinRequests` and
`accessApi.listUserDirectory` accept optional `RequestOptions` (signal
and timeout). Other callers do not change.
- Tests in `ui/src/pages/Inbox.test.tsx` ("optional reads and the reveal
gate"):
- rows reveal while the user directory is pending, and keep their order,
their DOM nodes and the keyboard selection when it arrives;
  - assignee grouping still waits for the user directory;
  - a delayed approvals read holds rows (the kept layout guarantee);
- a failed approvals or join-request read reveals rows with the retry
banner and no skeleton;
- after one failed attempt, rows reveal while the approvals read
retries;
  - a stalled approvals read times out and reveals rows;
- approvals or join requests that keep timing out under the app retry
policy (`shouldRetryRequest`) show the missing-items notice and retry
button.

### Kept gates

Approvals and join requests stay in the gate. On Mine they sort in among
the task rows by time (`getInboxWorkItems`). A pending approval is
usually recent, so it sorts near the top. If it arrived after the rows
were visible, it would push every task row down by one row. I could not
reserve space for it, because the count is unknown until the read
returns, and an empty reserved slot would collapse and shift the rows
the other way. So I kept these two gates, and I bounded them so they
cannot hold rows hidden on failure or stall.

Keyboard selection already follows the selected row by key, so late data
does not reset it. The user-directory test checks this.

## Verification

Focused checks on this revision:
- `cd ui && npx vitest run src/pages/Inbox.test.tsx
src/api/access.test.ts`: all passed (91 Inbox tests).
- Red/green: with the #20388 versions of `Inbox.tsx`, `approvals.ts` and
`access.ts`, three new tests fail (user directory pending, retry after
first failure, stalled approvals). They pass with this change. The other
four pin guarantees that already held.
- Review fixes: the timeout-notice tests fail without `eb5d55da86` and
pass with it. `cd ui && npx vitest run src/pages/Inbox.test.tsx`: 94
passed.
- After merging `origin/master` (`54d73aef19`): `cd ui && npx vitest run
src/pages/Inbox.test.tsx src/api/execution-workspaces.test.ts`: 101
passed. `pnpm --filter @paperclipai/ui typecheck`: passed.
- CI note: on head `e438e16c80`, `ci / e2e shard (3/8)` failed in
`planning-mode-visual-verification.spec.ts` (the first task's
`task-chat-composer-mode` did not appear within 30 s). That journey runs
onboarding into the first task chat. It does not open the inbox or use
the changed reads, so I treat it as unrelated. The other 33 tests in the
shard passed. The new head reruns it. `pnpm check:token-gates`: clean.
- `pnpm verify --base perf/inbox-referenced-workspaces` selected 2,219
Vitest files because `ui/src/api/access.ts` is imported widely. I did
not run that broad set locally (the shared host was under heavy load).
Required CI supplies that coverage.

Performance (same local fixture as #20388: authenticated mode, non-admin
member, 8,750 execution workspaces, 38,750 tasks, production UI bundle,
headless Chromium, new context per run). The host load average was about
30–48 from other work, so I used alternating pairs.

| Scenario | #20388 | This PR | Median paired saving |
| --- | --- | --- | --- |
| No injected delay, 6 pairs | median 870 ms (747–1,922) | median 1,046
ms (795–2,002) | mixed signs, no measurable change |
| User directory delayed 3 s, 5 pairs | median 3,980 ms (3,852–6,048) |
median 1,096 ms (698–1,263) | 2,814 ms |
| Approvals stalled 20 s, 1 run each | 20,829 ms | 10,737 ms | — |

On this fixture the optional reads finish before `inbox/mine`, so this
PR does not change the normal cold load. It helps when an optional read
is slower than `inbox/mine`, for example under server load.

## Risks

- Low risk. The change only removes or bounds waits for reads that do
not decide row order.
- Assignee names can appear a moment after the rows on a slow user
directory. The rows keep their height and position.
- If approvals or join requests time out on every retry, the notice and
retry button appear. If they time out and later succeed, they insert
into the list after the rows are visible. This happens only in that
failure path.
- The 10 s timeout applies to these inbox reads only. Other pages that
read the same data do not change.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with
tool use (shell, file edit, headless browser). 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 20:29:47 -06:00
Devin FoleyandClaude Opus 5.5 a9d0946e64 fix(ui): keep the persisted cache's final state when it cancels reads (#20431)
The persisted read cache cancels in-flight reads with revert: false when
it clears account data or evicts a denied task, then writes their final
state itself. A non-silent cancel settled later as a query error, which
overwrote that state with CancelledError and reached QueryCache.onError.
Cancel these reads silently so the written state stays.

Refs #20430

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 18:20:57 -08:00
Devin FoleyandClaude Opus 5.5 5570debc5b fix(ui): stop reporting cancelled queries to Sentry (#20430)
React Query's CancelledError keeps the default Error name, so the
name-based abort check never matched it. A query cancelled with
revert: false (for example by the persisted cache when an issue read
is denied or the account changes) reached the query cache onError as
an "unknown" error and was sent to Sentry. Recognize it by class.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 17:43:34 -08:00
DottaandPaperclip 915edcff49 fix(workspaces): preserve archived native recovery during migration (#20424)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Durable workspace migration must retain saved files and pending
recovery work.
> - An archived Git workspace can have an authenticated restore recipe
and a terminal native run with pending remote files.
> - Its task can select a different workspace. Reopening the archived
workspace would change that task.
> - The current migration owner refuses this source because it has no
live local directory.
> - This pull request uses the existing online recovery preservation
owner for this exact archived recipe case.
> - Migration retains the old task, provider allocation, recovery
action, and native sync reference.

## Linked Issues or Issue Description

Refs #20298, #20394, and #20418. Related cleanup work in #19812 does not
cover this migration case.

**What happened?**

A historical archived Git workspace with a saved restore recipe and a
terminal prepared native sync reference cannot migrate online. Admission
reports `remote_copyback_unresolved` and
`workspace_recovery_unresolved`. The old task can already select another
workspace, so reopening it is not a safe migration path.

**Expected behavior**

Authenticate the archived files into private scratch. Preserve all typed
native and provider recovery obligations in the existing migration
journal and markers. Keep the original task selection, history, archive
state, and restore flags unchanged.

**Steps to reproduce**

1. Archive a Git worktree with staged, unstaged, and untracked files in
a saved recipe.
2. Retain a terminal failed native run, its prepared sync reference, and
its matching remote allocation and recovery action.
3. Remove the old local worktree and select a different workspace on the
task.
4. Call `migrateRemoteWorkspace` without `writersStopped`.

**Paperclip version or commit**

Reproduced on `b2a7d322ed0bafda04a0b78ce14e4f25c8d5d170`.

**Deployment mode**

Self-hosted server with PostgreSQL and configured object storage.

## What Changed

- Permit the existing online recovery owner to inspect recipe-backed
archived Git sources.
- Acquire the existing native finalization locks for those sources.
Reinspect their exact proofs after lock acquisition and before
publication.
- Lock related source-task ancestry before publication.
- Reuse the existing private Git restore, authenticated archive,
journal, marker, and recovery-source contracts. Add no schema or
provider calls.
- Leave active sources and archived sources without a valid recipe on
their existing refusal paths.
- Include the shared UI correction from #20418. A nullable draft
override follows fresh task data when the user has no local choice.
Explicit choices increment a generation. Normal and recovered successful
sends clear only their own choice; failed sends and newer choices remain
intact.

## Verification

- Unchanged master RED: the new historical-source fixture fails with
`remote_workspace_migration_blocked:remote_copyback_unresolved,workspace_recovery_unresolved`.
- Five focused real PostgreSQL, filesystem, Git, and encrypted-storage
tests pass on Node 24.22.2. The success case loses an upload reply,
resumes the same operation and snapshot, restores the
staged/unstaged/untracked bytes, and verifies unchanged task history,
lease, recovery action, finalizer, and recipe flags.
- Recipe, prepared reference, and provider allocation changes during the
actual upload each refuse publication. A competing native finalization
owner refuses before capture.
- Migration command: `pnpm exec vitest run
server/src/services/remote-workspace-migration.test.ts -t 'unselected
archived recipe|archived prepared source|competing native F owner'`.
- All server typecheck layers, module boundary checks, and whitespace
checks pass for the migration revision. The migration source bytes
remain unchanged through the shared UI follow-ups.
- Initial CI run 42727051535 exposed an existing planning-mode browser
failure: shard 3/8 passed 33 cases but the refreshed task mode did not
appear. The same issue occurred twice on #20418. A real local regression
proved that initial cached data could leave the composer in its old
mode. This path does not invoke migration.
- Review then exposed genuine defects in the initial shared UI repair:
an optimistic save rollback could reset a failed draft, and an earlier
send could replace a newer draft choice. Recovered delivery also left a
consumed choice pinned. All three have real RED→GREEN controls. The
final shared fix uses an explicit nullable override, a choice
generation, and one attempt-bound settlement owner for normal and
recovered success, with no value-equality inference or durable payload
changes.
- Exact shared UI source now passes all 145 composer tests (2.88s), the
original desktop/mobile planning browser case (15.5s; assertions and
timeouts unchanged), token gates, and the canonical UI typecheck. The
test harness uses the full persisted `IssueWorkMode` type, including
`skill_test`, to fix the observed CI typing failure.
- These UI results are reused from the identical shared source in #20418
through `ba1f85f18085dd0e84c594ed0cb68d5bf817c93c`. The unchanged
planning browser pass is on the explicit-state model before the later
settlement-helper extraction. The final receipt/resend controls, full
composer suite, typecheck, and token gates include that extraction. They
do not claim a local browser run on this entire combined branch. Fresh
required CI and review qualify this PR head.
- Previous-release state: existing recipe, v3 prepared reference, and
journal shapes carry forward unchanged. A saved partial operation
resumes with the same IDs. Migration does not certify remote file
recovery or provider termination. Live provider recovery and hosted
migration remain separate acceptance steps.

## Risks

The change admits one more source class into an existing preservation
path. An incorrect proof could publish incomplete file authority. The
canonical recipe validation, full authenticated restore, native
finalization locks, source reinspection, and publication checks remain
mandatory. Pending remote data stays pending and retains its original
allocation and reference. The shared UI repair updates only an unchanged
draft mode; its regression checks preserve a local user choice.

## Model Used

OpenAI Codex, GPT-6, with reasoning, repository tools, and code
execution. The runtime does not expose a deployment suffix or
context-window size.

## Checklist

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

> - Paperclip is the open source app people use to manage AI agents for
work
> - The Mine inbox is the first page a board user opens to see the tasks
that need them
> - The page loaded the company's full execution-workspace summary list
and kept task rows hidden until that list arrived
> - On a company with thousands of workspaces, that read checks each
workspace row against the privacy predicate. It was the slowest request
on the page
> - The inbox needs workspace names only for the workspaces its rows
reference, and only when the view shows workspace data
> - This pull request adds an `ids` filter to the summary read, makes
the inbox request only referenced workspaces, and stops rows from
waiting for workspace data when the view does not show it
> - The benefit is that Mine rows appear when the task data arrives, and
the workspace read becomes a small bounded query

## Linked Issues or Issue Description

**What existing behavior does this improve?**
The Mine inbox (`/:companyPrefix/issues?view=mine`) shows its task rows
late on companies with many execution workspaces.

**Subsystem affected**
Board UI inbox and the execution-workspace summary API.

**Current behavior**
The page calls `GET
/api/companies/:id/execution-workspaces?summary=true` with no filter. On
a large company this returned 8,758 rows (2.5 MB) and took 12.3 s on the
server. The reveal gate included this read, so rows appeared at 13.4 s
although `inbox/mine` arrived at 1.6 s.

**Proposed behavior**
The inbox requests summaries only for the workspaces its rows reference.
Rows appear when the Mine task data arrives, unless the view shows
workspace data.

**Reason and benefit**
The workspace read becomes a small bounded query, and it leaves the
critical path for the default Mine view.

**Breaking changes**
None. The `ids` parameter is optional and additive.

## What Changed

- `packages/shared`: add `executionWorkspaceSummaryIdsQuerySchema`
(comma-separated UUIDs, 1–100 distinct values) and
`EXECUTION_WORKSPACE_SUMMARY_IDS_MAX`.
- `server`: `GET /companies/:companyId/execution-workspaces` accepts
`ids`. Bad input returns 422. The service adds
`inArray(execution_workspaces.id, ids)`. The company scope and the actor
read condition still apply, so privacy does not change.
- `ui/src/api/execution-workspaces.ts`: `listSummaries` accepts `ids`.
New `listSummariesByIds` reads ids in chunks of 100 and concatenates the
results.
- `ui/src/pages/Inbox.tsx`: the workspace query is keyed on the sorted
distinct `executionWorkspaceId`s of the rows the inbox holds (Mine,
touched, context and remote search issues). It uses `keepPreviousData`
and runs only when isolated workspaces are on and at least one id
exists.
- Reveal gate: rows wait for the workspace read only when workspace data
changes what the rows show. This is workspace grouping, a visible
Workspace column, or an active search (workspace names are searchable).
Otherwise rows reveal without it.
- Tests: route ids pass-through and 422 cases (non-uuid, empty,
separators only, more than 100); service test for the id filter with
company scope and the read condition; UI api chunking test; Inbox tests
for "rows reveal while the workspace read is pending when no workspace
column or grouping is active", rows waiting when the Workspace column is
visible, and "shows the resolved isolated workspace name" with the
id-filtered call.

Follow-ups (not changed here, they still read the unfiltered list):
- `ui/src/pages/LegacyInbox.tsx:772`
- `ui/src/components/IssuesList.tsx:985`
- `ui/src/components/LegacyIssuesList.tsx:888`
- `ui/src/components/NewIssueDialog.tsx:570` (filtered by project and
reuse, but not by ids)

## Verification

Focused checks on this revision:
- `cd server && npx vitest run
src/__tests__/execution-workspaces-routes.test.ts
src/__tests__/execution-workspaces-service.test.ts -t "summar|ids"`: 14
passed (embedded Postgres ran the service test).
- `cd ui && npx vitest run src/api/execution-workspaces.test.ts
src/pages/Inbox.test.tsx`: all passed.
- `pnpm --filter @paperclipai/server --filter @paperclipai/ui --filter
@paperclipai/shared typecheck`: passed.
- `pnpm check:token-gates`: clean.
- `pnpm verify` selected 2,481 Vitest files (the shared validator change
reaches most suites). I stopped it at a 25-minute limit on a heavily
loaded shared host. Before the limit, three server tests outside this
change failed: `agent-commentary-skills` (skill mount),
`agent-config-read-access` (24.8 s timeout) and
`agent-directory-working-copies`. Required CI supplies the full
coverage.
- Review fix `fc3f82dae0`: when workspace data is visible, the reveal
gate also waits while `keepPreviousData` supplies the previous id set's
summaries. A new test switches Mine → Recent to a new workspace and
checks the Recent row stays hidden until its workspace loads. It fails
without the change.
- Follow-up commits `bf34f84823` and `8fc743b5ca` add a 100-row Mine
test with a counted workspace id scan. It fails with the old `= []`
defaults and passes with the stable `NO_ISSUES` default.

Performance (local, synthetic fixture):
- Instance: isolated worktree instance, `authenticated` mode, signed in
as a non-admin company member so the real workspace privacy predicate
runs. Production UI bundle (`PAPERCLIP_UI_DEV_MIDDLEWARE=false`).
Headless Chromium 1440×900, a new browser context per run (cold HTTP
cache and cold query cache). macOS, Apple Silicon, with other local
sessions running.
- Fixture: 8,750 execution workspaces (each with a source task and a
privacy-source key), 38,750 tasks, 150 Mine tasks; the Mine snapshot
returns 100 rows that reference 100 workspaces.
- Finish event: the first `[data-slot="task-row"]` is in the DOM.

| Measurement | Before (`02bfe71a6b`) | After |
| --- | --- | --- |
| Cold time to rows, median of 7 | 1,209 ms (1,046–1,312) | 702 ms
(567–1,209) |
| Workspace summary read, server wait (time to first byte), median of 7
| 496 ms unfiltered, 2.0 MB | 18 ms with 100 ids, 22 KB |

The difference of medians is 507 ms. This is close to the unfiltered
read's wait on this fixture (about 550–590 ms in the browser runs). The
runs were sequential batches, not alternating pairs.

Limits: this fixture does not reproduce the 12.3 s server time seen on
the development instance. Here the unfiltered read cost 0.5–1.7 s and
grew with the company's task count. The browser gain on the original
instance should be close to that instance's unfiltered read time, but I
did not measure it there.

## Risks

- Low risk. The `ids` filter only narrows the existing query. It does
not change the read condition or the company scope.
- A row whose workspace is not readable by the actor shows no workspace
name, as before.
- When the Workspace column or grouping is visible, rows still wait for
the workspace read. That read is now small.
- When the id set changes, the query refetches all chunks. Before a view
first reveals, rows wait for the new summaries if workspace data is
visible. After that, the previous names stay visible while a refresh
loads.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with
tool use (shell, file edit, headless browser). 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 18:02:55 -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