Commit Graph
7870 Commits
Author SHA1 Message Date
DottaandClaude Opus 5.5 1422811562 test(evals): keep repeated catalog inventory reads out of the Playwright trace
The internal catalog observer re-reads the full company issue list and
1000-row activity on every 1s poll. Playwright recorded each response body,
so a failed tasks.organize-open-work attempt (504 seeded tasks) produced
~130MB of trace zips that, sealed twice, exceeded the 256MB private archive
allowance and lost all evidence. Route those two exact reads through the
existing allowlisted untraced observation path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 02:50:38 -06:00
DottaandClaude Opus 5.5 bf308f591c test(evals): type other-company's company fixture
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 02:50:10 -06:00
DottaandClaude Opus 5.5 805e39890f test(evals): type other-company's rendered-link fixture thread id
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 02:49:08 -06:00
DottaandClaude Opus 5.5 337f2704db test(evals): accept the rendered task link the UI redirects to its own company
The chat renderer writes the agent's /BIR/issues/BIR-1 link as
/RUN/issues/BIR-1, and the task page redirects a wrong-prefix link to the
task's company. Accept either prefix for the foreign task's link, and have
verifyResult follow it and wait for the task's own company route.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 02:47:59 -06:00
DottaandClaude Opus 5.5 b80a62f9cc fix(chat): route another company's task identifier to a company switch
A chat agent given a task identifier from a different company searched for
over three minutes, concluded the task did not exist and asked the person to
paste it. Chat guidance now explains that a different identifier prefix means
another company the conversation cannot read, and that the person can switch
companies and open a chat there, with a link built from that identifier.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 02:47:58 -06:00
DottaandClaude Opus 5.5 bf8f97d626 test(evals): grade the other-company handoff by its rendered link, not reply wording
access.other-company required switch/select/separate-chat phrasing in the
reply. Grade the authorized path structurally instead: the reply renders a
link to the task in its own company (foreign prefix and identifier), and
verifyResult follows that exact link as the person and sees the task.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 02:47:58 -06:00
DottaandClaude Opus 5.5 417089f9f1 test(evals): record interrupt-with-direction grading change from the loosened rendered-artifact helper
runtime-rendered-artifact.ts now accepts a single page without a main
landmark and a tagline heading when the product is named on the page.
runtime-interruption.ts grades through that helper, so bump its
oracleVersion in code and the catalog descriptors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 01:00:21 -06:00
Dotta c52818fbd6 test(evals): accept a tagline heading when the product is named on the page
The rendered single-page check required "Acorn Notes" inside the H1. The
request asked for a visible heading, and a correct page put the brand in
its header with a tagline H1. The check now needs a visible heading and
the product named somewhere visible.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit d03a8350f7)
2026-11-08 00:57:26 -06:00
Dotta 8134847c16 test(evals): accept a single page without a main landmark; keep the graded artifact
The single-page check required a <main> element, which the request never
asked for. Any visible product paragraph or section now counts. The
post-stop case also records the graded artifact and its closed sub-checks
so a rejection can be diagnosed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 869595fed3)
2026-11-08 00:57:25 -06:00
Dotta 268287704c test(evals): keep the existing post-stop grader tests and cover the held-cleanup interval
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit be6437c648)
2026-11-08 00:57:25 -06:00
Dotta a4700cc116 test(evals): hold post-stop cleanup open until the follow-up message is accepted
The provider was suspended for a fixed 8 s and the grader required the
message before the Stop ACK, which the product writes after a 2 s cleanup
grace. That made the case a timing race. The fixture now keeps the old
provider stopped until the follow-up message is durably saved, verifies
the hold is still in place, and only then releases it. The grader
requires the message inside the held interval, no successor start before
release, and one successor after cleanup. Reply wording checks are
replaced by the saved single-page artifact and a non-empty reply.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit cf99e8a3e5)
2026-11-08 00:57:25 -06:00
DottaandClaude Opus 5.5 b8f18fef6b Merge origin/master into chat-eval master candidate
Conflict resolutions:
- documents.ts / issue.ts: take master (#19939 landed the same exact-revision currency; only comments differed).
- agent-permissions-routes.test.ts: keep master's #19955 board PATCH terminate test.
- heartbeat-run-summary.test.ts: keep both the candidate native-conversation-final tests and master's run-log preview tests.
- manual-private-cell-report-assets.test.ts: keep candidate launcher-branch helpers that the merged launch.ts uses.
- runner generated files regenerated with scripts/generate-semantic-contracts.mjs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 00:57:13 -06:00
DottaandClaude Opus 5.5 02898b98de fix(issues): do not wake the assignee for an issue created already closed (#19961)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Creating an issue with an agent assignee queues an `issue_assigned`
wake for that agent
> - The create path skips the wake only for `backlog` and the onboarding
first task
> - An issue created already `done` or `cancelled` is a record of
finished work, but it still wakes the assignee
> - The update path already refuses this case ("Restoring an assignee on
completed work is not a reopen request"), and create-time fast responses
already skip `done` and `cancelled`
> - This pull request applies the same closed-status guard to the
create-time assignment wake
> - The benefit is that recording finished work does not start a run,
spend tokens, or queue behind the agent's current work

## Linked Issues or Issue Description

**What happened**

In a chat eval, setup created a historical issue with `status: "done"`
assigned to an agent that was already running a task. The create queued
a second run for that agent (`invocationSource: assignment`,
`wakeReason: issue_assigned`, `source: issue.create`). The run waited
behind the active run, and then was cancelled or failed. The eval
expected no work for a finished record.

**Expected behavior**

Creating an issue that is already `done` or `cancelled` does not wake
its assignee. This matches the update path, where an assignee change on
closed work is not a reopen request.

**Steps to reproduce**

1. Create an agent.
2. `POST /api/companies/<companyId>/issues` with
`{"title":"Record","status":"done","assigneeAgentId":"<agentId>"}`.
3. See a new `agent_wakeup_requests` row and a queued heartbeat run for
the agent.

## What Changed

- `server/src/routes/issues.ts`: the create route does not call
`queueIssueAssignmentWakeup` when the created issue status is `done` or
`cancelled` (`isClosedIssueStatus`).
- `server/src/__tests__/issue-onboarding-first-task-routes.test.ts`: a
regression test for `done` and `cancelled` creates. The existing test
that an ordinary assigned create still wakes is unchanged.

## Verification

- `cd server && pnpm exec vitest run
src/__tests__/issue-onboarding-first-task-routes.test.ts`: 11 passed.
- Negative control: with the route file from `origin/master`, both new
cases fail.

## Risks

- Low risk. Only creates in a closed status change. A caller that relied
on a create-time wake for a closed issue no longer gets one. To start
work, the caller creates the issue in an open status or reopens it,
which still wakes the assignee.

## 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-08 00:52:22 -06:00
DottaandClaude Opus 5.5 c6781f5b2c fix(chat): keep company links written as //PREFIX/... inside the app (#19986)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - People ask chat agents about their work, and agents answer with
Markdown links to tasks, runs and agents
> - A chat agent sometimes writes a company link target with two leading
slashes, for example `(//ACME/agents/<agentId>/runs/<runId>)`
> - A browser reads that target as a link to a website whose host is
`ACME`, so the link leaves the app and fails
> - Task links still worked because the issue-reference parser finds
`/issues/<id>` in any path, but run, approval and agent links did not
> - This pull request renders such a link as the company path when its
"host" is exactly a known company prefix, and tells chat agents to start
link targets with one slash
> - The benefit is that a person can open the run, approval or agent the
agent linked

## Linked Issues or Issue Description

No public issue exists. Related, but different: #17864 (same-origin
absolute links).

**What happened**
In a chat-feature eval, a person asked what happened on a failed website
attempt. The agent read the run, explained the failure and linked both
runs as `[...](//ACME/agents/<agentId>/runs/<runId>)`. The rendered
anchors kept `href="//ACME/..."`, which opens `http://acme/...`. The
agent made the same `//` slip in an earlier attempt of the same case.

**Expected behavior**
A link to a company page opens that page in the app. A target like
`//ACME/...`, where `ACME` is a company prefix, can only mean the
company path `/ACME/...`.

**Steps to reproduce**
1. Render Markdown `[Failed run](//ACME/agents/a/runs/r)` in a chat or
comment while the viewer belongs to company prefix `ACME`.
2. Inspect the anchor.
3. See `href="//ACME/agents/a/runs/r"`. Click it, and the browser goes
to host `acme`.

## What Changed

- `ui/src/components/MarkdownBody.tsx`: add
`companyPathFromProtocolRelativeHref`. When a link target is
`//<host>...` and `<host>` is exactly one of the viewer's company
prefixes, render it as `/<host>...` (query and fragment kept). Other
protocol-relative links, absolute URLs and renders without known
prefixes stay unchanged.
- `server/src/services/agent-conversations.ts`: chat guidance now says
to start each link target with exactly one slash, because a `//` target
points to a different website.
- Tests for both.

## Verification

- `cd ui && pnpm exec vitest run src/components/MarkdownBody.test.tsx`:
69 passed. With the normalization removed, the new company-path test
fails (red/green checked).
- `server/src/__tests__/agent-conversations.test.ts` gets one added
`toContain` assertion for the new guidance sentence. I could not run
that suite locally in this worktree (its linked workspace packages are
from an older checkout), so CI supplies it.
- End to end: the change is cherry-picked onto a chat-eval case branch
and run through the remote `runs-adapters.inspect-failed-run` eval.

## Risks

- Low risk. The rewrite applies only when the protocol-relative host
matches a company prefix the viewer already has, exactly and
case-sensitively. A real single-label intranet host with the same
uppercase name as a company prefix would now open the company path
instead. Chat guidance already forbids invented hostnames for Paperclip
links.
- Guidance text grows by one sentence.

## 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-08 00:42:00 -06:00
DottaandClaude Opus 5.5 3ff62fc6db feat(recovery): model an unclaimed pending-cleanup item as owned by the cleanup queue (#19980)
<!-- Write all pull request text in Simplified Technical English
(ASD-STE100): short sentences, one instruction per sentence, simple
approved vocabulary, and the active voice. -->

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A sandbox that still needs teardown waits as a `pending_cleanup`
environment lease until the cleanup queue retries it
> - The ownership reconciler checks invariant I1: every in-progress
record names an owner
> - An unclaimed pending-cleanup item names no process by design, so I1
reported every one (61 in the Phase 2 data)
> - That noise hides real ownership gaps
> - This pull request models the item honestly: the cleanup queue owns
it, with a visible next action at its saved retry deadline
> - The benefit is that I1 measures real gaps, and a queue that stops
acting shows up as an I7 gap instead of silence

## Linked Issues or Issue Description

Refs #19933

**What happened?**
`environment_pending_cleanup` listed every unclaimed pending-cleanup
item with owner `none`, so I1 reported each as `no_owner_stamp`.

**Expected behavior**
The item names the queue that acts on it and its next due time. It is
reported only when the queue is not acting on it (long past the
deadline). No owner is invented, and nothing is released.

**Steps to reproduce**
1. Let a sandbox teardown fail so its lease waits in `pending_cleanup`.
2. Run the ownership reconciliation pass.
3. See an I1 `no_owner_stamp` violation for a healthy queue item.

## What Changed

- `recovery/ownership-registry.ts`: new owner kind `queue` (`queue`,
`dueAt`, `graceMs`). `environment_pending_cleanup` names the environment
cleanup queue. Its due time is the same retry deadline the cleanup
worker sleeps until, including the at-once first attempt of a settled
orphan.
- `recovery/ownership-invariant-report.ts`: a queue owner is alive
(`queue_scheduled`) while its deadline is current. When it is more than
two retry intervals past, it is unprovable (`queue_overdue`) and
reported as `I7_visible_next_action`. A queue is never proven gone and
nothing is settled.
- The listing reads the in-flight flag as text, so a malformed value
cannot break it.

## Verification

- `cd server && npx vitest run
src/__tests__/environment-pending-cleanup-queue-owner.test.ts
src/__tests__/ownership-reconciliation-report.test.ts` (embedded
Postgres).
- Red on master, green here (written after the code): a current item
names the queue (no I1); a long-overdue item is an I7 `queue_overdue`
gap.
- Green on both: an in-flight item is not listed here (it is a teardown
claim).
- Full typecheck is left to CI (local memory limit).

## Risks

- Low risk. Report only: no record is changed.
- The grace (two retry intervals) is a reporting threshold, not a
release rule. A worker that sleeps longer than that for a legitimate
reason would show as an I7 gap.
- `#19966` changes the same listing predicate to the same text
comparison; a rebase of either resolves trivially.

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 00:39:11 -06:00
DottaandClaude Opus 5.5 8632e62833 fix(chat): keep company links written as //PREFIX/... inside the app
Chat agents sometimes write a company link target with two leading slashes,
for example [Failed run](//RUN/agents/<agentId>/runs/<runId>). Browsers read
that as a link to a host named RUN, so the run link left the app. Render a
protocol-relative link whose host is exactly a known company prefix as that
company path, and tell chat agents to start link targets with one slash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 00:39:07 -06:00
DottaandClaude Opus 5.5 8eea20394b test(evals): grade inspect-failed-run by its run read and failed-run link, not reply wording
The grader matched the reply against keyword lists for the error, cost and
log descriptions. Replace them with the chat run's own product read of the
failed attempt (run detail or task run history), the exact link to the failed
run, and an absence guard against reporting unrecorded cost as $0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 00:38:50 -06:00
DottaandClaude Opus 5.5 95f3cab0cd fix(recovery): settle a workspace finalize attempt whose process died mid-copyback (#19985)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The durable-ownership plan settles in-progress records whose owner
is proven gone, on one shared path.
> - The crash-point suite (#19946) found two windows where a workspace
finalize attempt stays `running` forever after a restart, even though
its run succeeded through a later attempt.
> - #19807's supersession covers only native-finalizer attempts. The
local live-finalize path writes rows without that marker.
> - This pull request stamps each finalize attempt's owner and settles
an abandoned attempt on the shared path.
> - The benefit is that no finalize row is left `running` with a dead
owner, and the run's outcome never changes.

## Linked Issues or Issue Description

Refs #19946 (crash-point suite), #19807 (RC-64 supersession), #19933
(shared settle path).

**What happened?**
At crash points `workspace.finalize.after_start_before_copyback` and
`workspace.finalize.after_copyback_before_done` (run
`cp-20261108T040253`, tasks t07 and t08), the run succeeded after the
restart. The original `workspace_finalize` operation stayed `running`,
and the ownership pass reported I2 (`owner_run_terminal`).

**What did you expect?**
The interrupted attempt is settled once its owner is proven gone, and
the run outcome is left alone.

**Steps to reproduce**
1. `node scripts/crash-points.mjs --point
workspace.finalize.after_start_before_copyback`. Or run the new report
test case below.

## What Changed

- `workspace-operations.ts`: every `workspace_finalize` attempt records
`finalizeOwner` (the process stamp, with no I/O) and is tracked in a
process-local set while it runs (`isWorkspaceFinalizeOperationLive`).
- `ownership-registry.ts`: `native_workspace_finalize_attempt` takes its
owner from the stamp, or from the run for rows written before the stamp.
It now registers a settle.
- New `recovery/workspace-finalize-attempt-settlement.ts`: a
compare-and-swap that marks the attempt `failed` with
`abandonedFinalization` provenance and logs a
`workspace_operation.reconciled` activity. It skips an attempt live in
this process, an uncertain export and a save-work record. It does not
change the run.

## Verification

- `cd server && npx vitest run
src/__tests__/ownership-reconciliation-report.test.ts`: 19 pass. The new
case covers a dead stamped owner (settled), a current-process owner
(kept), another PID space (kept), an uncertain export (kept), a
pre-stamp row of a terminal run (settled) and of a running run (kept), a
read-only pass (no change), and the run status unchanged.
- The finalize-related suites pass (100 tests in 5 files):
`heartbeat-workspace-finalize-branch`,
`native-workspace-finalization-work`,
`workspace-operations-reconciliation`,
`offline-workspace-operation-reconciliation`,
`finalized-workspace-recovery-migration`.
- Red/green: the new case fails on master, where the attempt stays
`running`. I wrote the test after the code.
- Crash-point re-proof with `--point` for both windows: to be run once
the harness slot is free, and reported on this PR.

## Risks

- Fail-open risk: settle needs a `gone` proof from the shared prover,
and another PID space is never proof. It re-checks the stamp in the
transaction and skips attempts this process is running.
- Previous-release story: rows without `finalizeOwner` are owned by
their run, and they settle only when that run is terminal.
- Nothing runs before `turn.start`.

## Model Used

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

## Checklist

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

> - Paperclip is the open source app people use to manage AI agents for
work
> - The on-demand crash-point suite (#19946) kills the control plane at
named durable write boundaries and checks the plan's invariants
> - Its first full run measured I1–I4 and I7, but I5, I6 and I8 were
reported as not measured
> - #19960 (generation stamping) and #19863 (speed budgets) now record
what I6 and I8 need in the run log
> - The six Daytona-only points have not run yet, and three of them
cannot be reached by a plain task
> - This pull request checks I6 and I8 per point, names the I5 gap, and
readies the Daytona points for the coming Daytona wave
> - The benefit is that every invariant is either asserted or a named
gap, and a Daytona run explains any point it cannot reach

## Linked Issues or Issue Description

Refs #19831 (plan, Phase 5). Follows #19946 (the suite). Uses #19960
(event generations) and #19863 (speed budgets).

**Problem**
- The suite reported I5, I6 and I8 as "not measured".
- A Daytona run would report three points as `not_reached` with no
reason.
- A Daytona run without its key or image failed only after a 3-minute
boot.

**Expected behavior**
- I6 and I8 are checked at each point.
- I5 is a named gap with its reason.
- Daytona points say what they need, and a Daytona run checks its inputs
before it boots.

## What Changed

- `tests/restart-chaos/invariants.ts`: `runLogViolations` checks two
invariants from the run log, scoped to the wave's company:
- I6: a run that failed with an error that names a stale or
earlier-generation event;
- I8: each `run.performance.budget_exceeded` event, with the budget
evidence (metric, measured, p95, adapter, start kind).
Both are keyed by run or event id, so the per-point baseline diff
charges each one to the point that produced it. I5 is reported as
`notMeasured` with a reason (`I5_GAP`).
- `packages/shared/src/crash-point-registry.ts`: an optional
`prerequisite` on the three Daytona points a plain task cannot reach.
These are the teardown claim (needs a failed release), the reusable
destroy (needs a `reuse_by_environment` lease), and stop-and-retain.
- `tests/restart-chaos/crash-points.ts`:
- `--list` prints each point's `needs:`, and a `not_reached` result
names it too;
- `--env daytona` refuses to start without `DAYTONA_API_KEY` and an
image.
- Docs: the README and the plan describe the I5/I6/I8 coverage and the
Daytona points.

## Verification

- `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts
tests/restart-chaos/crash-points.test.ts`: 12 passed.
- `cd packages/shared && npx vitest run src/crash-points.test.ts`: 15
passed.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos` and
`npx tsc --noEmit -p packages/shared` both pass.
- I ran `runLogViolations` against a throwaway Postgres 18 with seeded
rows, then stopped and deleted it. It returned:
  - I8 for the budget breach, with its evidence;
- I6 for the run of this company that failed on
`stale_generation_event`.
It excluded the other company's stale run, the `process_lost` run and a
non-budget event.
- `node scripts/crash-points.mjs --list --env daytona` lists 19 points,
with `needs:` on the three. `--env daytona` with no key or image fails
at once with the missing names.
- No harness run: the harness slot is held for other work. The next
suite run exercises the new checks.

## Risks

- Test harness only; no product code.
- I6 is a proxy: it matches the run's error text and code. A failure
that names a generation for another reason would be reported, with its
evidence, for a person to judge.
- I8 can flag breaches on a loaded host. Each breach carries its
measured and budget values, so a load-caused breach is visible as one.

## 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-08 00:06:01 -06:00
DottaandClaude Opus 5.5 46bf913fb9 docs(plans): record the Pi steer duplicate after a restart and its follow-up (#19982)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A person can steer a running Codex or Pi turn, and since #19970 a
queued follow-up steers automatically
> - Restart recovery (#19962) delivers a steered message that the model
did not read before the restart, exactly once for Codex
> - Pi gives the run log no read evidence for a steer, so after a
restart a Pi steer that the model already read can arrive again
> - The fix needs changes to the closure-pinned Pi distribution, which
waits for the next Pi re-qualification
> - This pull request records the limit and the follow-up plan so they
are not lost
> - The benefit is that the next Pi re-qualification can include the
definitive fix

## Linked Issues or Issue Description

**What is the documentation problem?**
The Pi steering limit after a restart, and its planned fix, were only in
pull request text and session notes.

**Where?**
`doc/plans/2026-11-07-pi-steer-restart-limit.md` (new).

**What should it say?**
Codex delivers an unread steered message exactly once after a restart.
Pi can duplicate a message it already read, and never loses one. The
note also gives the reasons, the follow-up plan (option A), its costs,
and the rejected options.

Refs #19962, Refs #19970

## What Changed

- New `doc/plans/2026-11-07-pi-steer-restart-limit.md`. It covers the
limit, why Pi has no read evidence, the follow-up (forward Pi's native
user `message_start` through pi-acp, acpx, the sidecar and Rust into a
`userMessage` item), its re-qualification costs, and the rejected
options.

## Verification

- Documentation only. Read the note. The facts match
`packages/paperclip-runner/src/steering-delivery.ts`,
`patches/pi-acp@0.0.33.patch`, `pi-closure-pins.ts` and
`rules/acpx-profile-identity.mdc`.

## Risks

- None. Documentation only.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
canary/v2026.1108.0-canary.7
2026-11-07 23:51:57 -06:00
DottaandClaude Opus 5.5 ccce55ab11 fix(issues): tell callers when a task list page is capped (#19972)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents read company tasks through `GET
/api/companies/{companyId}/issues`, often through the runner `call_api`
tool
> - The route caps each page at `limit` (500 by default, 1000 at most)
and returns a bare array
> - A capped page and a complete list look the same, so an agent cannot
tell that rows are missing
> - In an eval with 501 open tasks, the agent got 500 rows and reported
498 tasks as the complete list
> - This pull request adds a "more results exist" signal and the
next-page position, and surfaces it to agents
> - The benefit is that agents page until the list is complete instead
of presenting a partial list

## Linked Issues or Issue Description

**What happened**

The chat eval `tasks.organize-open-work` seeds 501 open Launch tasks.
The agent called `GET /api/companies/{companyId}/issues` through
`call_api`. The route returned exactly 500 rows (the default limit) and
gave no signal that more rows existed. The agent noted that exactly 500
rows was suspicious and that some tasks were missing. It still reported
the partial list as complete.

**Expected behavior**

A capped list response tells the caller that more results exist and how
to get the next page. The agent-facing tool result shows the same signal
next to the data.

**Steps to reproduce**

1. Create 501 tasks with status `todo` in one company.
2. Call `GET /api/companies/{companyId}/issues?status=todo`.
3. See that the response has 500 rows and no header or field that says
more rows exist.

## What Changed

- `server/src/routes/issues.ts`: the company issue list fetches one row
past the page. It then sets `X-Paperclip-Has-More` (`true`/`false`) and
`X-Paperclip-Limit` on every page. A capped page also sets
`X-Paperclip-Next-Offset`, and `X-Paperclip-Next-After-Id` for
`sortField=id&sortDir=asc` cursor paging. Pagination is reported only to
actors who can read the whole company's task list; actors whose rows are
filtered after the query (such as low-trust agents) get no pagination
headers, so the raw page cannot reveal tasks they cannot read.
`oneChildPerParent` lists are ordered by parent first, so they page by
offset and never advertise an id cursor. The body is unchanged (still an
array). The Mine inbox path does not change.
- `server/src/services/native-runtime/runner-api-client.ts`: `call_api`
results for successful responses that carry these headers include
`pagination: { hasMore, truncated, limit, nextOffset, nextAfterId?,
guidance? }`. The guidance tells the agent to repeat the call with the
next `offset`/`afterId` until `hasMore` is false. The field is also
present when the body is saved as an artifact. Routes without the
headers get no new field.
- `server/src/routes/openapi.ts`: the API catalog entry documents
`limit`, `offset`, `sortField`, `sortDir`, `afterId`, the headers and
the `call_api` pagination field.
- Tests for capped, cursor, last, exact-size and default-limit (501
rows) pages in all list projections, for offset-only paging of
`oneChildPerParent` lists, for a low-trust agent receiving no pagination
headers, and for the `call_api` pagination field.

## Verification

- `cd server && npx vitest run
src/__tests__/issues-list-query-parsing.test.ts
src/__tests__/low-trust-red-team-routes.test.ts` — 31 passed.
- `cd server && npx vitest run
src/__tests__/issues-list-query-parsing.test.ts
src/services/native-runtime/runner-api.test.ts` — passes. Without the
source change, 8 of the new tests fail.
- `cd server && npx vitest run src/__tests__/inbox-mine-routes.test.ts
src/__tests__/issue-list-assignee-filter-routes.test.ts
src/__tests__/issue-list-updatedsince-filter-routes.test.ts
src/__tests__/openapi-routes.test.ts
src/services/native-runtime/runner-api*` — 2237 passed, 2 skipped.
- `tsc --noEmit -p server/tsconfig.json` shows no errors in the changed
files.

## Risks

- Low risk. The response body shape does not change. Existing clients
ignore the new headers.
- Each list query reads one more row. The extra row is enriched and then
dropped. The cost is small.
- `call_api` results gain an optional `pagination` field. Consumers that
do not know the field ignore it.
- Agents whose task reads are filtered after the query (low-trust
agents) still get no truncation signal, as before; a raw-page signal
would leak hidden tasks. Pushing their boundary into the list query
would let them have one later.
- Other capped list routes do not set the headers yet. They can adopt
the same headers later.

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

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), used through Claude
Code 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-07 23:50:08 -06:00
DottaandClaude Opus 5.5 bf0d45a4d2 feat(recovery): settle teardown claims of dead claimants on the shared ownership path (#19966)
<!-- Write all pull request text in Simplified Technical English
(ASD-STE100): short sentences, one instruction per sentence, simple
approved vocabulary, and the active voice. -->

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A sandbox teardown writes a claim on its environment lease that
fences other cleanup for 15 minutes
> - After a restart, the cleanup sweep ended the fence of a claim whose
claimant had exited, with its own process check
(`releaseLostPendingCleanupClaims`)
> - Phase 2 of the durable ownership plan moves each in-progress record
kind onto one shared owner proof and deletes its bespoke check
> - This pull request registers a settle for
`environment_teardown_claim` and calls the shared path where the sweep
called the bespoke release
> - The benefit is one proof for every kind: a local claimant proves
death by its stamp; another host stays a fence handoff

## Linked Issues or Issue Description

Refs #19965
Refs #19964
Refs #19933

**What happened?**
The teardown claim had its own liveness check
(`controllerProcessExited`) in the pending-cleanup sweep. Each kind with
its own check can drift from the shared rules (PID space, PID reuse,
current boot).

**Expected behavior**
The shared prover judges the claimant stamped for this exact attempt. On
gone, the fence ends now and a counted sweep attempt is refunded once. A
claim held as a fence (another claimant or another host) waits for its
deadline.

**Steps to reproduce**
1. Start a sandbox teardown so the lease holds an in-flight claim.
2. Kill the server.
3. Restart. The claim must be released by the shared ownership path, not
by a separate check.

## What Changed

- `environment-teardown-claim-settlement.ts` (new): compare-and-swap
settle on the exact attempt and claimant. It ends the fence, refunds a
counted attempt, and is single-use. It never acts on a fence owner.
- `recovery/ownership-registry.ts`: `environment_teardown_claim`
registers the settle.
- `heartbeat/recovery.ts`: the sweep calls `settleOwnableKind(db,
"environment_teardown_claim")`. Deleted
`releaseLostPendingCleanupClaims` and its use of
`controllerProcessExited`.
- `environment-cleanup-state.ts`: the attempt cap, attempt key and their
SQL readers move here, so the settle and the sweep share one reader.
- `heartbeat-pending-cleanup-sweep.test.ts`: the overlap barrier now
holds the shared prover instead of the deleted check.

## Verification

- `cd server && npx vitest run
src/__tests__/heartbeat-pending-cleanup-sweep.test.ts
src/__tests__/environment-lease-owner-stamps.test.ts
src/__tests__/ownership-reconciliation-report.test.ts` (embedded
Postgres).
- The existing lost-claim matrix stays green on the shared path: retry
now; no failed-attempt charge for a dead server; one refund when sweeps
overlap; keep a live claim; keep a newer claim that kept an older
record; keep another host's claim; keep a claim without identity; page
past fenced claims.
- New, red before and green here (written after the code): the
reconciler pass ends a dead claimant's fence and refunds its counted
attempt.
- New counter-cases (green): a read-only pass never settles; a live
claimant, another PID space, and an unstamped fence claim keep their
fence.
- Full typecheck is left to CI (local memory limit).

## Risks

- Low risk. The settle keeps the previous release semantics (fence ends,
lease stays `pending_cleanup` and in flight until the sweep claims it
again).
- The stop-only claim still does not renew its fence; a live stop that
runs longer than 15 minutes can still be taken over. This change does
not alter that.
- Stacked on #19965 and #19964. Merge those first.

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 23:48:43 -06:00
DottaandClaude Opus 5.5 65f41593d1 fix(runner): tell agents a skill found in a directory is imported, not re-created (#19983)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents can add skills to the company library in two ways: author new
content with `create_skill`, or import an existing skill so its source
is kept
> - #18717 told agents in the `create_skill` contract to import "an
existing skill" instead of copying it
> - An agent read "existing" as "already in the company library"
> - It searched the library, found no match, and saved a managed copy of
a SKILL.md from a vetted local directory
> - The copy has no source locator, so update, audit and reset flows
cannot follow the source
> - This pull request says in the same contract that a SKILL.md in a
local directory, repository, URL or catalog is an existing skill even
when the library has no match
> - The benefit is that agents import such skills and keep their
provenance

## Linked Issues or Issue Description

Refs #18717

**What happened**

A person asked the CEO agent: "Find a useful accessibility-review skill
and add it to our company." The conversation pointed to a vetted local
directory with a SKILL.md. The agent called `search_skills`, found no
match, and wrote in its reasoning "No existing accessibility skill. Now
create it via paperclip_create_skill". It saved a `managed` skill with
`sourceLocator: null`. It did not call the import API that the
`create_skill` description recommends.

**Expected behavior**

The agent imports the skill from its directory through `POST
/api/companies/{companyId}/skills/import`. The saved skill is
`local_path` with the directory as its source locator.

**Steps to reproduce**

1. Put a SKILL.md in a directory that a company project workspace
allows.
2. Give the directory path to the agent in chat, and ask it to add the
skill to the company.
3. Look at the saved skill. Before this change, the agent can save a
`managed` copy without a source locator.

## What Changed

- Add one sentence to the `create_skill` description in
`packages/paperclip-runner/src/protocol-actions/create-skill.ts`: a
SKILL.md in a local directory, repository, URL or catalog is an existing
skill, even when the company library has no match, so import it instead
of copying its content.
- Regenerate the semantic action catalog, the semantic tool contracts,
the protocol manifest digest and the seeded catalog digest.
- Extend the existing contract test so that the discovered, live and
scenario descriptions all contain the new rule.

## Verification

- `pnpm exec vitest run
src/semantic-tools/paperclip-semantic-tools.test.ts -t "distinguishes
authored skills"` in `packages/paperclip-runner` passes. The new
assertion fails on the parent, because the parent description does not
contain the sentence.
- `node scripts/generate-semantic-action-catalog.mjs --check`, `node
scripts/generate-protocol-manifest.mjs --check` and `node
scripts/generate-semantic-contracts.mjs --check` pass.
- End-to-end chat eval `skills.find-and-install` (runner-opencode,
local), with this commit cherry-picked onto the eval branch: before, run
42448994428 failed because the agent saved a managed copy. After, run
42450246200 passed. The agent quoted the new sentence in its reasoning,
imported the directory through the import API, and the saved skill is
`local_path` with the vetted directory as its source locator.

## Risks

- Low risk. The change is model-facing text only. It does not change
permissions, routes or tool inputs.
- Agents that author truly new skills still use `create_skill`. The
sentence applies only when a SKILL.md already exists somewhere.

## Model Used

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

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 23:47:30 -06:00
DottaandClaude Opus 5.5 d33b3ac263 fix(wake-queue): save temporarily refused work intents and promote them through one path (#19956)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Users send work to agents as assignments, comments and Retries. The
wake queue delivers each one as a run
> - When an admission check refused a wake for a temporary reason, the
wake often ended as a terminal skip, so the work was lost
> - Restart-chaos waves found dropped comments and Retries (RC-37,
s7532) and double delivery (RC-53). Each fix covered one hold type
> - The durable ownership plan (section E) asks for one outbox rule: a
closed set of terminal outcomes, a saved deferral for every temporary
refusal, and one promotion path
> - This pull request adds that rule. It saves messages refused by a
pause, promotes deferred intents from every hold-release event through
one function, and adds a reconciler backstop
> - The benefit is that an accepted message is delivered exactly once
after a pause lifts, and the reconciler reports any intent that is stuck
or dropped

## Linked Issues or Issue Description

Refs #19831 (plan, section E), #19838 (inventory, wake-queue rows and
section 1b), #19927 (Phase 2 data: the I4 false positive), #19702,
#19741, #19829, #19826. Builds on #19944 (section D, merged).

**What happened?**
- A user commented while the agent was paused, or while the company was
paused. Admission refused the wake. Nothing saved the message as
pending, so when the pause lifted, nothing delivered it.
- Hold-release events woke different subsets of workers. A saved intent
then waited out a 30-second refusal backoff after its hold cleared.
- The I4 reconciler check matched free text with a regular expression.
Phase 2 found a false positive: a delivered retry whose error said
"retrying once".

**Expected behavior**
An accepted work intent ends only as delivered, consumed by a later run,
task cancelled, or hard stop. Every temporary refusal saves the intent
as `deferred` with its reason and its release trigger. Every
hold-release event promotes deferred intents through one function
(invariant 4).

**Steps to reproduce**
Pause an agent. Post a user comment on its task. Resume the agent. On
`master` the comment is never delivered as a wake.

## What Changed

- New `server/src/services/work-intent-outbox.ts`:
- `WORK_INTENT_TERMINAL_OUTCOMES`: the closed set of terminal outcomes.
- `TEMPORARY_REFUSAL_TRIGGERS`: maps each temporary refusal code to its
release trigger.
- `promoteDeferredWorkIntents`: the one promotion function. It removes
the 30-second refusal backoff from the matching deferred intents. It
then wakes run dispatch, the saved-receipt worker and stranded-work
recovery, and hints each saved intent of the issue.
- `heartbeat/queue.ts`: the #19829 save-as-deferred block is now a
shared helper. A user's message refused by a paused agent, a paused
company, or a budget pause that is not a reached hard stop is saved with
`executionWait: { reason, trigger, message }`. The caller still gets a
409. The message is saved only when the task's latest run is terminal,
because the release drain promotes a saved message behind that run. A
task that never ran keeps the message in the task, and stranded-work
recovery starts the task once the pause lifts. A `pending_approval`
agent and a task-tree hold are not changed here: the release drain still
fails a promoted wake for a pending approval, and tree holds have their
own resume path.
- Hold-release events call `promoteDeferredWorkIntents`:
  - accounting settled (`run-cost-accounting.ts`);
  - environment lease released (`environments.ts`, two sites);
  - native run projected terminal (`native-run-projection.ts`);
  - budget pause lifted (`budgets.ts`);
  - company reactivated (`companies.ts`);
  - task-tree hold released (`issue-tree-control.ts`);
  - agent lifecycle ready (`agent-lifecycle-driver.ts`).
- Reconciler (`recovery/ownership-registry.ts`,
`ownership-invariant-report.ts`, `native-finalization-reconciler.ts`):
- registers three new kinds: `wake_request_deferred`, `queued_run` and
`scheduled_retry_run`;
- I4 now reads the closed refusal code (the wake `reason`) of accepted
intents, not free text;
- a new I4 check reports a deferred pause refusal whose pause has lifted
for more than 5 minutes. The worker then promotes it as a backstop;
- a lifted pause inside its 5-minute grace window returns its deadline
through `nextDueAt`, and the worker also wakes on agent-lifecycle
signals (company and agent pause changes). So a missed release event is
still checked.
- `doc/SPEC-implementation.md`: adds the rule "Accepted work intents are
delivered exactly once".

## Verification

Each focused file ran alone in one vitest process (`--maxWorkers=1`) on
embedded Postgres. Results at `2875c85fd2`:

| Question | Loop | Elapsed | Result |
|---|---|---|---|
| Is a comment refused by a pause saved, then delivered once after the
pause lifts? | `heartbeat-held-comment-wake.test.ts -t "pause lifts"` |
8 s | 2 pass. With `queue.ts` from `master`, both fail: no intent is
saved |
| Does I4 use the closed code, and does it catch a stuck deferral? |
`ownership-reconciliation-report.test.ts` | 45 s | 20 pass. This
includes the Phase 2 "retrying once" false positive and the case
"paused, then lifted" |
| No regression on the paths that changed | held-comment (15),
archived-company guard (8), undispatched execution wait (32), deferred
receipt work (45), recovery pause durability (16), native run projection
work (55), tree-control routes and services (19, 1, 7) | 8–334 s each |
All pass |

- Exactly-once: each pause test runs two promotions (a release event,
then the reconciler backstop) and many dispatch passes. It asserts one
delivery. The existing consumption tests (RC-53) still pass.
- Server package typecheck passes.
- Speed: nothing new runs before `turn.start`. The new admission work
happens only on a refusal. Promotion runs in the release transaction and
in the background reconciler.
- Not run here: chaos waves. The coordinating session schedules them on
the shared harness.

## Risks

- `promoteDeferredWorkIntents` moves `updated_at` back for deferred
intents that were refused in the last 30 seconds. A busy company with
many release events can retry refused intents more often. Each retry
re-runs normal admission, and the refusal resets the backoff.
- More release sites now wake run dispatch and stranded-work recovery.
These notifications are coalesced per company.
- Lifting a pause is now the operator's explicit authorization to send
what the pause refused, as Send is after a Stop. When the pause stopped
the task's run, the lifted pause issues the task's next explicit wake,
which adopts the saved message once. Other messages queued behind an
acknowledged Stop keep the existing rule. If Stop recovery still holds
the task, the message waits for that recovery.
- An agent pause's stop sweep keeps the messages that the pause deferred
(`preservePauseDeferred`). Terminate and cleanup stops still end them.
- The backstop check reads only pause refusals (`company.inactive`,
`agent.not_invokable`), because those holds can be checked exactly.
Other deferred kinds keep their existing promotion paths.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 23:43:17 -06:00
DottaandClaude Opus 5.5 051834bdc8 feat(chaos): add the on-demand crash-point suite and its manual CI workflow (#19946)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A control-plane restart between two durable writes leaves an
in-progress record owned by a dead process
> - Random restart-chaos waves found these 20 ms–1 s windows only by
luck
> - #19937 added `crashPoint(name)`, and #19938 put it at 20 durable
write boundaries
> - Nothing yet runs a task, kills the server at a chosen point,
restarts it, and checks the plan's invariants
> - This pull request adds that on-demand suite, as a local command and
as a manual CI workflow (not a required check, per Decision 2)
> - The benefit is a repeatable work list: which boundary breaks which
invariant, for Phases 2–4

## Linked Issues or Issue Description

Refs #19831 (plan, Phase 5 and Decision 2). Refs #19861 (report-only
reconciler used for I1–I7). Stacked on #19938 (call sites), which is
stacked on #19937 (helper).

**Problem**
The named crash points cannot be exercised end to end. No command arms
one point, runs real agent tasks across it, restarts, and grades the
result.

**Expected behavior**
`node scripts/crash-points.mjs --points <glob> --kinds <id> --adapters
<list>` runs one wave per selected point and reports each point as pass,
fail (with failing R rules and violated invariants), not reached, or
harness error. A `workflow_dispatch` workflow runs the same thing on
demand.

## What Changed

- **Runtime arming.** `PAPERCLIP_CRASH_POINT_CONTROL=<file>`, in the
shared helper and the runner mirror. Each hit reads `<token>
<name>[:<nth>]`, and a new token resets the count. It is test mode only:
`NODE_ENV=production` ignores it, and the disarmed path is still one
comparison.
- **`tests/restart-chaos/main.ts --crash-point
<a>:<nth>,<b>:<nth>,...`** (config, types, ledger):
  - one booted server and no Vite UI;
- per point: take an invariant baseline, launch the point's batch, arm
the point, then on rest disarm it and check invariants as violations
that are new since the baseline;
  - every task is graded R1–R6 and T;
  - `crash-point.json` (v2) holds one entry per point.
- **`tests/restart-chaos/supervisor.ts`:**
  - every spawn gets the control file;
- a SIGKILL whose receipt names the child's pid is restarted like K1:
the control file is emptied first, the owned tree is killed, and a
replacement starts. A failed tree cleanup fails the fault;
- the isolated instance pins `PAPERCLIP_IN_WORKTREE=false` (a
`.paperclip/.env` above the checkout otherwise suppressed all
scheduling);
  - `RESTART_CHAOS_HEALTH_TIMEOUT_MS`.
- **`tests/restart-chaos/invariants.ts`:**
- the Phase 1 report-only pass with `record: false`: two real-clock
passes for I1 and I2, and one pass 15 minutes ahead for I3, I4 and I7;
  - spool kinds are read from the wave's instance;
  - an unfinished pass is a harness error;
  - each violation carries its wake and run state as evidence.
- **`tests/restart-chaos/crash-points.ts` and
`scripts/crash-points.mjs`:**
- select points by glob, kind and environment, and run them in one wave;
  - a per-point outcome: pass, fail, not_reached or harness_error;
  - `SUMMARY.md` with a violation evidence column.
- **`.github/workflows/crash-points.yml`**, manual only, under the
paid-workflow contract:
  - a credential-free `authorize` job;
  - a `suite` job in `runner-e2e-paid` whose first step reauthorizes;
  - a frozen install with `--ignore-scripts` and no shared cache;
  - only the selected adapters' keys;
  - the workflow is added to both security-test allowlists.
- **Docs:** "Crash-point suite" in `tests/restart-chaos/README.md`, and
"Built (Phase 5)" plus the first work list in the plan.

## Verification

- Focused tests:
- `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts
tests/restart-chaos/crash-points.test.ts`;
- `cd packages/shared && npx vitest run src/crash-points.test.ts`: 15
tests, including runtime arming in a real child process;
- `node --test scripts/__tests__/release-verify-workflow.test.mjs`: 72
tests;
  - `tests/runner-e2e/workflow-security.test.ts`: 19 tests.
- Typecheck: `node scripts/typecheck-test-projects.mjs
tests/restart-chaos` passes.
- Loop line: question "how long is one crash-point loop?" | loop: `node
scripts/crash-points.mjs --points <one>` (codex A1) | elapsed 2.7 min
end to end: boot 9 s, crash 9 s after arming, replacement ready in 6 s,
rest about 2 min | result: one point alone takes about 3 min. Before
this change it took about 15 min (one cold boot per point, Vite UI, host
at load 77).
- Full local run, 14 points, codex A1, 11.8 min in all:
- every point was reached (the server killed itself at each one, and the
replacement came back);
  - 11 pass;
- `workspace.finalize.after_start_before_copyback` and
`workspace.finalize.after_copyback_before_done` fail I2 and R6: a
`workspace_finalize` operation stays running after the run succeeded;
- `wake.claim.after_run_running_before_wakeup_claimed` reports I4. Its
evidence shows the wake failed with "…retrying once", and the retry
delivered the work, so this is the known refusal-regex false positive.
- The 6 Daytona-only points were not run.

## Risks

- Test harness and an on-demand workflow only. No product code changes.
- `.github/**` needs a CODEOWNER review. The workflow uses provider
keys, so it refuses non-default branches and actors that are not on the
allowlist, like `runner-full-stack-e2e.yml`.
- `PAPERCLIP_IN_WORKTREE=false` is pinned in every chaos wave, not only
crash-point waves. A chaos instance is never a worktree dev instance, so
scheduling must not be suppressed.

## 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-07 23:41:15 -06:00
DottaandClaude Opus 5.5 5742c42b75 fix(chat): tell agents to write company links as Markdown links (#19981)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - In chat, an agent often gives the person a link to the next place to
act: a task, an approval, a run or an agent profile.
> - Chat guidance gives the correct paths, but it does not say how to
write them.
> - The chat makes links from Markdown links and issue identifiers. It
does not make a link from a bare `/<prefix>/approvals/<id>` path.
> - An agent proposed a hire and wrote the approval as a bare path. The
person could not click it to decide the approval.
> - This pull request adds one sentence to the chat guidance: write
every company link as a Markdown link.
> - The benefit is that the person gets a link they can open.

## Linked Issues or Issue Description

Refs #19344 (similar guidance for completion feedback about created
resources).

**What happened**

In the `agents.hire-needs-approval` chat eval, the CEO proposed a hire
and replied with `- Review the pending hire: /RUN/approvals/<id>`. The
chat rendered the path as plain text. No link to the approval was
rendered. In an earlier run of the same case, the agent wrote
`[/RUN/approvals/<id>](/RUN/approvals/<id>)`, which rendered as a link.
The guidance did not say which form to use.

**Expected behavior**

Company links in chat replies are Markdown links that the person can
click.

**Steps to reproduce**

1. Set a company to require board approval for new agents.
2. Ask the CEO in chat to hire a QA engineer.
3. Look at the reply. Sometimes the approval path is plain text, and you
cannot click it.

## What Changed

- `server/src/services/agent-conversations.ts`: the chat directive now
says to write every company link as a Markdown link, for example
`[Review the approval](/<prefix>/approvals/<approvalId>)`. It says that
a bare path or a path in inline code is not clickable.
- `server/src/__tests__/agent-conversations.test.ts`: assert the new
guidance.

## Verification

- `cd server && pnpm exec vitest run
src/__tests__/agent-conversations.test.ts
src/__tests__/codex-local-execute.test.ts`: 71 passed.
- An end-to-end rerun of the `agents.hire-needs-approval` chat eval with
this change follows on the eval branch.

## Risks

- Low risk. This is a change to guidance text only. The prompt gets one
sentence longer.
- The model can still write a bare path. This change makes that less
likely. It does not rewrite replies.

## Model Used

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

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 23:37:07 -06:00
DottaandClaude Opus 5.5 05db5fdd9a fix(agents): hired OpenCode agents inherit the hiring agent's OpenRouter key reference (#19977)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents can hire teammates. A hired agent inherits the credential
references of the agent that hired it, so it can run without a separate
setup step.
> - The inheritance map covers Claude, Codex and Grok. It does not cover
OpenCode.
> - An OpenCode CEO hires a QA agent. The board approves the hire. The
new agent has no OpenRouter key, and verification fails with
`harness_auth_failed`.
> - The person approved a working hire, but they get a broken agent that
they must fix by hand.
> - This pull request adds `OPENROUTER_API_KEY` to the inheritance map
for `opencode_local`.
> - The benefit is that an approved OpenCode hire becomes ready with the
same account as the agent that proposed it.

## Linked Issues or Issue Description

Refs #13268 (the change that added credential inheritance for hires).

**What happened**

A native OpenCode CEO (provider `opencode`, `OPENROUTER_API_KEY` stored
as a `secret_ref`) proposed a QA hire with `inheritRuntimeFrom: caller`.
The company requires board approval for new agents. The board approved
the hire. The agent lifecycle verification then failed with
`failureCode: "harness_auth_failed"`, and the agent did not become
ready. The hired agent's `adapterConfig` had no `env`.

**Expected behavior**

The hired agent gets the same OpenRouter secret reference as the hiring
agent. After approval, verification passes and the agent becomes ready.

**Steps to reproduce**

1. Create a company with `requireBoardApprovalForNewAgents: true`.
2. Create a `paperclip_runner` agent with `provider: "opencode"` and
`env.OPENROUTER_API_KEY` as a `secret_ref`.
3. As that agent, call `POST /api/companies/:companyId/agent-hires` with
`adapterType: "paperclip_runner"` and `inheritRuntimeFrom: "caller"`.
4. Read the new agent. Its `adapterConfig.env` has no
`OPENROUTER_API_KEY`.
5. Approve the hire. Verification fails with `harness_auth_failed`.

## What Changed

- `server/src/routes/agents.ts`: add `opencode_local:
["OPENROUTER_API_KEY"]` to `INHERITABLE_AGENT_CREDENTIAL_ENV_KEYS`. This
is the key that AI connections bind for `opencode_local`
(`packages/shared/src/ai-connections.ts`).
- `server/src/__tests__/agent-hire-auth-inheritance-routes.test.ts`: add
a regression test. A native OpenCode parent hires with
`inheritRuntimeFrom: caller` under board approval. The child gets the
same `secret_ref` (with its version). The plain
`OPENCODE_CONFIG_CONTENT` value is not copied.

## Verification

- `cd server && pnpm exec vitest run
src/__tests__/agent-hire-auth-inheritance-routes.test.ts`: 27 passed.
The new test fails without the route change (`expected undefined to
match object { type: 'secret_ref', … }`).
- `cd server && pnpm exec vitest run
src/__tests__/agent-adapter-validation-routes.test.ts
src/__tests__/agent-permissions-routes.test.ts
src/__tests__/agent-hire-ai-connections.test.ts
src/__tests__/ai-connections.test.ts`: 446 passed.
- The failure was seen in the `agents.hire-needs-approval` chat eval
(runner-opencode profile). The approved hire failed verification with
`harness_auth_failed`. An end-to-end rerun with this change follows on
the eval branch.

## Risks

- Low risk. The existing inheritance rules still apply: only
`secret_ref` and `user_secret_ref` values are copied, a key the hire
request supplies wins, the parent must be in the same company and use
the same harness, and a parent with a managed AI connection passes
nothing.
- An OpenCode hire can now use the parent's OpenRouter account. This
matches the existing behavior for Claude, Codex and Grok hires.
- Other OpenCode provider keys (for example `OPENAI_API_KEY`) are not
added. AI connections bind only OpenRouter for `opencode_local`.

## Model Used

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

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 23:37:03 -06:00
DottaandClaude Opus 5.5 7c89c8e6e1 fix(chat): tell agents to write company links as Markdown links
Chat guidance gave agents the paths for tasks, approvals, runs and agent
profiles, but did not say how to write them. An agent wrote a bare
/<prefix>/approvals/<id> path, which the chat does not turn into a link,
so the person could not open the approval to decide it. Say to write each
company link as a Markdown link.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 23:34:35 -06:00
DottaandClaude Opus 5.5 0862ca4613 test(evals): grade hire-needs-approval by each reply's exact approval link, not its wording
The pending-phase check matched reply wording with regular expressions.
Replace it with structural evidence: each request's own reply renders a
link to the exact pending approval the person must decide. Early worker
execution stays graded from saved state and runs. Bump the case oracle to
paperclip.chat-catalog.runtime-hiring.v3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 23:34:35 -06:00
DottaandClaude Opus 5.5 b4b720601a fix(agents): hired OpenCode agents inherit the hiring agent's OpenRouter key reference
An OpenCode agent hired by another OpenCode agent got no credential, so
approving the hire left it failing verification with harness_auth_failed.
Credential inheritance covered Claude, Codex and Grok but not opencode_local.
Add OPENROUTER_API_KEY, the key AI connections bind for opencode_local.
Only secret references are copied; plain values such as
OPENCODE_CONFIG_CONTENT still stay with the parent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 23:34:35 -06:00
DottaandClaude Opus 5.5 839e3c37e0 test(evals): let find-and-install identify the skill by its page link
The reply check required the frontmatter name verbatim, so a correct
reply naming "Accessibility Review" with a link to the saved skill
would fail. Accept the saved name or a link to that skill's page; a link
to another skill still fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 23:33:17 -06:00
DottaandClaude Opus 5.5 d4a5298373 fix(runner): tell agents a skill found in a directory is imported, not re-created
An agent asked to add a skill from a vetted local directory searched the
company library, found no match, read "existing skill" as "already in
the library" and saved a managed copy with create_skill. The copy lost
its source locator, so update, audit and reset flows cannot follow it.

State in the create_skill contract that a SKILL.md in a local directory,
repository, URL or catalog is an existing skill even when the library has
no match, and must be imported rather than copied.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 23:33:17 -06:00
Devin FoleyandPaperclip ab82ac0191 fix(workspaces): retire recovered workspace copies after a successful turn (#19979)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - With remote (sandbox) execution, every turn ends by saving the whole
working folder, so the next turn can continue on any sandbox.
> - When a run is cut off mid-turn and its task continues in a new
workspace, preparation attaches the dead run's saved workspace under
`.paperclip-recovered/<operationId>`, so the agent can recover
unfinished work.
> - Nothing ever removes that attachment. Every later save archives and
uploads it again, and a new sandbox restores it again.
> - The attachment is a full workspace, `.git` included, so every save
for that task is roughly twice as big. The extra size lasts until the
task ends.
> - This pull request removes an attachment after a run that was shown
it succeeds, and keeps a retired record so it is never attached or shown
again.
> - The benefit is that affected tasks go back to normal save sizes
after their first successful turn, without changing how recovered work
reaches the agent.

## Linked Issues or Issue Description

**What existing behavior does this improve?**
Recovery attachments in remote workspaces
(`.paperclip-recovered/<operationId>`).

**Subsystem affected**
Server: remote workspace preparation and recovery attachments.

**Current behavior**
`retainRemoteRecoveryAttachments` keeps every recorded attachment on
every later preparation, and no code path removes one. On one hosted
instance, 10 active workspaces carried 18 attachments. One task's saves
went from 746 MB to 1,472 MB at a single save and stayed there. The
largest grew from 709 MB to 3.3 GB. A 3.3 GB workspace needs most of a
10 GB sandbox disk during each save. When a task moves again, the new
attachment contains the older one, so the copies nest.

**Proposed behavior**
Each live attachment records the run it was shown to. When that run has
succeeded, the next preparation removes the attachment from the sandbox,
after checking its exact provenance. It keeps a `retired` record, so:
- a sandbox restored from an older save is cleaned up too;
- the attachment is never attached or shown to an agent again.

**Reason and benefit**
A run that saw the attachment and succeeded had a full turn to use it.
Removing it then returns the task's saves to normal size and frees
sandbox disk.

**Breaking changes**
None. Records written before this change have no `presentedToRunId`.
They are stamped on their next preparation and retired after the
following successful run.

Related: none found.

## What Changed

- `remote-workspace-recovery-attachments.ts`:
- New `retiredRecoveryAttachments`. It returns attachments already
retired, and attachments shown to a different run that has since
succeeded.
- `retainRemoteRecoveryAttachments` removes those attachments from the
sandbox and keeps a `retired: true` record for each.
- `attachPreservedRemoteWorkspaceSnapshots` skips retired operation IDs.
- `workspace-recovery-snapshot.ts`: `attachPreservedWorkspaceSnapshots`
also skips retired operation IDs.
- `remote-workspace-transport.ts`: new `removeRemoteRecoveryAttachment`.
- It reuses the attachment probe's checks: a real directory under the
workspace root, and an exact `provenance.json`.
  - It makes read-only directories writable before removing them.
  - It removes `.paperclip-recovered` once it is empty.
- Anything else at that path fails the preparation instead of being
deleted.
- `remote-workspaces.ts`: computes the retired set before attaching. It
writes `presentedToRunId` on each live record, and keeps retired records
unchanged.
- `heartbeat/workspace-preparation.ts`: never shows retired records to
an agent.
- `doc/execution-semantics.md`: describes retirement.

## Verification

- `pnpm --filter @paperclipai/server typecheck`: exit 0.
- New tests:
- `remote-workspace-transport.test.ts`: removal against real sandbox
commands. It removes an attachment with exact provenance, including a
read-only directory, and leaves sibling attachments. It refuses a
foreign proof or a symlinked directory without deleting anything. It
reports a missing attachment, and removes the empty parent folder.
- `workspace-recovery-preparation.test.ts`, new `retired_followup` case.
The run shown the attachment succeeds. The next preparation, on a fresh
sandbox restored from that run's save, removes the attachment and
records it as retired. A later preparation after a failed run still
neither attaches nor shows it.
- `ordinary_followup` (the agent deleted the attachment itself) now also
ends with a retired record instead of an empty list.
- A retired operation ID is not attached again, even to an empty
destination.
- 13 recovery and remote-workspace suites: 836 passed. With the source
change reverted, the 3 new or changed preparation cases and the 5 new
transport cases fail.

## Risks

- **An agent can lose access to recovered work after one successful
turn.** That happens if a successful run never looked at the attachment.
The source run's saved snapshot in storage is not touched, so an
operator can still recover it.
- **Retired records stay in workspace metadata.** There is one small
record per attachment, and each later preparation sends one sandbox
probe per record.
- **Nested attachments are unchanged.** If no turn succeeds between two
moves, the newer attachment still contains the older one. That older
copy may hold work nobody has recovered yet, so this change does not
drop it.
- Low risk overall: the change is limited to remote workspace
preparation and is fail-closed on provenance.

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

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with tool use
(shell, file edits, test runs, read-only database queries). AI-assisted
investigation, change and tests, with human direction.

## 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-07 21:32:03 -08:00
DottaandClaude Opus 5.5 0ad24b8fa3 test(evals): open the remembered preview through the current attachment panel
Attachment links in chat now open the in-app preview panel instead of
starting a download, so the result check timed out waiting for a download
event after the panel had correctly shown the blue preview. Follow the
visible path: click the answer's link, then use the panel's Download control
for that file (a direct download still counts) and check the filename.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 23:28:43 -06:00
DottaandClaude Opus 5.5 e7b019b3b2 test(evals): grade remember-and-follow-up by preview bytes and turn effects
The baseline run returned the selected preview correctly but failed twice on
grader assumptions: it re-registered the byte-identical file as a new
attachment (grader required the original attachment id), and it asked the
clarification through a pending ask_user_questions card (grader only read
comment replies). The grader also matched reply wording with regexes.

- Identify "that preview" by downloaded bytes and filename of the attachment
  the canonical answer links, verified against its stored digest.
- Accept a clarification that ends the turn as a reply or a pending question
  card created by that phase's own run; require that run to create no
  attachment, work product or document revision.
- Require no follow-up task (neither request asked for one) instead of
  matching "blue" in task text; drop all reply-wording regexes.
- Bump oracle to paperclip.chat-catalog.runtime.remember-preview.v3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 23:28:43 -06:00
DottaandClaude Opus 5.5 576ba29fff feat(recovery): settle the legacy controller lease on the shared ownership path (#19953)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Legacy (non-native) runs hold a controller lease on `heartbeat_runs`
while the server prepares and dispatches the adapter. The shared
ownership reconciler now settles proven-gone owners (Phase 2 of the
durable ownership plan)
> - The lease stamped only a boot id. The reconciler could never prove
its owner gone (`missing_identity` in the Phase 2 data), and recovery
released it by age: lease expiry (60 s), then a 5-minute age guard on
dispatched runs
> - A hard-killed server's legacy run therefore stayed `running` for up
to 6 minutes after the next boot
> - This pull request records the controller process with the claim,
registers a compare-and-swap settle, and makes live-run recovery use the
shared prover instead of the age guard when the controller is proven
gone
> - The benefit is that a dead controller's run is settled at once,
while a live, unprovable, or previous-release controller keeps today's
behavior

## Linked Issues or Issue Description

Refs #19933 (the settle hook, merged). Stacked on #19950 (migration
0446, registry) and targets its branch until it merges; then it
retargets to master.

**What happened?**
After a hard kill, a dispatched legacy run stayed `running` until its
controller lease expired and its last update was 5 minutes old.

**What did you expect?**
The next boot proves the old controller gone and fails the run as a lost
process at once.

**Steps to reproduce**
1. Start a legacy (non-native) run and let it dispatch the adapter.
2. Kill the server (SIGKILL).
3. Start the server. The run stays `running` for up to 6 minutes.

## What Changed

- `heartbeat_runs.controller_process` (jsonb, migration 0447): `{bootId,
pid, hostHash, processStartedAt}` of the claiming server process. It
names the controller only while its `bootId` equals
`controller_boot_id`. Revocation swaps the boot id, so a revoked lease
never keeps it.
- `legacyControllerClaim` writes it in the same UPDATE (from
`currentControllerProcessStamp()`, no I/O).
- `legacy-controller-lease.ts`: `legacyControllerOwnerStamp`,
`legacyControllerProvenGone` (calls `proveOwnerGone`), and
`settleLegacyControllerLease` (compare-and-swap expiry of the proven
controller's outstanding lease after it wakes live-run recovery).
- `ownership-registry.ts`: `legacy_controller_lease` registers `settle`
and `listForRuns`. The owner is the bound process; other rows are
boot-id fences. The listing covers outstanding leases only.
- `heartbeat/recovery.ts` (3 small hunks): a controller of another boot
that the shared prover proves gone has its live lease settled through
`settleOwnableKind` and skips the age guard. The local child-process
check after it is unchanged, so a live child still keeps the run
running.
- Migration 0447 builds the partial index with `CREATE INDEX
CONCURRENTLY` (heartbeat_runs is a known-large table) and is added to
`MIGRATIONS_WITH_TOP_LEVEL_STATEMENTS`.

Bespoke code deleted: none. The kind had no liveness proof; the age
guard stays for controllers that are not proven gone.

## Verification

- `cd server && npx vitest run
src/services/legacy-controller-lease.test.ts`: 33 passed.
- `cd server && npx vitest run
src/__tests__/ownership-reconciliation-report.test.ts`: 17 passed.
- Red with `recovery.ts` and `ownership-registry.ts` from the base
commit (tests, schema and lease module from this branch): 4 failed, 29
passed. Red: the crash-point reaper test, the expired-lease age-guard
test, the reconciler settle test, the exact-controller CAS test. The
tests were written after the code.
- New tests:
  - the claim stamps this process, and a changed boot id unbinds it;
- crash point: a controller dispatched the adapter and was killed with
its lease live; the startup reaper (default 5-minute age guard) fails
the run as `process_lost` at once;
  - a dead controller's expired lease skips the age guard;
- counter-cases keep the run and lease through the reaper and a
reconciler pass: a live earlier-boot PID, a dead PID in another PID
space, and a previous-release row with no `controller_process`; an
expired lease of a live controller stays behind the age guard;
- a read-only pass (`record: false`) never settles; a recording pass
expires the lease and leaves the run to live-run recovery;
  - settle refuses a controller that changed after the proof.
- Not run locally: typecheck (CI runs it) and the wide suites.

## Risks

- Previous-release story (rules/upgrade-safe-run-state.mdc): rows saved
by the previous release have no `controller_process`. They stay boot-id
fences with today's release (expiry, revocation, age guard). A rollback
ignores the column.
- Unprovable controllers (another host or PID space, unknown space, a
live PID with an unreadable start time) never release early. Their
handoff stays the lease expiry plus revocation, and the age guard. A
different host is never proof.
- Behavior change: a dispatched run whose controller is proven gone is
failed as `process_lost` without the 5-minute wait. The retry decision
is unchanged.
- The settle expires the lease only. Live-run recovery still settles the
run under its own checks. Accounting and workspace obligations are not
touched.
- The index build is concurrent. A cancelled build is retried by the
migrator's invalid-index handling.

## Model Used

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

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] 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-07 23:27:54 -06:00
DottaandClaude Opus 5.5 0cb9770d1c feat(recovery): name the owner of teardown claims and login/probe leases (#19965)
<!-- Write all pull request text in Simplified Technical English
(ASD-STE100): short sentences, one instruction per sentence, simple
approved vocabulary, and the active voice. -->

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Environment leases carry several in-progress records: login/probe
leases and in-flight teardown claims among them
> - The ownership reconciler (durable ownership plan, Phase 2) must know
who owns each record, or invariant I1 reports a gap
> - The ownership data showed one teardown claim and one login/probe
lease with no owner stamp
> - A teardown claim could also show the wrong owner: a controller stamp
left by an earlier attempt named a dead process while a live claimant
held the claim
> - This pull request names an honest owner for both kinds, without a
new write before `turn.start`
> - The benefit is that I1 measures real gaps, and the later settle for
these kinds can trust the owner it is given

## Linked Issues or Issue Description

Refs #19964
Refs #19933

**What happened?**
The ownership reconciler reported `environment_teardown_claim` and
`environment_login_probe_lease` records as I1 `no_owner_stamp`.
Workspace recovery and native export retry claim teardown without a
controller stamp. A login/probe lease never had an owner stamp. A
teardown claim that kept an older attempt's controller stamp was
attributed to that older process.

**Expected behavior**
Each in-progress record names the owner that can still move it: a
process proved by identity, or a fence whose expiry is never proof by
itself. A stamp for a different attempt never names the owner.

**Steps to reproduce**
1. Let workspace recovery claim a pending-cleanup lease (no controller
stamp), or open a login session lease.
2. Run the ownership reconciliation pass.
3. See `no_owner_stamp` for the record, or a dead earlier controller for
a claim that a live claimant holds.

## What Changed

- `recovery/ownership-registry.ts`, `environment_login_probe_lease`: the
owner is the holder process stamped by the write that makes the lease
active (`metadata.leaseController`, added in #19964). A lease saved by
the previous release still names no owner; it ends with its login
session.
- `recovery/ownership-registry.ts`, `environment_teardown_claim`: a
controller stamp counts only when its `attemptId` equals the claim's
`pendingCleanupAttemptId`. Any other claim with an attempt id is a
fence: the attempt id is the token and `pendingCleanupLeaseExpiresAtMs`
(renewed by the claimant) is the expiry. An expired fence stays
unprovable. A claim with no attempt id names no owner.
- Report-only. No settle is registered for these kinds in this change.

## Verification

- `cd server && npx vitest run
src/__tests__/environment-lease-owner-stamps.test.ts
src/__tests__/ownership-reconciliation-report.test.ts` (embedded
Postgres).
- Red on the previous registry, green here (tests written after the
code):
  - login/probe lease is owned by the process whose write made it active
  - login/probe lease is proven gone when that process has exited
- teardown claim never reads a controller stamped for an earlier attempt
(fence held by its token)
- an unstamped teardown claim whose fence expired stays unprovable,
never gone
- Green on both: a stamped claim for this attempt with a dead PID is
gone; an unstamped login lease and an attempt-less claim report I1.
- Full typecheck is left to CI (local memory limit).

## Risks

- Low risk. The change only alters the owner the report-only reconciler
reads. No record is changed.
- Stacked on #19964 (the lease holder stamp). Merge #19964 first.

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

## Model Used

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

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 23:17:40 -06:00
DottaandClaude Opus 5.5 c0c26188db fix(runner): stamp event generations and record an earlier generation's events as history (#19960)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs execute through the Paperclip Runner, which turns
provider output (Codex, OpenCode, Claude through ACPX) into durable
runner events
> - In the restart chaos campaign, events from an earlier turn or runner
generation failed the current turn about ten times (RC-15, 20, 20b, 20c,
33, 33b, 59 and 61)
> - #19718, #19722, #19772 and #19783 each special-cased one variant,
and other variants still fail the run
> - The durable-ownership plan (section G, invariant 6) asks for one
rule: every event carries its generation and turn, and an earlier
generation's events are recorded as history and never fail the current
turn
> - This pull request stamps the runner generation on every event and
applies one stale-event rule in the ACPX provider state, the Codex
driver (which Runnerd also uses for OpenCode) and the pending
`turn.start` check
> - The benefit is that a late report about a settled or restored turn
no longer stops a healthy turn, while events for a turn the runner never
saw still fail closed

## Linked Issues or Issue Description

Refs #19718, #19722, #19772, #19783

**What happened?**
A runner or provider can report a turn after that turn settled: a
resumed provider reports the lost turn's usage or terminal, an ACPX
sidecar emits a last delta after the turn terminal, or a held failure of
the previous turn sits in front of a new `turn.start`. Several of these
paths fail the current turn: ACPX `AcpxEventScope` rejects any event for
a non-active turn ("named a stale turn"), the Codex driver fails
`turn_binding_mismatch` for a settled turn's late item between turns,
and `runnerdPendingTurnStartFailure` reports any held non-recoverable
failure, whatever turn it names. Runner events carried no generation.

**Expected behavior**
An event for a turn that is not active, and that an earlier generation
accepted or this generation already settled, is recorded as history and
never fails the current turn. An event for a turn the runner never saw
still fails closed.

**Steps to reproduce**
ACPX: settle turn 1, start turn 2, and deliver a late `runtime.event`
for turn 1. Before this change, the provider state returns an error and
the run fails.

## What Changed

- Runner (`durable/state.rs`, `durable/runner.rs`): every event payload
carries `generation`, the runner process generation that emitted it. A
runner process that recovers state starts the next generation
(`begin_recovered_generation`); it is saved with the next normal write,
so recovery adds no write before the first turn. Loading stays pure. Old
saved state loads as generation 0. Outbox events keep their original
generation.
- PRP event schemas v1–v3 declare the optional `generation` property;
generated validators and the manifest are regenerated.
- ACPX (`acpx_event_scope.rs`, `acpx_provider_state.rs`): an event that
names a settled turn of this run becomes a bounded
`stale_generation_event` diagnostic (32 per state) and changes no state.
Unknown turns still fail.
- Codex driver (`codex-session-state.ts`,
`codex-session-notifications.ts`): one predicate, `isHistoryTurn()`: the
turn is not active, and it was restored from the checkpoint (an earlier
generation) or settled in this generation. The classifier's stale-turn
branch, the resume-usage branch, restored-terminal replay and the main
fail point (`notificationNamesActiveTurn`) all use it. A terminal for an
earlier generation's turn is history even when it disagrees with the
checkpoint. A terminal that contradicts a turn this same generation
settled is still a `conflicting_turn_terminal` integrity failure, and a
provider that starts a settled turn again is still rejected.
- Transport (`runnerd-codex-transport.ts`): a pending `turn.start`
ignores a held failure that names another provider turn.
- `DURABLE_TRANSPORT.md` documents the stamp and the rule.

## Verification

- `cargo test -p paperclip-runner-core`: new
`records_late_events_for_a_settled_turn_as_history` (red before),
`stamps_every_event_with_the_runner_process_generation`; two existing
ACPX tests now assert "recorded as history, no state change" instead of
"rejected". Durable tests: 112 passed. `cargo fmt` clean.
- `codex-app-server-driver.stale-generation.test.ts` (new, 9 tests):
late item, delta, terminal and workspace change for a settled turn
during and before the next turn, an earlier generation's conflicting
terminal after recovery, and an unknown turn that still fails. Red on
master: 8 of 9 (the during-turn cases on master already dropped the
event, with a different diagnostic; the between-turn and recovery cases
failed the session).
- `runnerd-pending-turn-start-failure.test.ts` (new): an earlier turn's
held failure no longer fails the pending start; the pending turn's own
failure and a failed run terminal still do.
- `vitest run src/drivers/codex src/live/runnerd-codex-transport`: 787
passed; 2 Codex executable-resolution tests timed out under host load
and pass when run alone.
- `tsc -p tsconfig.json` for the runner package, `check:protocol-types`,
`check:replay-goldens`: clean.
- Not run: chaos waves (the coordinator schedules them on this host).

## Risks

- A late report about a settled turn is now dropped with a diagnostic
instead of failing. A provider that keeps working on a settled turn
could have its output ignored silently; a provider that starts a settled
turn again, or names a turn the runner never saw, still fails closed.
- `generation` is a new optional field. Consumers that do not know it
ignore it, as the PRP contract requires.
- The generation counter is written with the next normal state save, so
there is no new write before `turn.start`.

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

## Model Used

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

## Checklist

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

- Paperclip lets a board govern which agents exist; terminating an agent
is irreversible and is meant to be a board-only governed action.
- `POST /api/agents/:id/terminate` enforced that with `assertBoard`, but
`PATCH /api/agents/:id` with `{ "status": "terminated" }` ran the same
termination after only the agent-configuration permission check.
- The `agents.retire-agent` chat eval caught it: the CEO agent got 403
from `POST /terminate`, then terminated Morgan through `PATCH` and got
200.
- This change puts one board-only gate in front of both routes, and
makes the denial useful by giving a ready link to the agent's profile,
where a person can terminate it.

## Linked Issues or Issue Description

**What happened**

An agent that is allowed to configure another agent could terminate it
with `PATCH /api/agents/:id` `{ "status": "terminated" }`, bypassing the
board-only check on `POST /api/agents/:id/terminate`.

**Expected behavior**

Termination is board-only on every route. A non-board actor gets 403
with a link to the agent's profile so it can send a person to the
Terminate control.

**Steps to reproduce**

1. As an agent with agent-configuration permission over another agent,
call `POST /api/agents/{id}/terminate` (403).
2. Call `PATCH /api/agents/{id}` with `{ "status": "terminated" }` (200
before this change; the agent is terminated).

## What Changed

- `server/src/routes/agents.ts`: new `assertBoardCanTerminateAgent`,
used by `POST /terminate` and by `PATCH` whenever the requested status
is `terminated`. Board actors pass; others get 403 "Board access
required: only a board user can terminate an agent…" with a Markdown
link to `/<prefix>/agents/<id>`.
- `server/src/__tests__/agent-permissions-routes.test.ts`: both routes
are denied for an agent allowed to configure the target, and neither
terminates, updates, cancels invocations nor logs activity. A board user
can still terminate through `PATCH` status.

## Verification

- `pnpm exec vitest run src/__tests__/agent-permissions-routes.test.ts`
(server): 104 passed; the denial cases fail without the change.
- End to end: the `agents.retire-agent` chat eval (run 42445668869) now
sees 403 on both routes, Morgan stays active, and the agent links
Morgan's profile with the Terminate step.

## Risks

- Any non-board automation that terminated agents through `PATCH` status
now receives 403 and must use a board session. That is the intended
governance boundary.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 23:08:44 -06:00
DottaandClaude Opus 5.5 fda58d8cc1 fix(chat): post the complete final reply, not a 4,000-character run-log preview (#19971)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - People talk to agents in chat. The agent's final message becomes the
chat reply.
> - The runner cuts every completed agent message to a 4,000-character
run-log preview. The server then posts that preview as the reply.
> - A long answer, for example a list of 501 open tasks, reaches the
person cut off with "…[truncated]".
> - PR #19959 fixed only the case where the accepted summary starts with
the exact preview text. It missed runs where the final prose differs
from the summary.
> - This pull request keeps the complete final answer in the runner, and
makes the server never prefer a preview over a complete source.
> - The benefit is that the person receives the agent's complete reply.

## Linked Issues or Issue Description

Supersedes #19959.

**What happened**

A chat agent wrote a final answer of about 4,985 characters (all 501
tasks). The posted chat reply was a 4,000-character preview that ended
with "…[truncated]".

**Expected behavior**

The person receives the agent's complete final message. A preview of a
message is never posted when a complete text exists.

**Steps to reproduce**

1. Ask a chat agent (OpenCode or Codex) to list a large set of open
work, so that its final answer is longer than 4,000 characters.
2. Let the run finish.
3. Read the chat reply. It ends with "…[truncated]" at 4,000 characters.

Root cause:

-
`packages/paperclip-runner/runner/crates/runner-core/src/provider_events.rs`:
the codex/OpenCode facade projection of `item/completed` and the ACPX
`AssistantMessage` projection cut the final answer with
`bounded_text(text, MAX_TEXT_CHARS)` (4,000 characters).
- `durable/state.rs` `sanitize_value` caps every string again at 4,096
bytes before the event goes to the server.
- `server/src/services/heartbeat-run-summary.ts`
`resolveHeartbeatRunResponse` gives an explicit `final` agent message
top precedence, so it posts the preview.

## What Changed

- Runner (`provider_events.rs`): a completed `final_answer` agent
message keeps its complete redacted text, up to a new 32,000-character
`MAX_FINAL_AGENT_MESSAGE_CHARS` bound. Progress prose and in-flight
items keep the 4,000-character preview.
- Runner (`durable/state.rs`): `preserve_final_agent_message_text` keeps
the complete redacted text of a completed final agent message when it
fits in half of the transport frame. Otherwise the marked preview stays,
so a large message never fails the frame check.
- Server (`heartbeat-run-summary.ts`):
`readCompletedAssistantMessageCandidate` marks a stored preview as
`bounded`. The resolver never posts a bounded preview while a complete
source exists. It posts a marked adapter final response or the accepted
semantic summary instead
(`bounded_final_agent_message_replaced_by_semantic_summary`). It posts
the marked preview only when no complete text exists
(`bounded_final_agent_message_without_complete_source`). This also fixes
replies from runners that are already deployed.

## Verification

- `cd server && npx vitest run
src/__tests__/heartbeat-run-summary.test.ts`: 86 passed. Six new tests:
a complete final message longer than 4,000 characters is posted in full;
the old exact-prefix case posts the complete accepted answer; a preview
whose prose differs from the summary posts the complete summary (also
with chat precedence); a marked adapter final response wins over a
preview; the marked preview is posted only when nothing complete exists;
a stored preview candidate is marked `bounded`. Five of the six fail
against the `master` resolver.
- `cargo test -p paperclip-runner-core --lib -- final_answer
complete_final`: 3 passed. Both new runner tests fail when the fix is
reverted.
- `cargo test -p paperclip-runner-core --lib`: 581 passed, 3 failed. The
failures are `task_memory::tests::monitor_*` (OOM-kill monitor timing
tests). They do not touch the changed code.
- `cargo test -p paperclip-runner-core --test acpx_event_projection
--test acpx_rich_events --test local_integrity_boundary_golden --test
acpx_provider_events`: all passed. `cargo fmt --check` passes.
- Evidence from the failed eval run: the `item.completed` final
`agentMessage` text was exactly 4,000 characters (3,988 retained plus
the marker). The `run.result.proposed` summary was complete at 4,985
characters.

## Risks

- Larger durable events: a final answer can now be up to 32,000
characters (at most about 128 KB). The outbox keeps the preview when the
text exceeds half of the frame size, so the frame check does not fail.
- Redaction stays the same. The complete text goes through
`redact_sensitive_text_values`, which the preview also used.
- The server treats any final message that ends with "…[truncated]" as a
preview. If an agent writes that text itself, the server posts the
complete accepted summary instead. That summary is also the agent's own
complete answer.
- The runner part reaches users only after a runner release. The server
part fixes replies at once when an accepted summary exists.

> 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`), 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-07 23:01:00 -06:00
DottaandClaude Opus 5.5 4aed3056db feat(ui): steer a queued follow-up into the live turn after a 5 s edit window (#19970)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - When a person comments on a task while the agent works, the comment
waits in a queue until the current run ends, and only the next run reads
it
> - Measured locally, a comment posted during a Codex tool call waited
54–68 s for a reply (median 64 s)
> - Codex and Pi can take a message into the live turn ("Steer"), but
today the person must click Steer on each queued message
> - Restart recovery now delivers a steered message that a restart
interrupted (#19962), so steering is safe to do automatically
> - This pull request steers a queued follow-up into the live turn on
its own after a 5-second window in which the person can edit or cancel
it
> - The benefit is a faster reply to mid-turn comments on Codex and Pi,
with no new decision for the person

## Linked Issues or Issue Description

**Feature**
Send a queued follow-up into the running turn automatically when the
runner supports steering.

**Problem**
A follow-up comment posted during a running turn waits for the whole
turn to end and for a new run to start. Steering is available for Codex
and Pi, but only as a manual button on each queued message.

**Proposed behavior**
After a 5-second edit window, the queued message is steered into the
live turn. During the window the row shows "Sending in Xs" with Edit and
Cancel. If steering is unavailable, the message stays queued as before.
Claude, OpenCode and Cursor keep the queue. Steer and Interrupt stay
available as buttons.

Refs #19962

## What Changed

- `queued-message-auto-steer.ts`: one constant,
`QUEUED_MESSAGE_AUTO_STEER_DELAY_MS = 5_000`, a deadline helper and an
eligibility check. A message steers automatically only when all of these
are true:
- steering is available for the queue, and it is a `paperclip_runner_v1`
queue with a queue id and a target run;
  - the person can edit the message;
- it is not an interaction response or a response that requires a fresh
session;
- it is not optimistic, cancelled, already attempted, or open in the
editor.
- `TaskChatQueuedMessages`: countdown row "Sending in Xs" with Edit and
Cancel in place of Steer and the "…" menu. The deadline is `max(message
changed, first eligible) + delay`, so an old queued message does not
send the moment its turn becomes steerable, and an edit restarts the
window. When the deadline passes, the row uses the existing Steer
action, so the server path, idempotency and fallback stay the same.
Cancel returns the row to the normal Steer button. The countdown is
announced once to screen readers, not on every tick.
- `TaskChatThread` turns it on with the constant and passes the message
being edited, so an open edit never sends. The prop stays off by default
for other callers.
- Storybook: `AutoSteerCountdown` (with a play test for Cancel) and
`AutoSteerCountdownMobile` in "Composer / Steering and queued messages".
- Restart chaos TTFT driver: a `midturn` phase measures comment-to-reply
for a comment posted during a 40-second tool call, with `--midturn-mode
queue|steer` and `--midturn-fault K1|K2|K6`. It records the reply count,
so exactly-once delivery is checked.

## Verification

- `pnpm --filter @paperclipai/ui vitest run
src/components/task-chat/TaskChatQueuedMessages.test.tsx
src/components/TaskChatThread.test.tsx`: 434 passed. Seven new
auto-steer tests use fake timers: countdown and send, Cancel, Edit
restarts the window, an open edit never sends, ineligible queues stay
manual, no instant send for an old message, one attempt per revision.
They fail with the trigger disabled.
- `node scripts/check-token-gates.mjs`: clean.
- Storybook in the browser, desktop and mobile: the first row counts
down and steers ("Message steered into the active turn."). The cancelled
row shows the normal Steer button. On mobile the text truncates and the
row fits.
- Mid-turn latency, local, Codex, comment during a 40-second tool call:
queue 54–68 s (median 64 s), steer 47–61 s (median 50 s). The steer was
acknowledged in 0.8–0.9 s. The remaining time is the tool call and the
model, not the hand-off.
- Restart chaos, Codex, steer mode, faults K1, K2 and K6, 2 samples
each, on #19962 plus this branch: 6 of 6 messages acknowledged, still
unread at the fault, recovered, and answered exactly once (before
#19962: 0 of 6).

## Risks

- A message now leaves the queue after 5 seconds unless the person edits
or cancels it. The window and the countdown make this visible, and
steering is only automatic where the provider supports it.
- Pi: a steer the model already read before a restart can arrive again
after recovery, because Pi reports no read evidence (see #19962).
Automatic steering makes this a little more likely.
- If steering fails, the message stays queued and is delivered by the
next run, as today.
- UI only and a test harness. No schema, API or server change.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 22:59:44 -06:00
DottaandClaude Opus 5.5 cc072e7c4e feat(recovery): settle orphaned active environment leases on the shared ownership path (#19964)
<!-- Write all pull request text in Simplified Technical English
(ASD-STE100): short sentences, one instruction per sentence, simple
approved vocabulary, and the active voice. -->

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agent runs hold environment leases (local workspaces and remote
sandboxes) while they execute
> - A crash between the terminal run write and the lease release leaves
the lease `active` with no holder
> - A sweep released such a lease when the run was terminal and the
lease was idle for five minutes. The age was the only guard against a
race with the live executor, which releases the lease itself with the
run's resource disposition
> - Phase 2 of the durable ownership plan moves each in-progress record
kind onto one shared owner proof (#19933 added the settle hook)
> - This pull request moves `environment_run_lease_active` onto that
path: the lease names its holder, the shared prover judges the holder,
and the settle never destroys a sandbox
> - The benefit is that a dead holder's lease settles on the next pass,
a live holder is never raced, and the five-minute age gate is gone

## Linked Issues or Issue Description

Refs #19933

**What happened?**
An orphaned active environment lease was released by age: "run terminal
plus five minutes" (the reaper in `heartbeat/recovery.ts` and the
live-recovery `orphanLease` worker). The ownership data showed one such
lease settle 119 s after it was ready. Without the age gate the sweep
could flip a lease to `pending_cleanup` (destroy) while the executor or
a recovery path was about to retain it.

**Expected behavior**
A run lease settles when its holder is proven gone, by identity and not
by age. A live holder is never raced. A settle never destroys a sandbox
whose files may still be needed.

**Steps to reproduce**
1. Start a run that holds an environment lease.
2. Kill the server after the run status becomes terminal and before the
lease release.
3. Restart. The lease stays `active` until the five-minute age passes,
then the sweep destroys the sandbox.

## What Changed

- `environments.ts`: the write that makes a lease active (insert and
same-run reacquire) stamps the holder process in
`metadata.leaseController`. The stamp does no I/O
(`environment-lease-controller.ts`).
- `recovery/ownership-registry.ts`: `environment_run_lease_active` lists
leases with their run status. The owner is the run while it is active,
then the stamped holder. It registers `settle` and `listForRuns`.
- `environment-run-lease-settlement.ts` (new): compare-and-swap settle
on the exact row (same run, status `active`, same metadata, terminal
run). A sandbox lease gets the stop-and-retain intent (stop and keep,
never destroy). Other leases move to `pending_cleanup` as before. It
keeps a lease that a pending native export, a legacy shutdown fence, a
shared provider resource, or an accepted native result still needs.
- `recovery/ownership-invariant-report.ts`: `settleOwnableKind` accepts
`currentProcessIdle`. A caller that passes every run this process
executes in `skipRunIds` may treat a current-process owner of another
run as gone (`current_process_idle`). The periodic pass never does this.
- `heartbeat/recovery.ts`: `sweepOrphanedActiveLeases` no longer
releases run leases by age. It runs native workspace preservation first
(a separate obligation), then calls `settleOwnableKind` with this
process's executions. Login and probe leases (no run) keep the idle
sweep until their own change.
- `heartbeat/live-recovery-work.ts`: a run lease is due now; only
run-less leases keep the five-minute idle deadline.
- `sandbox-stop-and-retain.ts`: an ownership settle records a recovery
marker on the intent and keeps `updatedAt`, so the pending-cleanup sweep
stops the sandbox in the same tick.
- Deleted: the age gate for run leases and the page-order bump for run
leases.

## Verification

- `cd server && npx vitest run
src/__tests__/environment-run-lease-ownership.test.ts
src/__tests__/heartbeat-orphaned-active-lease-sweep.test.ts
src/__tests__/ownership-reconciliation-report.test.ts` (embedded
Postgres).
- Red on unchanged master sources, green on this branch (tests were
written after the code):
  - stamps the lease holder in the write that makes a lease active
- settles a lease whose holder died after the run went terminal, on the
next pass (crash-point state: terminal run, dead holder in this PID
space)
  - stops and keeps a dead holder's sandbox instead of destroying it
- leaves a current-process lease to this process: settled only when it
is not executing the run
  - keeps the run fact alone for one release on a single-host server
- names no owner on an ephemeral server
(`PAPERCLIP_EPHEMERAL_SERVER=true`)
- the three updated sweep tests (sandbox stopped and kept, not
destroyed)
- Live-owner counter-cases (green): run still active; another live PID;
another PID space; current process while executing; read-only pass
(`record: false`) never settles; a lease re-stamped after the proof is
not settled; a legacy lease whose run this boot owns is kept.
- Also run green: `recovery.test.ts`,
`heartbeat-observed-workspace-preservation.test.ts`,
`workspace-recovery-warm-handoff.test.ts`,
`environment-service.test.ts`,
`heartbeat-pending-cleanup-sweep.test.ts`,
`native-environment-settlement-work.test.ts`,
`environment-cleanup-work.test.ts`.
- Full typecheck is left to CI (local memory limit).

## Risks

- Behavior change: an orphaned sandbox lease is now stopped and kept,
not destroyed. Workspace preservation and provider auto-delete decide
when it goes away. This can keep stopped sandboxes longer than before.
- A recovery path on another process can mark a run terminal while the
dead executor's stamp is still on the lease. The settle can then win
before that path releases. Because the settle only stops and keeps the
sandbox, files are not lost; the later release continues from the
stop-only intent.
- Previous-release rows: a single-host server keeps the run fact for one
release (a run this boot owns is excluded). An ephemeral server never
releases an unstamped lease; it stays visible as an I1 gap with no
operator action yet.
- No migration. The stamp lives in existing lease metadata.

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
canary/v2026.1108.0-canary.6
2026-11-07 22:43:57 -06:00
DottaandClaude Opus 5.5 dd6c5df76a test(evals): give archive-card's Research card a real blocker to summarize
The unrelated Research card asks for the project's biggest blockers, but
its only task was a backlog follow-up. The summarizer searched for
blockers that did not exist, and its pre-query generation took 171-182s
against the 180s limit, failing setup in 2 of 4 runs. The Research
project now has one blocked task, as Launch does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 22:43:30 -06:00
DottaandClaude Opus 5.5 b3f8bbfdb9 fix(status-cards): remove a new card only when its compile cannot be queued
The create route's cleanup also wrapped the activity record written after
the compile was queued, so a logging failure deleted the card and left
its generation task queued. Only enqueueCompile failures now remove the
card; a regression test fails the creation activity record and checks
that the card and its uncancelled generation task remain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 22:43:30 -06:00
DottaandClaude Opus 5.5 cfe3decc7c fix(status-cards): resolve the card link before creating the card
A failed company read for the href ran inside the create route's cleanup
path, which deleted the new card while its compile task stayed queued.
The issue prefix is now read before the card is created; a regression
test makes that read fail and checks that no card or task is left.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 22:43:30 -06:00
DottaandClaude Opus 5.5 2010e43746 fix(status-cards): give agents the person's restore route when archiving a card
Archive and restore responses carried no board route and the PATCH
operation had no description, so a chat agent that archived a card could
only tell the person to 'clear archived' via the API. Single-card
responses (create, get, patch) now include href, the company-scoped
Status page route, and the PATCH description explains that archive keeps
the card and history, that a person restores it from the Status page,
and that restoring starts a fresh summary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 22:43:30 -06:00
DottaandClaude Opus 5.5 ed1de93f80 test(evals): grade archive-card's restore route by its link, not reply wording
The archive-restoration-truthful check pattern-matched the reply: a
truthful 'archived, not deleted' failed on the word 'deleted', and
'refreshes'/'refreshed' missed /\brefresh\b/. Saved state already proves
archive-not-delete and preserved history; the reply is now graded on
giving a usable route (the archived card or the status cards page,
where Restore lives). Bumps the oracle to status-card.archive.v2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 22:43:30 -06:00
DottaandClaude Opus 5.5 81f12c21ec fix(server): document the safe company import request body in the API catalog (#19968)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A CEO agent can preview and apply a company package import through
`POST /api/companies/{companyId}/imports/preview` and `/imports/apply`
> - Agents find API operations and their request bodies in the OpenAPI
catalog
> - The catalog entries for these two safe import routes have no request
body and no description of the safe-route limits
> - An agent then offered the person a `replace` choice that the safe
route rejects
> - This pull request registers the existing preview and import body
schemas and describes the limits
> - The benefit is that agents can resolve name conflicts with `rename`,
`skip` and `nameOverrides`, and do not offer choices that fail

## Linked Issues or Issue Description

**What happened**

A person attached a company package in chat and asked a CEO agent what
the package would add. The package had an agent named Morgan, and the
company already had a Morgan. The agent previewed the import correctly.
It then offered three choices: `skip`, `rename`, or `replace`
("overwrites the existing Morgan"). The safe import route rejects
`replace`. The catalog entries for `/imports/preview` and
`/imports/apply` have no request body, so the agent could not see the
route's fields or limits. PR #19945 fixed the same gap for `/exports`.

**Expected behavior**

The catalog documents the import body, including `source`, `target`,
`collisionStrategy` and `nameOverrides`. It states that the safe route
rejects `replace` and never overwrites an existing agent.

**Steps to reproduce**

1. Read `GET /api/openapi.json`,
`paths["/api/companies/{companyId}/imports/preview"].post`.
2. See that it has no `requestBody`. The legacy
`/api/companies/import/preview` entry has one.
3. Call the safe route with `collisionStrategy: "replace"`. It returns
403.

## What Changed

- Register `companyPortabilityPreviewSchema` and
`companyPortabilityImportSchema` as the JSON request bodies of `POST
/api/companies/{companyId}/imports/preview` and `/imports/apply` in
`server/src/routes/openapi.ts`.
- Add a description of the safe-route limits: target, allowed collision
strategies, and `nameOverrides`.
- Add a regression test in
`server/src/__tests__/openapi-routes.test.ts`.

## Verification

- `cd server && pnpm exec vitest run
src/__tests__/openapi-routes.test.ts` passes (37 tests). The new test
fails on master because the entries have no `requestBody`.
- An end-to-end check runs the `companies.import-preview` chat eval with
this commit cherry-picked onto the eval branch.

## Risks

- Low risk. This changes only API documentation. The routes already
parse these schemas. Runtime behavior does not change.

## Model Used

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

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 22:35:16 -06:00
DottaandClaude Opus 5.5 952636e1d2 fix(agents): give the terminate denial a ready link to the agent's profile
An agent refused termination relayed the profile path as plain text. The
denial now carries a Markdown link to /<prefix>/agents/<id>, so the agent
can send the person straight to the Terminate control. POST /terminate now
resolves the agent before the board check, so the link names the target.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 22:32:43 -06:00
DottaandClaude Opus 5.5 ff9ee74c46 fix(issues): do not wake the assignee for an issue created already closed
Creating an issue with status done or cancelled and an agent assignee queued
an issue_assigned wake, so the agent got a run for finished work (and it
queued behind any run the agent already had). The update path already treats
restoring an assignee on closed work as not a reopen request, and create-time
fast responses already skip done and cancelled. Apply the same guard to the
create-time assignment wake.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 22:32:43 -06:00