Commit Graph
2503 Commits
Author SHA1 Message Date
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
DottaandClaude Opus 5.5 b184e40108 perf(ui): serve every project list from one includeArchived read (#20395)
## Thinking Path

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

## Linked Issues or Issue Description

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

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

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

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

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 17:42:47 -06:00
DottaandClaude Opus 5.5 c7eb35809e perf(ui): read skills and routines only when an editor mounts (#20392)
## Thinking Path

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

## Linked Issues or Issue Description

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

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

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

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

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 17:42:39 -06:00
DottaandClaude Opus 5.5 7e93ccb4b3 perf: fetch recent task summaries in one request (#20389)
## Thinking Path

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

## Linked Issues or Issue Description

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

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

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

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

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 17:42:32 -06:00
DottaandPaperclip eebdfe71b1 fix(ui): show submitted screenshots and Markdown in steering messages (#20367)
## Thinking Path

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

## Linked Issues or Issue Description

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

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

## What Changed

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

## Verification

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

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

## Risks

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

## Model Used

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

## Checklist

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

---------

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

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

## Linked Issues or Issue Description

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

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

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

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

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

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-09 15:49:31 -06:00
Dotta 98f49af70a feat: filter agents by model, status, harness, and connection (#20317)
Add shared filters to both All Agents layouts and views. Keep chart ancestors and bulk selections when filters hide agents. Use the same metadata for filter choices and table cells.

Add tests and update the filter journey and design guidance.
2026-11-09 14:21:38 -06:00
DottaandClaude Opus 5.5 93f7c3b5d3 fix(tasks): keep a budget-held Retry's failure visible and refresh the open task (#20338)
## Thinking Path

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

## Linked Issues or Issue Description

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

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

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

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

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

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

---------

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

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

## Linked Issues or Issue Description

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

**Paperclip version or commit**

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-09 13:46:28 -06:00
DottaandPaperclip 0e325e8d52 fix(runner): settle saved handoffs after verified shutdown (#20336)
## Thinking Path

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

## Linked Issues or Issue Description

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

**What happened?**

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

**Expected behavior**

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

**Steps to reproduce**

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

**Deployment mode**

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

## What Changed

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

## Verification

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

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

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

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

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

## Risks

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

## Model Used

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

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-09 13:46:16 -06:00
Dotta a3e7e63330 Guard Native recovery with process-space proof and explicit containment (#19813)
## Thinking Path

> - Paperclip manages agents and their work.
> - Recovery must preserve execution ownership and accepted results.
> - A saved PID proves nothing outside its recorded process namespace.
> - Uncertain unfinished work must stay held until its original owners
stop.
> - Operators need a source-bound containment action and a separate
ordinary Retry.
> - Accepted results must use the existing finalizer without another
provider turn.
> - This PR keeps those paths safe, recoverable, and quiet between real
obligations.

## Linked Issues or Issue Description

Refs: #19713

Refs: #19591

Recovery could use a local PID to release or replace a controller from
another process namespace. Connected-chat Retry also rejected successful
containment on a task in `todo`. The new containment action used a
broader permission than the existing owner-stop action. A later review
found that result-less guards also rejected the existing accepted-result
confirmation action.

## What Changed

- Record and verify the controller's kernel/PID namespace. Keep unknown
and foreign unfinished ownership held. Preserve independent durable stop
evidence and the existing remote lease fences.
- Use the shared accepted-result ownership reconciler. Bind settlement
to the observed lease token, generation, boot, PID, namespace and
process birth. Retain finalizer, cleanup, current-controller, live-owner
and remote exclusions.
- Share the remote-owner reader across recovery consumers. Scope runner
leases to the same company, task and run. Do not make local PID probes
for remote accepted-result ownership.
- Gate original-host containment on `runtime:confirm_owner_stop`. Bind
it to the current task, source, assignee, original expiry, lease and
Stop state. Audit separate controller/provider containment and reviewed
action outcomes.
- Keep containment separate from ordinary Retry. Use one canonical
committed receipt and the normal admission, budget, workspace and
lineage checks. Preserve the recorded external-action outcomes.
- Keep the controller-only confirmation actionable for both result-less
work and accepted results. Result-less work gets its next source-bound
attention in the same transaction. Accepted results keep their original
result and use normal shared settlement/finalization.
- Keep identity inspection quiet. Preserve the original foreign-owner
expiry and use the existing scoped finalizer in accepted-result recovery
controls.
- Update the recovery card, validators, execution projection, docs and
access maps. Reuse existing canonical columns; add no migration or
ownership backfill.

## Verification

Current publication head: `71ea76b568de09d6313d8992e803131d9d5c54f3`.

**The current fix has a causal red/green check.** On the test-only
revision, both foreign-local stopped/fenced controller cases failed
because recovery probed saved runner, process-group and provider PIDs in
this host's namespace. This measured the wrong probes; it did not run
duplicate providers. After the fix, the same two cases passed every
no-probe, no-claim, no-controller-stamp, no-stop-receipt and
unchanged-authority assertion. Three existing remote preparation, grace
and fenced-controller progress cases also passed. Total: **5 passed, 0
failed;170 filtered rows excluded**. All three normal affected App
TypeScript programs passed on this head (660,681 and428 roots).

The fix separates a controller wait from execution stop proof.
Independent live/grace evidence still schedules recovery. A
stopped/fenced foreign controller can pass this namespace guard only for
remote execution, which keeps its later checkpoint and never-launched
checks. Foreign local execution stays held before runner or provider PID
inspection.

Earlier source-scoped results remain evidence, not fresh executions on
the current head:

- On `c86dd741`, the complete failed-owner file passed **25/25**, with
no skips or filters. Empty scans wrote no controller stamps. Foreign
ownership kept its original expiry. Normal returned-claim settlement and
attention at the exact deadline passed.
- On `c86dd741`, **15 selected accepted-result/restart controls
passed**. The81 filtered rows are excluded. Both completion cases used
the existing scoped finalizer and kept their accepted result without
another provider turn.
- Trusted CI at synthetic merge `50d072a5` (actual master `1b7fe334`
plus `c86dd741`) passed the complete **17-case maintenance-server
file**. This includes the original90-real-second quiet observation and
two qualified hourly windows. The quiet case took118,918ms. The four
jobs that failed on the earlier publication all passed on that synthetic
merge.
- On `6243b32c`, the complete ownership-report file passed **29/29**.
Native NULL namespace stayed unprovable and generic compatibility
remained. Three selected recovery controls passed;170 filtered rows are
excluded.
- On `f81f2c3c`,356 business cases passed: whole ownership-route221,
recoveryUI70, owner-proof38 and controller-confirmation8; selected
accepted-result authority18; and one real connected-chat containment,
separate authorized Retry, useful Native result and downstream reply
case. The2,024 filtered rows are excluded. A separate earlier
accepted-result causal case kept the original result and completed after
restart without replay.

Normal Shared exports, App programs, UI typecheck, token gates and map
checks passed on their recorded revisions. Unchanged DB, adapter, Eval
and Runner inputs/outputs have explicit carry proofs. The clean normal
merge with fetched master `8fe524f7` preserves all31 exclusive owned
paths and14,827 unrelated master entries. The nine overlapping paths
were reviewed, including the upstream unidentified-run stop action.
Fresh CI must validate that actual merge composition.

PG cases used existing external PG17, fresh owned databases, pools at
most4, one worker and no SQL observer during cases. Registered clients
and recorded process groups closed. Independent final SQL audits found
zero campaign sessions before normal database cleanup. Original failing
fixtures remain retained.

Original failures are preserved. One new test first failed compilation
due to a missing getter import; an import-only correction passed its
full owning programs before the causal red/green check. A local quiet
attempt stopped in the normal prerequisite hook before any business case
and its collector rejected two normal build markers. The later17-case CI
pass establishes quiet recovery; it does not turn that failed local
collector into a pass. An earlier useful case passed before its private
cleanup collector failed on an owned read-only directory; normal cleanup
completed after all owners closed. Earlier runner-shutdown cancellation
and a separate private whole-suite failure remain unexplained.

Current-head checks are complete:57 success,1 neutral and3 skipped,
including the successful aggregate CI gate. The legacy status passed.
Greptile is5/5 and Ponytail gives an explicit5/5 Ship verdict for this
head. All seven review threads are resolved. Full recursive typecheck,
the full test suite and release build were not repeated locally. No
result here proves deployed suspension, a live provider or termination
on another host.

## Risks

- Unknown physical ownership can require an owner/admin action. A false
containment statement can be unsafe.
- New work, reassignment, Stop, renewed authority, accepted results or
pending cleanup can invalidate an action. The operator must refresh.
- Controller confirmation does not prove provider/tool containment.
Completed external actions must not be replayed.
- Generic legacy and child-process compatibility remains separate from
the strict Native controller policy.
- Existing saved state and canonical schema are preserved. Permanent
safety refusals remain held with their normal named recovery path.

## Model Used

OpenAI `gpt-6.1-sol` agents implemented and tested the changes with
reasoning, repository tools and code execution. A GPT-6-based Codex
managing agent reviewed and integrated them. The host does not expose
the manager's exact build or the context-window sizes.

## Checklist

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

> - Paperclip is the open source app people use to manage AI agents for
work
> - Before a new attempt can start on a task, Paperclip must prove the
previous run's agent stopped, so two agents never work the same task at
once
> - A local native run can be interrupted after it asked to start the
runner but before it recorded the runner's process. The runner may have
saved state and started a provider, and provider processes can outlive
it, so the existing check (#19694) rightly refuses to assume the run
stopped
> - Today that case has no way out: every Retry is accepted and then
waits forever as `process_identity_missing` (storage chaos ST1, t02/t03)
> - This pull request makes the wait explicit and actionable. The task
explains why it waits, and a board operator who has checked can confirm
the run stopped; the parked Retry then starts at the click
> - A second shape from the Daytona storage baseline (t06) parked a
Retry with no reason at all: a follow-up was interrupted by a graceful
shutdown while it was still restoring its workspace. No provider ran,
but the restore hold kept the same agent held, so the Retry never
started
> - The benefit is that no Retry stays accepted-and-waiting with nothing
anyone can do

## Linked Issues or Issue Description

Refs #19831. Storage chaos wave `w-20261109T145628-s9902` (ST1), tasks
t02 and t03.

**What happened (CHA-3, run `3c3f6241`):**
- The run requested a runner launch (`native.process_start_requested`),
and a K2 restart hit before its process identity was recorded.
- After the restart, the resumed attempt failed on the storage outage.
The task was blocked.
- The board's Retry (202) was parked as `executionWait.reason =
process_identity_missing`: "The previous run has no verified stop
record."
- `recordUnidentifiedLocalRunnerStop` cannot prove the stop because this
run's runner saved control-plane state. Once that exists, a provider may
have started (#19694), and a provider is not guaranteed to carry
`--run-id` or `PAPERCLIP_RUN_ID` (the Codex provider env omits it on
purpose). The shared owner prover needs a PID, which the run never
recorded.
- Nothing could ever clear the wait.

**Daytona storage baseline `w-20261109T154810-s9911`, task t06:**
- The run succeeded. The follow-up was cancelled
(`continuation_task_ownership_changed`), and the next run was
interrupted by a graceful shutdown (`server_shutdown_interrupted`) 3 s
into `preparing`. It was restoring the saved remote workspace and had
launched no provider.
- Restore recovery saved the files (`workspaceRecovery.status:
recovered`, system recovery `complete`). But the
`legacy_execution_requires_reconciliation` restore hold releases only
for a different owner, or for a native Stop fenced before any provider
turn, so it kept the same agent held.
- The UI Retry returned 202 and parked with `executionWait.reason:
execution_recovery`, the generic wait, so the task showed a Retry that
never started.

Automatic proof needs the runner to record its identity before saving
state and to group provider processes under it. That is follow-up runner
work; this PR removes the endless wait now.

## What Changed

- `server/src/services/native-local-process-stop.ts`: new
`confirmUnidentifiedLocalRunnerStopped`. It accepts only a finished
native run of the task with no recorded process and whose latest launch
evidence is an unconfirmed launch on this host in a local environment.
It still refuses while a runner with `--run-id <run>` is running. It
records `native.local_process_stopped` with `basis: "operator_attested"`
and the attesting user. That is the receipt the existing continuation
gate already accepts.
- `server/src/routes/issues.ts`: `POST
/issues/:id/recovery-actions/confirm-run-stopped` (`runId`,
`stopEvidence` of 20 to 12,000 characters). It is board-only and
requires issue read plus `runtime:confirm_owner_stop`, like the other
stop attestations. It logs `issue.run_stop_confirmed` with the evidence,
promotes deferred intents for the issue, and resumes each parked task
Retry, so work starts at the click. Refusals return 409 with a code.
- `packages/shared`: `confirmRunStoppedSchema`, and the presentation
user action `confirm_run_stopped`.
- `server/src/services/task-interruption-presentation.ts`: a Retry
parked as `process_identity_missing` for a native run that recorded no
process now presents as waiting for the user (`canRetry: false`,
`userAction: "confirm_run_stopped"`), with a message explaining why and
who can act. A run with a recorded process keeps the existing
verification wait.
- UI:
- `ExecutionBlockerNotice` shows the explanation and, for users who can
confirm a stop, "Confirm it stopped" with a short evidence form.
- It reuses `PreviousServerStopConfirmation`, now with optional
`action`, explicit `runId` and `formId`, and a custom button label.
  - The Inbox names the action.

- `server/src/services/execution-blocker.ts`:
`interruptedPreparationBeforeProviderWorkCondition`. The restore hold
also releases for the same agent when the stopped run was a
system-interrupted preparation (legacy runtime never resolved, still
`preparing`) with no process identity, no runner or provider events, no
finalization or result, and every lease released with a confirmed stop.
It mirrors the existing interrupted-preparation proof and still requires
the completed workspace-recovery receipt. Auto-replay is unchanged; this
only lets the user's Retry or message start.
- Review fixes:
- `unidentifiedLocalLaunchRefusal` holds the endpoint's launch, host and
lease checks, and the presentation offers `confirm_run_stopped` only
when it passes.
  - The attested stop wakes cost accounting in the same transaction.
  - The confirmation form is keyed by task and run.
  - The route is registered in OpenAPI.

## Verification

Red/green (each new test fails against master and passes with this PR):
- `server/src/__tests__/issue-confirm-run-stopped-routes.test.ts`:
- The parked Retry presents `confirm_run_stopped` with the explanation.
- Confirming records the operator-attested stop and the activity entry,
satisfies the stop check, and resumes the parked Retry: its receipt is
coalesced into a newly started run.
  - It refuses with 409 while a runner for the run is running.
  - Agents and viewers get 403.
-
`ui/src/components/ExecutionBlockerNotice.confirm-run-stopped.test.tsx`:
the notice offers "Confirm it stopped" (and no dead Retry) and posts the
run and evidence. A viewer sees the explanation without the action.

- `server/src/__tests__/interrupted-preparation-restore-retry.test.ts`
(t06 shape):
  - The restore hold no longer blocks.
- A Retry is accepted and resumed into a started run, and the audit hold
keeps `replay: blocked`.
- The hold stays when a provider turn started, when a runner event was
recorded, when an operator stopped the run, or when the sandbox has no
stop receipt.
- `issue-confirm-run-stopped-routes.test.ts`:
- A case per refusal code checks the presentation keeps the generic wait
and the endpoint returns 409.
  - An `alreadyStopped` case.
  - An accounting-wake assertion.
- The UI test switches targets with stale evidence.

Regression and static checks:
- `ExecutionBlockerNotice`, `WorkspaceExportRecovery` and its control
tests: 74 passed.
- `tsc --noEmit` passes for `packages/shared`, `server` and `ui/src`.
`pnpm check:token-gates` is clean.

## Risks

- A board operator can now attest that an unidentified run stopped. The
attestation is permission-gated, requires written evidence, is
activity-logged, and is refused while a runner for the run is visibly
running. A wrong attestation could start a second agent beside an
orphaned provider, the same trust model as the existing previous-server
attestations.
- The confirmation component now accepts a run without a recovery
action. Existing callers pass the action as before.
- No schema migration.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 11:36:41 -06:00
DottaandClaude Opus 5.5 12523fb677 fix(chat): send a task whose automatic recovery stopped to the person's Retry (#20306)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A person asks the chat agent (the controller) to act on tasks that
other agents own
> - When a task's automatic recovery stops, for example after three
runner losses, the task is blocked and the person gets a Retry control
on it. Retry rechecks the task's gates and keeps the failed attempt's
evidence
> - The blocker told every reader "Try again or send a new message to
continue". A controller read that as its own instruction. It found that
Retry is board-only, then resumed the task with a comment and reassigned
the task to itself
> - That skips the assignee's budget and hold gates and hides the
failed-run Retry from the person
> - This pull request names Retry as the person's control in the blocker
text, and tells the controller to point the person to it
> - The benefit is that "try that task again" leads to the supported,
gated Retry instead of an agent takeover

## Linked Issues or Issue Description

Refs #20270 (automatic recovery after a lost local runner)

**What happened?**
In the chat eval
`chat-features.runner-opencode.local.recovery.retry-exhausted`, a person
asked the CEO agent to "Try the website task again" after the website
task's native recovery was exhausted. The agent read the task's
`executionBlocker` (`canRetry: true`, nextAction "... Try again or send
a new message to continue once the previous execution has stopped."). It
found that `POST /api/issues/{id}/retry` is board-only. It then posted a
`resume: true` comment, which moved the task from blocked to todo, and
reassigned the task to itself to re-run it. The task lost its failed-run
Retry control, and the original assignee's gates were not checked.

**Expected behavior**
The agent links the original task, says why it stopped, and tells the
person to use Retry there. It takes the work over only when the person
explicitly asks for that.

**Steps to reproduce**
1. Let a native run exhaust its automatic recovery (for example, three
runner losses).
2. In the controller chat, ask it to try that task again.
3. The agent comments to resume the task, or reassigns it, instead of
pointing to Retry.

## What Changed

- `execution-blocker.ts`: the text that is added when a person can retry
is now `EXECUTION_BLOCKER_PERSON_RETRY_ACTION`: "A person can use Retry
on this task, or send it a new message, to continue once the previous
execution has stopped. Agents cannot retry it for them."
- `agent-conversations.ts`: a new chat guidance paragraph. When another
task's automatic recovery has stopped, the supported next step is the
person's Retry on that task. The controller links the task and explains
why it stopped. It does not reassign the task, change its status,
comment on it to resume it, or create a replacement task, unless the
person explicitly asks it to take the work over.

## Verification

- `board-access-denial.test.ts`: the directive contains the new rule.
- `execution-blocker-read.test.ts`: a retryable blocker's nextAction
ends with the person-Retry text.
- 54 tests pass locally for these two files.
- Paid live run of `recovery.retry-exhausted` with this change and
#20270 on the eval branch: the run ID will be added below.

## Risks

- Low. Only text changes. Any UI that shows the blocker's nextAction
shows the new sentence instead of "Try again or send a new message ...".
- No schema or saved-state change.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 10:53:08 -06:00
DottaandClaude Opus 5.5 189fefb991 perf(task-chat): warm the whole first frame before a task opens (#20286)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - People open tasks from the issues list and the inbox to continue a
conversation with an agent
> - The compact task chat shows its first frame only after about ten
reads settle. These include recent comments, activity, recent runs,
interactions, attachments and live runs
> - Navigation prefetch warmed only the issue and its recent comments.
The other reads started on the click, and the slowest one decided when
the chat appeared. On staging that took 1.1–2.0 s
> - This pull request warms the whole first-frame set when an open is
likely, under the exact keys the page reads
> - The benefit is that a task the person is about to open can render
from cache. A pointer that only passes over rows does not add load

## Linked Issues or Issue Description

**Problem or motivation**
On a busy instance, the composer and the latest messages appear 1.1–2.0
s after a click on a task row. The page waits for about ten reads.
Before the click, only two of them are warmed.

**Proposed solution**
Warm the rest of the first-frame reads when an open is likely: a mouse
or pen press, a click, keyboard Enter, or a hover or focus that rests on
the link for 120 ms. The page and the prefetch use the same fetchers and
query keys.

**Alternatives considered**
- Warm everything on every hover. This sends about 7 requests for each
row that a pointer crosses, so it was rejected.
- Warm only on press. The time from press to click is short, so most of
the wait would remain.
- A single bootstrap endpoint. This is planned separately, because it
changes the API contract.

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

## What Changed

- New `ui/src/lib/issue-chat-history-queries.ts`:
- The recent activity and recent run window fetchers moved here from
`IssueDetail.tsx`, together with their query keys. The logic is
unchanged.
- `prefetchIssueChatHistory` warms activity, the run window,
interactions, attachments and live runs. It warms the active run only
for running work, as the page does.
- Live state uses a 5 s stale time. The other reads use the existing 30
s navigation stale time.
- The run-window prefetch uses the same conversation mode as the page.
Thus a reconnect in an agent conversation reads only the watched runs by
id.
- `prefetchIssueCommentInputFacts` warms the input facts of the recent
comments after those comments arrive.
- `ui/src/hooks/useIssueIdBatches.ts`: new `prefetchIssueIdBatches`
fills the same batch keys that the hook reads. The hook and the prefetch
share one batching function.
- `prefetchIssueDetailForNavigation` accepts `history: true` and then
also warms that set, plus the comment input facts.
- `IssueLinkQuicklook`:
- A hover still warms the light set at once. The quicklook trigger does
not start a history prefetch.
- A hover that rests on the link for 120 ms warms the full set. Leaving
the link before 120 ms cancels it.
- Keyboard focus uses the same 120 ms dwell as a hover. Blur cancels it.
  - A primary mouse or pen press and a click warm the full set at once.
- A touch can start a scroll, so it warms only the light set. The full
set waits for the click.
- The keyboard Enter handlers in `IssuesList`, `LegacyIssuesList`,
`Inbox` and `LegacyInbox` pass `history: true`.
- `IssueDetail.tsx` uses the shared fetchers and keys.

## Verification

- `cd ui && npx vitest run src/lib/issueDetailCache.test.ts
src/components/IssueLinkQuicklook.test.tsx
src/hooks/useIssueIdBatches.test.tsx` passes 29 tests. These are the new
ones:
- A `history` prefetch fills every key that the compact chat waits on,
with the same run-window shape that the page builds.
- A `history` prefetch fills the comment input-fact batch keys that the
chat reads.
- In an agent conversation, a reconnect reads the watched runs by id,
not every run.
- Prefetched id batches render with no loading state on the first
render.
  - The active run is read only for work that is running.
  - A light prefetch makes no history requests.
- A pointer that passes over a link in less than 120 ms warms only the
light set, with the quicklook on and off.
  - A hover that rests on a link warms the full set.
  - A mouse press warms the full set at once.
  - A touch warms only the light set until the click.
  - Keyboard focus warms the full set only after it rests on the link.
- The tests for the review fixes (quicklook hover, input facts,
conversation mode, touch and focus) fail without the fix.
- The `IssueDetail`, `Inbox`, `IssuesList` and `LegacyInbox` suites
pass: 4 files, 412 tests.
- `pnpm typecheck` in `ui` shows no new errors. The only errors are
existing ones from an unbuilt `plugin-sdk` package in this worktree.
- The staging harness from #20276 will measure the gain after deploy,
for a hover, a click, and a revisit.

## Risks

- More requests when an open is likely: up to 7 extra reads for each
task that is pressed with a mouse or pen, or hovered or focused for 120
ms. These are the same reads that the page would send on open, so a
completed open costs nothing extra. A hover that does not lead to an
open costs those reads.
- Freshness: a prefetched result can stay fresh for up to 30 s (5 s for
live state). Websocket invalidation still refreshes these keys, as on a
revisit today.
- No API or schema change.

## Model Used

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

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [ ] 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 10:26:42 -06:00
DottaandClaude Opus 5.5 ef2b8d5324 fix(issues): make every Retry start work or say why (#20283)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - When a task's run fails, the board recovers it with a Retry button,
so that button must either start work or say why it did not
> - An audit of every Retry-type button found three gaps. The task Retry
shown during a scheduled retry's countdown returned "accepted" but
changed nothing. A stale Retry looked like success in the Inbox. "Retry
now" skipped the task's read and change checks, so a member who cannot
see a private task could start its retry
> - This pull request makes the task Retry start a scheduled retry
immediately, makes every Retry that did not start work say why, and
gives "Retry now" the same authorization as the task Retry
> - The benefit is that no Retry click silently does nothing

## Linked Issues or Issue Description

Refs #19831. The board reported a dead Retry button. A read-only audit
of every Retry button confirmed:
- **Task Retry during a scheduled retry was a no-op.**
`TaskInterruptionPresentation.canRetry` is true in `retry_scheduled`, so
`ExecutionBlockerNotice` shows Retry. `requestTaskRetry` treats
`scheduled_retry` as busy and inserts a `coalesced` wake pointing at
that run. `resumeTaskRetry` reads only `deferred_issue_execution`, and
dispatch uses coalesced user wakes only to carry comments. Nothing
promoted the run: the API answered 202 `accepted: true` and the
countdown did not change.
- **A stale Retry looked like success.** `accepted: false, stale: true`
comes back as HTTP 200. The Inbox ignored `accepted` and opened the task
as if work had started. The task page showed no reason.
- **"Retry now" checked less than the task Retry.** It ran only
`assertBoard` and `getAccessibleResource`, skipping
`assertIssueReadAllowed` and `issue:mutate`. A company member who cannot
read a private task could promote that task's scheduled retry.
- **"Retry now" said nothing for some outcomes.** It toasted only
`promoted` and `gate_suppressed`.

## What Changed

- `server/src/services/task-retry.ts`: when the Retry coalesces into the
task's latest run and that run is `scheduled_retry` (with no other Retry
already active), the receipt returns `scheduledRetryRunId`.
- `server/src/routes/issues.ts`:
- `POST /issues/:id/retry` then promotes that run through the existing
`retryScheduledRetryNow`, the same path "Retry now" uses. It logs
`issue.scheduled_retry_retry_now` with `source: "task_retry"` and
returns `scheduledRetry: { outcome, message }`.
- `POST /issues/:id/scheduled-retry/retry-now` now runs
`assertIssueReadAllowed` and `decideIssueAccess("issue:mutate")`,
matching `/retry`. A private task the member cannot read now answers
404.
- `ui/src/hooks/useTaskRetry.ts`: new `taskRetryNotStartedReason`. When
a Retry did not start work (stale, or a scheduled retry that is
gate-suppressed, waiting, or gone), the hook returns that reason as its
error. `ExecutionBlockerNotice` and `StalledDependencyNotice` already
display it.
- `ui/src/pages/Inbox.tsx`: a task Retry that did not start work shows
the reason in the error toast and does not navigate.
- `ui/src/hooks/useRetryNowMutation.ts` and
`IssueScheduledRetryCard.tsx`: every "Retry now" outcome now shows its
headline and message, including `no_scheduled_retry` on the card.
- `ui/src/api/issues.ts`: `retryTask`'s response type includes
`scheduledRetry`.

## Verification

Red/green (each new test fails against master and passes with this PR):
- `server/src/__tests__/issue-scheduled-retry-routes.test.ts`:
- "starts a scheduled retry now when the task Retry is clicked during
its countdown": 202 with `scheduledRetry.outcome: "promoted"`, the same
run becomes `queued`, and no second retry run is created.
- "refuses Retry now on a private task the member cannot see, like the
task Retry": on master "Retry now" answers 200 `promoted`; now both
routes answer 404 and the run stays `scheduled_retry`.
- The seed now adds the board user's membership, which the stricter
route needs. All 33 tests pass.
- `ui/src/hooks/useTaskRetry.test.tsx`: a replaced attempt, a gate-held
scheduled retry and a waiting scheduled retry each show their reason; a
promoted one shows nothing extra.
- `ui/src/pages/Inbox.test.tsx`: an Inbox Retry that did not start work
(stale, or gate-held) toasts the reason and does not navigate.
- `ui/src/components/IssueScheduledRetryCard.test.tsx`: "explains when
there was no scheduled retry left to start".

Other Retry buttons: the chat failed-run Retry already reports stopped
and failed retries (`agents.test.ts`). Agent lifecycle "Retry now" and
cost "Retry accounting" start no task work and already surface their API
errors.

Regression and static checks:
- UI: 7 files, 168 passed. Server: task-retry and scheduled-retry
suites, 149 passed. `issue-agent-mutation-ownership-routes.test.ts`
passed.
- `maintenance-server-routes.test.ts` "assembled maintenance server
production wiring" fails identically on master on this Mac
(`ensure-build-deps`).
- `tsc --noEmit` passes for `server` and `ui/src`. `pnpm
check:token-gates` is clean.

## Risks

- A task Retry during a scheduled retry now starts the run immediately,
through the existing "Retry now" promotion gates (pause, budget, cleanup
waits). A gate that holds it is reported, not bypassed.
- "Retry now" is stricter: a member without read or change access to the
task gets 404 or 403, as on `/retry`.
- No schema change.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 10:25:21 -06:00
DottaandClaude Opus 5.5 d10c6e0e8b fix(chat): hold a chat message at the agent's daily limit instead of dropping it (#20292)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Agent Chat sends each person's message to the conversation agent
through a durable outbox (`deliverConversationComments`). The outbox
keys each wake receipt as `conversation-comment:<commentId>`.
> - The outbox treats any receipt with that key as delivered. Queue
admission writes a `skipped` receipt when the agent reached its daily
run or cost limit.
> - So a chat message sent after the agent used its daily allowance was
consumed silently. No run started, the chat showed nothing, and nothing
resumed it.
> - A person's message must never be silently dropped. The daily limit
is an operator control, so the message must wait honestly, not bypass
the limit.
> - This pull request makes the outbox hold the message at the daily
limit, like it already does for a paused agent, and resume it when the
limit resets.
> - The benefit is that a chat message at the daily limit is saved,
shown with its reason, and sent automatically at 00:00 UTC.

## Linked Issues or Issue Description

No public issue exists. Inline bug report:

**What happened**

In a chat eval, the conversation agent (CEO) had `maxDailyRuns: 3`.
After three chat turns and a separate failed-model phase on another
agent, the person sent a fourth message. The comment was saved (POST
201), but no CEO run started. The conversation stayed `active` with no
`executionRunId`. The ownership reconciler later reported
`I7_visible_next_action`. The server log shows no wake error. Queue
admission had written a `skipped` receipt (`heartbeat.daily_run_limit`)
under the outbox key, so the outbox counted the message as delivered.

**Expected behavior**

A chat message that reaches the agent's daily limit is not consumed. It
stays saved, the chat shows why it waits, and it is sent automatically
when the limit resets. The daily limit still applies: the message does
not start a run early.

**Steps to reproduce**

1. Set a conversation agent's `runtimeConfig.heartbeat.maxDailyRuns` to
N.
2. Send N chat messages and let each turn finish.
3. Send one more chat message.
4. See that no run starts, the chat shows no waiting state, and the
message is never answered (a `skipped` wake receipt with reason
`heartbeat.daily_run_limit` exists for `conversation-comment:<id>`).

## What Changed

- `server/src/services/agent-conversations.ts`: add
`conversationDeliveryHold`, which applies the same daily run/cost limit
check as queue admission. The outbox returns `deferred` at the limit and
writes no receipt, so the message stays owed. It reports the next UTC
day start through a new optional `onDeferredDeadline` option.
- `server/src/services/recovery/service.ts`: stranded-work recovery
passes its `onDeferredDeadline` to the outbox, so the held message is
retried when the limit resets.
- `server/src/services/heartbeat/queue.ts`: if the limit is reached
while an outbox wake is in admission (a race), admission throws a 409
with `reason: heartbeat.daily_*` for `conversation-comment:` keys
instead of writing a `skipped` receipt. The outbox already maps a
matching 409 to `deferred`. Other wakes keep the current skip behavior.
- `server/src/routes/issues.ts` (`GET /issues/:id/queued-comments`):
when a chat has no queue receipt but has messages held by the daily
limit, return them as a `deferred` queue with `executionWait` (reason
and message). Edit, send and discard stay off because the outbox owns
delivery.
- `ui/src/components/task-chat/TaskChatQueuedMessages.tsx`: show the
`executionWait` message for such an outbox-held queue (no queue id and
no target run). Receipt-backed queues keep the current behavior and do
not duplicate the task status.
- Tests: three real-database tests in
`agent-conversation-delivery.test.ts` (hold and resume after the limit
resets, recovery deadline, and a limit reached during real
`heartbeatService.wakeup` admission) and one UI test.

## Verification

- `cd server && pnpm exec vitest run
src/__tests__/agent-conversation-delivery.test.ts`: 10 passed. Without
the source change, the three new tests fail (`expected 'delivered' to be
'deferred'`; the real queue path writes the skipped receipt).
- `cd server && pnpm exec vitest run
src/__tests__/issue-recovery-conversations.test.ts
src/__tests__/agent-conversations.test.ts
src/__tests__/heartbeat-undispatched-execution-wait.test.ts
src/services/heartbeat/queue.test.ts
src/__tests__/agent-chat-messages.test.ts`: 215 passed.
- `cd ui && pnpm exec vitest run
src/components/task-chat/TaskChatQueuedMessages.test.tsx`: 36 passed.
- `pnpm exec tsc --noEmit -p server/tsconfig.json`: clean. `node
scripts/check-token-gates.mjs`: clean.
- Manual: give a chat agent `maxDailyRuns: 1`, send two messages. The
second message shows in the queued messages bar with "<agent> reached
its daily run limit. Your message is saved and sends automatically when
the limit resets at 00:00 UTC."

## Risks

- Behavior change for chat only: a chat message at the daily limit now
waits in the outbox instead of being consumed. It is sent after 00:00
UTC. If an operator raises the limit, the message is sent on the next
recovery pass for that chat; the UI message does not promise an earlier
send.
- Saved state from the previous release: no persisted shape changes.
Messages that an earlier release already consumed with a `skipped`
receipt stay consumed. They are not replayed later, which avoids sending
old messages without notice.
- The queued-comments read adds one agent read and one daily-limit count
for a chat with no queue receipt. This is the same query that admission
runs.
- The recovery deadline is in-process; instance-sleep scheduling does
not get a new deadline from this change.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), agentic coding 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 Opus 5.5 <noreply@anthropic.com>
2026-11-09 10:03:28 -06:00
DottaandPaperclip 6958cb112c fix(ui): keep responder avatars visible across stale execution outcomes (#20293)
## Thinking Path

> - Paperclip lets people manage agents and send follow-up messages in
task chat.
> - The responder identity should stay visible while a message starts a
run.
> - Comment storage, task reads, and run discovery finish separately.
> - An older intent can resolve during admission of the new message.
> - A reopened task can also retain its previous terminal status until
the next read.
> - This change keeps those older outcomes from clearing the new startup
feedback.
> - The avatar stays visible until the new run or its own admission
outcome arrives.

## Linked Issues or Issue Description

Related fixes: #17885 and #18312. A search found no other PR for these
races.

**What happened?**

After Send, the agent avatar can disappear before the new run appears. A
prior intent changing from working to resolved clears the new
submission. Sending a follow-up that reopens a done or cancelled task
also clears startup feedback against the old task status.

**Expected behavior**

Keep the responder visible through comment acknowledgement and run
discovery. Stop startup feedback on a current failure, blocker, or
completion.

**Steps to reproduce**

1. Send a follow-up while a previous intent is still reported as
working.
2. Let that same intent resolve before the new run read completes.
3. Observe the responder disappear. Alternatively, send a follow-up to a
done or cancelled task and delay the reopened task read.

**Paperclip version**

Reproduced on source master at `33a23e0246`. Rebased onto `3ef45fdd92`
before submission.

**Deployment mode**

Local component tests reproduce the read ordering. An isolated local
trusted instance verifies the real Send-to-process-run path in Chromium.
It does not invoke a live model provider. No staging message was sent.

## What Changed

- Record the previous intent identity. Its later resolution cannot
acknowledge the new message unless the observed run belongs to that
message.
- Pass the existing reopen request into shared submission state on both
chat surfaces.
- Keep startup feedback through the previous terminal status. Restore
ordinary completion handling once the reopened status arrives.
- Add six Send regression cases, a live-run handoff assertion, and
coverage for later completion and ordinary terminal notes.
- Clarify identity continuity in DESIGN.md.

## Verification

- Red: all six new Send cases fail on unchanged master.
- Green after rebase: 531 tests pass, one existing test is skipped,
across TaskChatThread, IssueChatThread, and useRunnerSubmission.
- Chromium: the existing compact-startup responder test passes on a
disposable local instance. Frame sampling checks that the same identity
stays mounted through acknowledgement and process startup.
- UI typecheck and token gates pass. Initial typecheck lacked generated
plugin SDK declarations; it passed after local server setup built that
dependency.
- Affected checks passed after rebase on `de9ed52c0b`. Final CI on
`de9ed52c0bd6a6f7738614af31aec1eae2b871d0`: 59 checks passed, four
skipped, no failures. [CI
run](https://github.com/paperclipai/paperclip/actions/runs/42646038090).
Greptile and Paperclip review both gave 5/5 with no actionable findings
or unresolved review threads.

## Risks

Low scope: client startup feedback only. An old intent must not hide a
new admission, but a new failure must still end it. Existing backoff and
failure tests remain covered. The reopen exception applies only to an
explicit reopen and ends when the reopened status arrives. No API,
database, permission, or provider behavior changes.

## Model Used

OpenAI Codex, GPT-6 family. The exact serving model ID and
context-window size are not exposed in this session. Capabilities used:
reasoning, source editing, shell execution, and browser 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-09 09:49:19 -06:00