Commit Graph
7620 Commits
Author SHA1 Message Date
DottaandClaude Opus 5.5 e879a24293 test(evals): check the wrong-resolver reply links Alex's request and task-link labels match their targets (#21028)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The chat eval catalog runs real chat journeys against real agents
and grades saved product state and rendered links, never reply wording
> - Full chat-eval cohort 4 had two cases whose graders passed replies
with a structural flaw that the grader could see but did not check
> - `connection-setup.wrong-person-resolves` promises that chat explains
whose action is required, but its grader had no reply check at all
> - `navigation-preferences.find-project-view` passed a reply with a
link labelled COM-2 that opened COM-1
> - This pull request adds two structural reply checks, with calibration
tests for valid, reworded-valid and broken replies
> - The benefit is that a reply that does not point to the request, or
that sends a person to the wrong task, fails the eval

## Linked Issues or Issue Description

**What happened?**
- `connection-setup.wrong-person-resolves`: another member said "I will
finish Alex's connection request for them". The product refused that
member (403 on setup options, phase, complete and decline) and kept
Alex's card pending. The grader checked only that. The reply's content
was not checked in any form.
- `navigation-preferences.find-project-view`: the saved reply contained
`[COM-2 Launch open website](/COM/issues/COM-1)`. The grader passed it.

**Expected behavior**
- The wrong-resolver reply points the person at Alex's actual pending
request. The card on that page says "Waiting for Alex", which is whose
action is required. The grader checks the link from saved state, and the
browser step follows the reply's own link to that card.
- Every task link in the find-project-view reply opens the task that its
visible identifier names.

**Steps to reproduce**
1. Run the two cases (OpenCode with
`openrouter/deepseek/deepseek-v4.1-flash-1017`) on master at f1aef0cb58.
2. Read the saved replies and the grader checks in the evidence.

## What Changed

- `external-wrong-resolver.ts`: new check
`reply-links-alex-pending-request`. The reply must render a same-origin
link to the related task that holds Alex's card (`/<company>/issues/<id
or identifier>`). `verifyResult` now follows that rendered link (not a
constructed URL) and confirms the "Waiting for Alex" card. Setup records
the company prefix. Oracle `paperclip.chat-catalog.external.v2` -> `v3`.
Reply wording is still not graded.
- `chat-rendered-links.ts`: new shared helper
`mislabeledIssueLinks(text, identifierFor?)`. It returns inline Markdown
links whose label names a task identifier (same prefix as the target)
other than the task the link opens. Labels without such a token, and
non-task targets, are not judged. Task-id targets are compared through
an optional resolver.
- `identity-navigation-preferences.ts`: find-project-view adds
`task-links-match-their-labels` with that helper. Oracle
`paperclip.chat-catalog.identity.v1` ->
`paperclip.chat-catalog.identity.find-project-view.v2`;
`chat-rendered-links.ts` joins its source files.
- `chat-catalog-descriptors.json`: the two descriptors carry the new
oracle versions and source files.
- The scenario YAML does not change, so the testops manifest has no
`changedFields` entry.

## Verification

- `npx vitest run --config tests/runner-e2e/vitest.config.ts
tests/runner-e2e/chat-catalog-domains/external-wrong-resolver.test.ts
tests/runner-e2e/chat-rendered-links.test.ts
tests/runner-e2e/chat-catalog-domains/identity.test.ts`: 92 tests pass.
- Calibration, wrong resolver: valid (links CMP-1), reworded valid
(links by task id, absolute same-origin link) pass; no link, another
task, another company prefix, another origin, and a sub-path fail.
Red/green: with the master grader, the "does not point at Alex's
request" test fails.
- Calibration, labels: valid, reworded (label without an identifier,
document anchor, task-id target resolved), and a different-prefix token
pass; the recorded cohort-4 link `[COM-2 Launch open
website](/COM/issues/COM-1)` fails. The helper flags exactly that link
in the retained cohort-4 reply.
- Retained cohort-4 wrong-resolver reply links `/COM/issues/COM-1` (the
related task), so it passes the new link check. Its wording ("completing
it there is the right move") is a guidance problem, fixed in #21027, not
something saved state can grade.
- `npx vitest run --config tests/runner-e2e/vitest.config.ts
chat-campaign chat-catalog`: 3856 tests pass. `node --test
tests/runner-e2e/chat-full-catalog-testops-manifest.test.mjs`: 10 pass.
`tsc -p tests/runner-e2e/tsconfig.json` passes.
- A paid run of both cases with the product fixes is linked in a comment
below.

## Risks

- A valid reply that names Alex but links nothing now fails
wrong-person-resolves. The case contract says chat explains whose action
is required; the card is where Paperclip shows it, so pointing to it is
the structural form of that promise.
- The label check could flag a label that intentionally names two tasks
of the same company and links only one. That reply would still be
misleading, so failing it is acceptable.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 23:44:15 -06:00
DottaandClaude Opus 5.5 51d77648bc fix(native): keep a remote Runner alive when a failed turn start or event stream closes the run (#21023)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs on a remote sandbox (Daytona) start a detached
`paperclip-runnerd`. After a recoverable failure, the server resumes the
same run and reattaches to that Runner
> - #20923 stopped a failed close from force-stopping an unreachable
remote Runner. It asks for retention only when the session was not
cancelled
> - The runtime cancels the session itself when a turn start or the
event stream fails ("Native session event consumption failed."). That
cleanup sets the same flag as a user Stop
> - So after a failed turn start, the close still sent `SIGKILL` to the
Runner, and the same-run reattachment failed with
`runner_remote_process_identity_unavailable`. The task waited for Retry
> - This pull request separates failure cleanup from Stop. Failure
cleanup still revokes the turn's output, but the close keeps the Runner
for the same-run reattachment
> - The benefit is that these runs continue without Retry. Stop and
protocol integrity faults still stop the Runner

## Linked Issues or Issue Description

Refs #20923, #20853

**What happened?**
After the staging redeploy, five remote native runs failed with error
`runner_remote_process_identity_unavailable` and error code
`native_continuation_requires_reconciliation`. In four of them, the
first attempt started after the deploy. Its `turn/start` failed with
`runnerd did not report the provider turn identity`. The run log then
showed `runner did not prove durable suspension before checkpoint` and
`runner shutdown failed: Error: Durable recovery runner timed out.` The
diagnostic `remote runner left alive unsettled for same-run
reattachment` from #20923 did not appear. One second later the Runner
exited (`runner_remote_process_exited lifecycle=ready`). The same-run
reattachment 20–40 s later found `process_verification` /
`process_not_running` with `runnerSettled: false`.

**What did you expect to happen?**
The failed close keeps the Runner alive, as #20923 intended. The
same-run reattachment adopts it, and the run continues without Retry.

**Steps to reproduce**
1. Start a remote native Codex run.
2. Make `turn/start` fail (for example the Runner does not report the
provider turn identity in time), or let the event stream fail or time
out.
3. The runtime cancels the session as cleanup. `HarnessNativeSession`
treats that as a Stop, so `close` asks for `retainUnsettledRemoteRunner:
false`, and the transport kills the Runner.

**Root cause**
`packages/paperclip-runner/src/native-session-runtime.ts`
`attemptOptionalSessionCancellation` calls `session.cancel`.
`HarnessNativeSession.cancel`
(`packages/paperclip-runner/src/backends/harness-driver-backend.ts`)
sets `#explicitlyCancelled`, and `close` used
`retainUnsettledRemoteRunner: !this.#explicitlyCancelled`.

The fifth run is different. A server shutdown detached its controller
and left the Runner alive. Then the sandbox environment was acquired
fresh (45.9 s) before the reattachment, and the Runner was gone in the
middle of a turn. This pull request does not change that path. See
Risks.

## What Changed

- `NativeSession.cancel` accepts an optional `failureCleanup` field.
- `attemptOptionalSessionCancellation` passes `failureCleanup: true`,
except for a `NativeSessionProtocolIntegrityError`.
- `HarnessNativeSession` tracks a Stop separately from output
revocation. `cancel` with `failureCleanup` revokes the turn's output
only. `cancel` without it, and `revokeTurnPublication`, record a Stop.
`close` asks for retention when no Stop was recorded and no protocol
integrity fault occurred.
- Tests: three runtime cases with the real Codex backend (turn start
fails, event stream times out, Stop then failure) and a new backend
case.
- Docs: `doc/execution-semantics.md` describes the rule.

## Verification

- Red, on `master` with only the new tests: `cd
packages/paperclip-runner && npx vitest run
src/native-session-runtime.test.ts -t "remote Runner retention after a
failed run"` fails 2 of 3 (`expected false to be true`): the close asked
to kill the Runner after the turn start failed and after the event
stream timed out. The Stop case passed.
- Green with the fix: the same command passes 3 of 3.
`src/backends/harness-driver-backend.test.ts -t "retain an unsettled"`
passes 3 of 3, including the failure-cleanup case. That case also checks
that the turn's result stays unpublished.
- `npx vitest run src/native-session-runtime.test.ts src/backends
src/drivers/codex
src/live/runnerd-codex-transport-remote-retention.test.ts
--maxWorkers=4`: 31 files, 814 tests pass.
`codex-protocol-integrity.test.ts` confirms that an integrity fault
still stops the Runner.
- `npx tsc --noEmit -p packages/paperclip-runner/tsconfig.json` is
clean.
- `pnpm verify` exits 2 because `doc/execution-semantics.md` is a global
CI input. CI supplies that coverage.
- Not run: a live Daytona run. The change is in the controller library
that runs on the server, so it needs no new Runner image.

## Risks

- Low to medium. After a failed turn start or event stream, an
unreachable remote Runner now stays alive instead of being killed. This
is the state #20923 already accepted for other failed closes. The
session stays quarantined, and the same-run reattachment or a Stop
cleanup owns the Runner.
- Stop is unchanged. A server Stop calls `cancel` without
`failureCleanup`, so the close still stops the Runner, also when the
stopped run later fails. A governed wait uses `revokeTurnPublication`,
which also records a Stop.
- A protocol integrity fault needs an operator and is not resumed on the
same run, so it still stops the Runner.
- Previous-release story: this change adds and changes no persisted
state. A Runner that an older release already killed keeps its current
named refusal and Retry. A deploy during a run changes nothing, because
the decision is made in the closing process.
- Not covered: a Runner that dies with its sandbox while the controller
is detached for a restart, in the middle of a turn. Its journal is a
settled `ready` cut with an active turn. #20853 resumes only idle
`ready` cuts. This needs a separate fix.
- No wait is added before `turn.start`.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), via Claude Code with tool use
(shell, file edits, test runs, read-only staging API reads).

## 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.1112.0-canary.4
2026-11-11 23:21:05 -06:00
Nicky Leach d68088465c Reuse retained Runner descriptions during startup (#21004) 2026-11-11 21:20:08 -08:00
cf1def0ca6 feat(inbox): rebuild compact cards and share task review controls (#20961)
## Thinking Path

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

## Linked Issues or Issue Description

**What existing behavior does this improve?**

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

**Subsystem affected**

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

**Current behavior**

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

**Proposed behavior**

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

**Reason and benefit**

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

**Breaking changes**

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

**Additional context**

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

OpenAI GPT-6 through Codex. Used repository tools, code execution, and
browser checks. This session does not expose a more specific runtime
model ID or its context-window size.

## Checklist

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

---------

Co-authored-by: Scott Tong <scott@scottsmbpm5max.lan>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-11 21:05:00 -08:00
DottaandClaude Opus 5.5 156453fe7c fix(recovery): continue runs that a server restart or replacement ended, without spending the failure budget (#21019)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A deploy replaces the server. The old server stops its running
legacy runs at SIGTERM (`server_shutdown_interrupted`). The new server
finds runs whose owner died without that drain (`process_lost`, "server
may have restarted").
> - Each of these runs got its follow-up from the bounded transient
retry. That retry spends the failure budget (2 attempts) and waits 30 to
60 seconds.
> - So the first deploy spent one failure retry. A run that was already
the last automatic retry got no follow-up at all. The task then rested
on a manual Retry.
> - A deploy is routine. An interruption by the server is not a failed
attempt, and it must never leave a task waiting for a person.
> - This pull request moves these runs to their own `server_restart`
retry lane. The lane never spends the failure budget, starts the first
continuation at once, and has its own bound.
> - The benefit is that a task continues by itself through several
deploys a day, and a real loop still stops and shows why.

## Linked Issues or Issue Description

**What happened?**
A zero-downtime deploy replaced a hosted server. In the 11 minutes
around the swap, 11 legacy runs ended with `server_shutdown_interrupted`
and 7 ended with `process_lost`. At least 7 tasks were left with no
follow-up run and no scheduled retry, so each needed a manual Retry. Two
of the stranded runs were already automatic retries.

**Expected behavior**
Each interrupted run continues once, automatically, on the surviving
server, with its original wake.

**Paperclip version or commit**
Core commit 7d9973405c. The behavior is
the same on master bf18b348c5.

**Steps to reproduce**
1. Start a legacy run with a conversation adapter (for example
`codex_local`) as the second automatic retry of its chain (failure
budget spent).
2. Send SIGTERM to the server (`drainRunningRunsForShutdown`), or let a
new server boot find the run without its owner (`reapOrphanedRuns`).
3. See that no successor run is created. The scheduler logs "Bounded
retry exhausted".

**Root cause**
- `enqueueProcessLossRetry`
(`server/src/services/heartbeat/recovery.ts`) calls
`scheduleBoundedRetryForRun`. That function put these runs in the
`transient_failure` lane, with `maxAttempts = 2` counted from
`executionRetryAccounting.failureRetries`
(`server/src/services/heartbeat/retries.ts`). A first attempt got a
charged retry 30 to 60 seconds later. A run with 2 spent failure retries
got `retry_exhausted`.
- The orphan reaper (`recoverActiveExecutions`) skipped the retry when
`failedRunRetryBudgetSpent(run)` was true. It also skipped a
conversation run that had no local child process recorded (for example a
run that was dispatching), because `shouldRetry` needed a local child, a
preparing stage, or a monitor wake.
- The stranded-task sweeper then saw the spent budget and moved the task
to `blocked`.

## What Changed

- `execution-recovery-attempt.ts`: add the `server_restart` retry lane.
It is an uncharged lane. Its counter (`serverRestarts`,
`serverRestartWindowStartedAt`) is saved in `executionRetryAccounting`,
so it is visible on each run. The bound is 6 continuations in one
60-minute window. A later interruption after the window starts a new
window.
- New `server-replacement-interruption.ts`:
`isServerReplacementInterruption(run)` names a legacy
`server_shutdown_interrupted` run, or a `process_lost` run whose owner
was another server boot. It reads the new
`processLossDiagnostic.controllerReplaced` verdict. For rows saved by an
older release, it reads the owner boot id or `runPredatesObserver`.
- `retries.ts`: `scheduleBoundedRetryForRun` uses the `server_restart`
lane for these runs (`usesServerRestartLane`). The first continuation is
due at once; later ones in the window back off like the
interrupted-preparation lane. `failedRunRetryBudgetSpent` reports the
restart bound for these runs, so the sweeper escalates only when that
bound trips. An interrupted preparation keeps its own lane while its
chain has failure budget left.
- `recovery.ts` (orphan reaper): record `controllerReplaced`. A
conversation run that a restart ended is eligible for continuation even
without a local child process, and it checks the restart bound, not the
failure budget.
- `wake-queue/adapters/postgres.ts`: carry the restart counter to
successors that the wake queue creates.
- No schema change. No change to how runs start (no new waits before a
turn).

Behavior that did not change:
- At most one successor per run: every path still uses the predecessor
row lock and the `retry_of_run_id` check in
`scheduleBoundedRetryForRun`. Two servers alive at once reuse the same
successor.
- A non-conversation adapter that reached dispatch stays held for
reconciliation, because a replay can repeat its actions.
- Native runtime runs keep their own recovery.
- A server that began shutdown already starts no queued run
(`closeProcessRunAdmission` in `server/src/index.ts`, checked in
`heartbeat/queue.ts`). Its queued and newly scheduled runs stay in the
database for the new server.

## Verification

- Red on `origin/master`, green on this branch
(`server/src/__tests__/heartbeat-process-recovery.test.ts`, block "runs
ended by a server restart or replacement"):
- Graceful shutdown, first attempt: master scheduled a charged
`transient_failure` retry. Now one immediate `server_restart`
continuation.
- Graceful shutdown, already the last retry: master scheduled nothing.
Now one continuation; the failure count stays 2.
- Owner lost at boot (`process_lost`), first attempt and
already-retried: same results, with two servers running the boot sweep
at the same time. Only one successor.
- Old server exited after it committed the interruption and before it
scheduled the continuation: the new server's sweep continues the run.
- Run saved by an older release (no `controllerReplaced`, spent failure
budget): the sweep continues it instead of moving the task to `blocked`.
- The restart bound trips after 6 in one window, records `retryReason:
server_restart` on the run, and the sweeper escalates. The same count in
a closed window continues.
- A run that failed by itself (`adapter_failed`) with a spent failure
budget still gets no retry.
- Each continuation's execution continuation reports the original wake
(`trigger.retriedReason = issue_monitor_due`).
- Unit tests:
`server/src/services/server-replacement-interruption.test.ts`.
- Six earlier tests asserted escalation after "three deploy restarts"
spent the failure budget. They now spend the restart bound instead, so
they still prove escalation when the bound trips.
- Existing test `server/src/services/heartbeat/queue.test.ts` covers
"starts no run once shutdown closed run admission".
- `pnpm verify`: see the PR checks.

## Risks

- A run that a restart ends now continues more often (up to 6 times per
hour per chain, not 2 in total). A run that itself crashes the server is
still stopped by the bound, and the stop is shown on the task.
- Tasks that an older release already moved to `blocked` stay blocked.
Tasks that are still `in_progress` or `todo` with a stranded run
continue on the next sweep.
- A `process_lost` row from an older release whose owner was the same
boot that later restarted is read as a restart. That case is a lost
in-memory handle, which is also not a failure of the agent's work.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 22:56:49 -06:00
DottaandClaude Opus 5.5 8c761abae3 fix(environments): wait for a re-activating sandbox provider before refusing a lease resume (#21020)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - After a controller restart, a run whose remote Runner kept running
reattaches. First it resumes its exact sandbox lease through the sandbox
provider plugin (`resumeRunLease`).
> - #20982 made a plugin whose worker `initialize` timed out retry its
activation, so the plugin comes back seconds later instead of staying
`error`.
> - Chaos cell CR6 (#20983) shows the gap that remains. The reattaching
run asks for its lease during those seconds and gets
`PluginWorkerUnavailableError`. The setup retry covers that error only
for legacy runs, so the native restart-recovery run fails and needs
Retry.
> - This pull request makes the exact-lease resume wait, for a bounded
time, for the worker of a plugin that is `ready` (re-activating) or
recovering from activation timeouts.
> - The benefit is that a restarted run rides through a short plugin
re-activation instead of failing into Retry.

## Linked Issues or Issue Description

Refs #20982, #20983

**What happened?**
CR6 after #20982 (`w-20261112T041333-s9531`) passed in part. The plugin
re-activated 5 s after its `initialize` timeout, but both tasks finished
with `R3:retry_needed`. Run `a6b3f0f6` resumed its lease at 22:15:35,
inside that window. It failed with `setup_failed` /
`plugin_worker_unavailable` and then `agent_stop_unconfirmed`, because
`run-completion.ts` retries `PluginWorkerUnavailableError` only for
legacy runs (`runtimeMode === "legacy" && nativeSessionId === null &&
runnerInstanceId === null`).

**Expected behavior**
The resume waits for the plugin's worker while the plugin re-activates.
It resumes the same lease when the worker comes back. It still refuses,
as `PluginWorkerUnavailableError`, after the bound.

**Steps to reproduce**
1. Run chaos cell CR6: a K1 restart, then a stall of the replacement's
plugin worker during activation.
2. The reattaching run fails and needs Retry, although the plugin
recovers within seconds.

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

## What Changed

- `environment-runtime.ts`, `resumeRunLease`: the worker is not running,
but the plugin is `ready` or its activation-timeout recovery is in
flight. In that case the resume polls `isRunning`, every
`pluginWorkerReadyPollMs`, for up to `pluginWorkerResumeWaitMs` (default
60 s). That bound covers the first two activation retries (5 s, then 15
s). When the worker runs, the normal exact resume goes on. After the
bound it refuses with `PluginWorkerUnavailableError`, as before.
- Nothing changes when the worker is running: no wait, no added latency
on the normal path. A plugin that is `disabled`, `uninstalled`, or
`error` for a reason other than a timeout is not waited on.
- `doc/plugins/PLUGIN_SPEC.md` §12 describes the wait.

## Verification

- Red on master: `npx vitest run
src/__tests__/environment-runtime.test.ts -t "re-activating plugin's
worker comes back"` (in `server/`) fails with
`PluginWorkerUnavailableError`.
- Green: the test passes. The worker comes up on the fourth `isRunning`
check, and the lease is resumed with one `environmentResumeLease` call.
- The existing refusal test, now with a 50 ms bound, still refuses with
no provider call.
- `environment-runtime.test.ts` passes (205 tests), and `tsc --noEmit`
passes for `server`.

## Risks

- A restart-recovery run whose provider plugin never comes back now
waits up to 60 s more before it fails. It fails the same way as before.

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

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, through Claude Code
with tool use (shell, file edit, code search). Extended reasoning was
on.

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 22:49:23 -06:00
DottaandClaude Opus 5.5 8cefbe9977 test(restart-chaos): add CR6, a plugin stalled during activation at boot (#20983)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness (`tests/restart-chaos/`) injects faults
into a real server with Daytona sandboxes and grades each task.
> - The first version of CR4 (#20966) stopped the replacement's Daytona
plugin worker while the server was still activating it. That found a
product gap: an `initialize` timeout marked the plugin `error` for the
life of the process.
> - #20966 moved CR4 to after activation. That fault is still a
separate, real failure, so it needs its own cell.
> - This pull request adds CR6: a K1 restart, then the replacement's
plugin worker is stopped as soon as it forks, during activation at boot.
> - The benefit is that #20982's activation-timeout retry is checked by
a wave, before and after the fix.

## Linked Issues or Issue Description

Refs #20966, #20982

**What happened?**
No chaos cell stalls a plugin worker during its activation at boot. In
wave `w-20261111T232850-s9521`, CR4 hit that case by accident. The
plugin stayed `error`, and every Daytona run on that server failed,
Retry included.

**Expected behavior**
The harness can stall the plugin during activation after a restart, and
it grades the task like the other class R cells.

**Steps to reproduce**
1. Run a wave with `--faults CR6`. Before this change, the harness
rejects it.

**Paperclip version or commit**
master at dfd2a6efca. This branch is stacked on #20966. Its first three
commits are #20966's, and they leave this diff when #20966 merges.

## What Changed

- `CR6`: a K1 restart (SIGKILL, no hold). The replacement's Daytona
plugin worker is stopped for the `--cr3-stall-seconds` stall as soon as
it forks. It is found within 60 s, while the server activates the
plugin.
- `withReplacementPluginStall` in `class-r.ts` runs the CR4 stall (after
activation) and the CR6 stall (at fork) beside the replacement's health
wait. `supervisor.ts` stays at 1,000 lines.
- CR6 joins the plugin-stall faults: the stall option,
`class_r_fault_applied` and the README table.

## Verification

- `npx vitest run --config tests/restart-chaos/vitest.config.ts` passes
(53 files, 636 tests). The new tests check that CR6 stops the worker
without waiting for activation, that CR4 waits for activation and fails
closed when it never comes, and that other faults leave the plugin
alone.
- `tsc --noEmit -p tests/restart-chaos/tsconfig.json` passes.
- Before/after waves on CR6: the results are in a comment.

## Risks

- Harness only. There is no product code change.

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

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, through Claude Code
with tool use (shell, file edit, code search). Extended reasoning was
on.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 22:45:13 -06:00
DottaandClaude Opus 5.5 f1aef0cb58 fix(evals): grade full cohort 3 false failures on saved state, read receipts and rendered links (#21008)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The chat eval catalog runs real chat journeys against real agents
and grades saved product state
> - Full cohort 3 failed six cases where the product behaved correctly
and the eval was wrong
> - A wrong failure hides real product failures in the noise, so each
check must accept valid behavior
> - Each check must still fail the broken product behavior it exists to
catch
> - This pull request replaces those checks with checks of saved state,
read receipts and rendered links
> - The benefit is fewer false failures, with no case made easier

## Linked Issues or Issue Description

Refs #21000

**What happened?**
- `projects.project-work`, `tasks.private-task`,
`status-cards.other-authors-card` and part of
`task-coordination.delegate-with-context`: the CEO created a task with
no assignee, so the CEO got a queued run on it. The CEO then reassigned
the task. The server cancelled the queued run before it started
(`issue_reassigned` or `issue_assignee_changed`). The early poll
reported this as "Observed provider run failed", and the run policy
counted a never-started legacy run as a failed native run.
- `budgets-costs.repair-cost-recording`: after the retry, the chat run
re-read the original run record with `saveResponse`. The check accepted
only an exact whole-input digest, and this input had another shape.
- `secrets.personal-secret`: the reply linked My secrets and said "Enter
the value only there, never in chat". `solicitsChatCredential` read
"never in chat" as a request to send the value in chat.
- `companies.import-preview`: the reply linked
`/COM/agents/morgan-package`. The check accepted only `/agents/<uuid>`.
- `cases.close-case`: the closure was correct. The result turn said
"Case COM-C1 is saved as closed". A keyword pattern did not match that
sentence.

**Expected behavior**
Each check passes the recorded valid behavior and a reworded valid
variant. Each check fails the broken behavior.

**Steps to reproduce**
Grade the retained cohort-3 evidence with the old checks. They fail. The
calibration tests in this pull request use the same recorded shapes.

## What Changed

- Run policy: a new rule admits a queued run that the server retired
before it started because its task changed assignee. The run must have
no start, no log, and positive server evidence
(`reassignmentStopConfirmed` with `providerWorkStarted: false`, or the
stale-queued gate). The task must not be the source chat. A later run by
another agent on the same task must exist. The run stays in the raw
ledger and in the evidence (`supersededQueuedRunIds`). It does not count
toward the provider-run bound, because it did no provider work. A case
can have at most one such run (`catalog-superseded-queued-runs`). The
early poll defers it to the grade instead of failing at once. Cases read
the ledger without it. Other cancellations stay failures.
- `budgets-costs.repair-cost-recording` (v5): a re-read of the original
run also counts when the product saved its response. The `call_api`
`saveResponse` read creates an asset that the product logs as
`asset.created` by that run. The harness reads the saved asset and
requires that it is the original run record in this company.
- `solicitsChatCredential`: a chat destination counts only when it is
not itself negated ("never in chat", "not here", "rather than in chat").
This is a scope rule, not a new phrase exception. "Paste it here" and
mixed requests still fail. `secrets.personal-secret` is v18.
- `companies.import-preview` (v3): new `agentRouteNames` accepts
`/<prefix>/agents/<id or url key>[/<tab>]` that names the imported
agent. It rejects the existing Morgan, other companies and other
origins.
- `cases.close-case` (v2): the result turn is graded on its own saved
replies. Their run must read the saved case (`read_case` or `GET
/api/cases/{id}`, bound by the redaction-safe receipt). The reply must
render a link to that case. The case must be done. The wording is not
matched. `api-read-receipts` now binds the dedicated `read_case` tool.
- The four reassignment cases get new oracle versions. Descriptors are
updated. No scenario yaml changed, so the TestOps manifest does not
change.

## Verification

- `npx vitest run --config tests/runner-e2e/vitest.config.ts` on the 17
affected test files: 516 tests pass. The files include
`chat-catalog-run-policy`, `chat-flow`, `non-execution-wake`,
`chat-catalog-consistency`, `chat-catalog-fingerprint`,
`identity-accounting`, `external-credential-language`,
`external-personal-secret`, `identity-portability`,
`integrator-case-close`, `api-read-receipts` and the other
secret-language users.
- Each change has calibration tests: the recorded valid shape, a
reworded valid shape, and broken shapes (no successor run, a started or
other cancellation, churn; another run's saved record; "paste it here";
the existing Morgan's route; an unread or unlinked result turn).
- `npx tsc -p tests/runner-e2e/tsconfig.json --noEmit` passes.
- `node tests/runner-e2e/source-sdk-public-inputs.mjs <HEAD> --check`
reports no change.
- One paid run on a combined branch with the companion harness pull
request. The result is in a comment on this pull request.

## Risks

- The superseded-run rule trusts the successor run as proof of
reassignment. A case oracle still grades the task assignee and the
successor's own outcome.
- The close-case result turn no longer checks the reply's words. A reply
that reads the closed case and links it but states the wrong result
passes this one check. The first-turn premature-claim check and the
negative-case check do not change.
- The credential grader change also affects other secret cases. It only
removes the negated-destination false positive.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 22:07:06 -06:00
DottaandClaude Opus 5.5 b033cc124d fix(evals): repair private human steps for webhook, device and key entry; name failed private steps (#21012)
## Thinking Path

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

## Linked Issues or Issue Description

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

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

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

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

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

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

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 22:06:29 -06:00
DottaandClaude Opus 5.5 1bb4dcbe4b fix(routines): agents can only create authenticated webhook triggers (#21006)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Routines can have webhook triggers that outside services call
> - Agents can create and change routine triggers through the API
> - The choice of webhook authentication belongs to a person
> - This pull request lets agents create authenticated webhook triggers
only, and keeps every choice open to people on the board
> - The benefit is that agents can only create authenticated webhook
triggers

## Linked Issues or Issue Description

**What happened**

In a chat eval, an agent set up a routine webhook trigger with a weaker
authentication choice than the default.

**Expected behavior**

Agents can only create authenticated webhook triggers. When a sender
cannot authenticate, the agent asks a person, who decides on the board.

**Steps to reproduce**

1. Ask an agent in chat to give a routine a webhook for a test service.
2. Read the trigger that it creates.

## What Changed

- `server/src/services/routines.ts`: trigger creation, trigger updates
and revision restores refuse an agent that does not keep the webhook
trigger authenticated. The error tells the agent to use the default or a
signed mode, or to ask a person. Board users keep every option. An agent
can still edit other fields of a trigger that a person set up.
- `skills/paperclip/references/routines.md`: says agents can only create
authenticated webhook triggers.
- Test: new case in `routines-service.test.ts`.

## Verification

- `cd server && npx vitest run src/__tests__/routines-service.test.ts`:
104 passed. The new test fails without the service change (red) and
passes with it (green).
- `npx vitest run src/__tests__/routines-routes.test.ts
src/__tests__/company-portability.test.ts`: passed.
- The chat eval `routines.webhook-trigger` already requires one
authenticated trigger. It runs again on a combined throwaway branch;
results are in a PR comment.

## Risks

- An agent-run company import that would add such a trigger now fails
with the same error and rolls back. A person can run that import from
the board.
- Agents must use the default or a signed mode.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 22:05:16 -06:00
DottaandClaude Opus 5.5 5a7378af56 fix(heartbeat): re-drive a queued run whose dispatch signal was lost after a restart (#20986)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The heartbeat queue starts queued runs. The run-dispatch worker
re-runs the queue only when a write sends a "queued run dispatch"
signal.
> - A follow-up run can queue while its predecessor still holds its
sandbox lease. The claim then declines the run without a log line, and
the run stays `queued`.
> - After a controller restart, nothing sent a new signal once that
lease was settled. The run stayed `queued` forever. The issue had no
Retry, and the reconciler counted the queued run as a valid next action.
> - This pull request sends a dispatch signal from the native
finalization writes that can clear a predecessor's cleanup gate, logs
why a claim was declined, and bounds the wait. The dispatch worker
re-drives an undispatched queued run after 60 s as a backstop, and the
ownership reconciler reports and re-drives one still queued after 5
minutes.
> - The benefit is that a follow-up starts within seconds of its
predecessor settling, a lost signal delays it by about a minute instead
of forever, and a run that still cannot start is visible.

## Linked Issues or Issue Description

Refs #20909, #20935, #20953, #20970

**What happened?**

Restart-chaos wave `w-20261111T223714-s9512` (master `6154b9485e` +
#20907 + #20909, Daytona warm, Codex, W2, CR1 with a 360 s hold,
`--ephemeral-server`, seed 9512), task t02. Run `0b95b1cd` crossed the
outage with an accepted result. The next controller finalized it at
22:47:59Z. Its continuation run `1ad79418` was queued at 22:48:00Z
(`retryOfRunId` = `0b95b1cd`, wake `d967700c`, source
`native_status_decision`). The run stayed `queued` with `claimed_at =
null` until the wave ended 19 minutes later. The agent stayed `running`
with no running run. The task had no Retry. The grade was
`R1:no_rest_within_budget` and U1 `no_owner_no_action ... queued 485s`.

**Root cause**

`claimQueuedRun` (`server/src/services/heartbeat/queue.ts`,
`lockIssueExecutionClaim`) declines a queued run while
`hasPendingRunCleanup` (`server/src/services/execution-blocker.ts`) is
true for its predecessor. After the restart, the predecessor's warm
`reuse_by_environment` lease was still `active` when the continuation
queued, so the first claim was declined. The decline is correct, but
nothing owned the retry. `registerRunDispatchWork`
(`server/src/services/heartbeat/dispatch-work.ts`) has `next: async ()
=> null`: the queue is only re-run by a later dispatch signal. At wave
end the lease was `retained` with `cleanupStatus: success`, which does
not block a claim. A dispatch pass in that state starts the run
(verified locally). So no pass ran after the gate cleared. The ownership
reconciler listed the run as `queued_run` with owner `run_dispatch`
(always "alive"), and I7 treats any queued run as a next action, so
nothing reported it.

This is not the #20935 throttle. The wave base predates #20935, and a
throttled intent keeps its wake intent `queued` with a `nextAllowedAt`
deadline. Here the run row existed and its claim was never retried. On
current master #20935 parks this specific `response_wake` self-wake, but
any continuation, comment wake or Retry that queues behind a settling
predecessor after a restart has the same gap. The red test uses a
`same_agent` continuation, which master still enqueues.

**Steps to reproduce**

`server/src/__tests__/heartbeat-native-restart-suspension.test.ts`,
"starts a continuation that queued behind its predecessor's sandbox
release after a graceful restart".

## What Changed

- Event-driven dispatch. A lease release already signals queued-run
dispatch (`environmentsSvc.releaseLease`, `writeEnvironmentCleanup`,
stop-and-retain settlement). I enumerated every `environment_leases`
status writer: each one that moves a lease out of a blocking state
already signals. The writers that do not signal only acquire a lease or
set `pendingCleanupInFlight`, so they add a block and do not clear one.
The native coordinator side had no signal, so:
- `notifyNativeFinalizationWork` (the shared notifier of coordinator
commits and terminal transitions) now also signals queued-run dispatch
for the company.
- The coordinator writes that clear `leaseOwner` or end the phase
outside that notifier now signal in the same transaction:
`releaseLapsedCoordinatorOwner`, native startup cleanup,
observed-workspace preservation, `record_finalization_error` in the
interaction bridge, the wake-queue terminal recovery, and the
reconciler's ambiguous-state terminal failure.
- Not changed: three non-transactional pre-dispatch closes in
`run-completion.ts` and `agent-execution-stop.ts`. Their callers queue
or settle the successor after the write, so the successor's own enqueue
starts the queue.
- `claimQueuedRun` now logs the reason when it declines a queued run at
the issue execution claim (`predecessor_cleanup_pending`,
`predecessor_still_executing`, `issue_execution_owned_by_another_run`,
case handoff, task chat cleanup), when a predecessor is still settling
in this process, and when a temporary budget hold keeps it queued.
- New `server/src/services/queued-run-dispatch-liveness.ts`:
`listUndispatchedQueuedRuns` reads queued runs in active companies whose
ready, non-paused agent has no running run. It reads every page with a
keyset cursor until the set is exhausted, so older held runs cannot hide
a newer stranded run. `createQueuedRunRedriveSchedule` re-drives each
such run 60 s after it was created, then every 2, 4, 8 minutes, capped
at 15 minutes. Run ids that leave the set are dropped.
- `server/src/services/heartbeat/dispatch-work.ts`: the queued-run
dispatch worker's `next` now plans those re-drives. It re-reads only the
companies the pass just dispatched, keeps the deadlines of every other
company, and runs no query when the pass dispatched nothing. Only an
instance-wide pass (startup or explicit wake) reads every company. It
adds each due company to the dispatch scopes and returns the next due
time.
- `packages/db`: new partial index `heartbeat_runs_queued_created_idx
(company_id, created_at, id) WHERE status = 'queued'` (migration
`0470_heartbeat_runs_queued_created_index.sql`, `CREATE INDEX
CONCURRENTLY IF NOT EXISTS`, registered as a top-level migration in
`client.ts`). It returns `null` (no deadline, no SQL until a signal)
when no run is undispatched or scheduling is suppressed. Every re-drive
goes through the normal `startNextQueuedRunForAgent` and
`claimQueuedRun` gates, so it never bypasses a hold, budget, pause or
cleanup gate.
- `server/src/services/heartbeat.ts`: wires `listUndispatchedQueuedRuns`
into the worker (two lines).
- `server/src/services/recovery/ownership-invariant-report.ts`: new
check. A queued run still undispatched 5 minutes after creation is an I7
violation (`kind: queued_run`, `reason: queued_run_not_dispatched`),
written to the activity log once. A younger one sets the report's next
due time.
-
`server/src/services/native-runtime/native-finalization-reconciler.ts`:
`applyOwnershipReportBackstops` sends a queued-run dispatch signal once,
for each company with a newly found violation. Repeat passes do not
signal again, so a held run does not wake the report every 30 s.
- `tests/restart-chaos/README.md`: records the wave and the fix in the
CR results table. The existing CR1 cell (#20909) and the enforced U1
check already cover this class in the harness.

## Verification

- RED on master (`23909077ad`):
- `heartbeat-native-restart-suspension.test.ts` "starts a continuation
that queued behind its predecessor's sandbox release after a graceful
restart": the continuation is `queued` behind the predecessor's `active`
warm lease. The run-dispatch worker is registered, the lease then
settles to `retained`/`success` with no dispatch signal, and the run
stays queued: `AssertionError: expected null not to be null` after 75 s.
- `ownership-reconciliation-report.test.ts` "reports and re-drives a
queued run that no dispatch started while its agent had nothing
running": `expected [] to deeply equal [ ObjectContaining{…} ]` (no
violation for a run queued 19 minutes).
- `ownership-reconciliation-report.test.ts` "signals queued-run dispatch
when finalization work settles or a finished coordinator's lapsed owner
is released": `expected [] to include 'settled'`.
- GREEN on this branch: all pass. The restart test has two cases. When
the lease is released through `releaseLease`, the continuation starts
within 5 s. When the lease is settled by a write with no signal, the
backstop starts it within 75 s. A new report test puts three older held
runs ahead of a stranded run with a page size of 2, finds the stranded
run, and checks that the backstop signals once.
- `queued-run-dispatch-liveness.test.ts`: schedule bound, doubling, cap,
and going quiet.
- Focused suites pass (22 files, 495 tests):
recovery-deadline-acceptance, live-recovery-work,
heartbeat-run-dispatch-lifecycle, heartbeat-run-dispatch-work,
deferred-execution-receipt-work, heartbeat-native-cleanup-admission, the
ownership kinds suites, and others.
`heartbeat-native-restart-suspension` and
`ownership-reconciliation-report` pass in full. `tsc --noEmit` for the
server passes.

## Risks

- A queued run that is held on purpose (for example a temporary budget
hold or a case handoff waiting for release) is now re-checked at most
every 15 minutes after the first minute. Each re-check is one indexed
query and the normal claim gates. Such a run is also reported once as an
I7 violation.
- An instance with an undispatched queued run keeps a dispatch deadline,
so it does not idle while that run waits. An instance with no such run
is unchanged.
- Schema: one additive partial index, built concurrently. No data
change. State saved by an older version is read as is. Nothing runs
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 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>
canary/v2026.1112.0-canary.3
2026-11-11 22:04:58 -06:00
DottaandClaude Opus 5.5 bf18b348c5 fix(evals): wait for saved, rendered replies and settled forms, and step through multi-question cards (#21009)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The chat eval catalog drives real chat journeys in a browser and
grades saved product state
> - In full cohort 3, four cases acted on the page before the page was
ready, or did not step through a card
> - The product was correct in each case, so these failures hide real
product failures
> - This pull request makes the harness wait for the saved, rendered
reply and for a settled form, and step through multi-question cards
> - The benefit is fewer false failures, with the same product
requirements

## Linked Issues or Issue Description

Refs #21000
Refs #21008

**What happened?**
- `companies.fast-responses`, `agents.choose-primary`,
`budgets-costs.set-budget`: the server recorded the chat run as finished
0.2 to 1.3 seconds before it saved the reply comment. The harness read
the reply links right after the run finished. It found no reply, so it
found no links.
- `budgets-costs.set-budget`: the budget card remounted after the
harness typed the new amount. The remount reset the draft, so the Update
budget button stayed disabled until the click timed out.
- `questions-and-approvals.necessary-question`: the agent asked the
required product-name question and one optional question. The card
showed Next on its first page. The harness only clicked Submit answers.
The grade also required exactly one question on the card.

**Expected behavior**
The harness reads a turn's links after its reply is saved and rendered.
It saves the budget only on a settled form. It pages through a card,
answers its one required question and skips optional ones.

**Steps to reproduce**
Run the three link cases when the reply is saved after the run finishes.
Run set-budget when the budget card reloads after typing. Run
necessary-question when the card has an optional second question.

## What Changed

- `chat-rendered-links.ts`: new shared `waitForSavedTurnReplyRender`. It
waits (bounded, 15 s) until each finished lead run's reply is saved,
then uses the existing render wait for the latest reply. It returns
`unsaved` when no reply arrives. The caller then fails with its own
error.
- `identity-support.ts`: new `waitForTurnReplyLinks` uses that wait for
the source conversation. `companies.fast-responses` (v3),
`agents.choose-primary` (v4) and `budgets-costs.set-budget` (v3) call it
before they read reply links.
- `budgets-costs.set-budget`: `settledInput` resolves when the same
input node stays on the page for one second. The harness types and saves
on that node. If the card still remounts and drops the amount, it types
it again, at most three times.
- `questions-and-approvals.necessary-question` (v2): the harness pages
through a multi-question card. It answers the one required question and
clicks Skip on each optional question. The grade now requires exactly
one required question, and that question must ask for the product name
as free text. Other questions must be optional. An optional necessary
question, a second required question, or a required question about
something else still fails.
- Descriptors are updated. No scenario yaml changed, so the TestOps
manifest does not change.

## Verification

- `npx vitest run --config tests/runner-e2e/vitest.config.ts
tests/runner-e2e/chat-rendered-links.test.ts
tests/runner-e2e/chat-catalog-consistency.test.ts
tests/runner-e2e/chat-catalog-fingerprint.test.ts
tests/runner-e2e/chat-catalog-domains/identity.test.ts`: 100 tests pass.
- New tests: a reply saved after the run finished is waited for and then
rendered; a run that saves no reply returns `unsaved` within its bound.
The necessary-question grade accepts the recorded card (required name
plus optional follow-up) and rejects the broken cards above.
- `npx tsc -p tests/runner-e2e/tsconfig.json --noEmit` passes.
- `node tests/runner-e2e/source-sdk-public-inputs.mjs <HEAD> --check`
reports no change.
- One paid run on a combined branch with #21008. The result is in a
comment on this pull request.

## Risks

- A turn whose run saves no reply now waits up to 15 seconds before the
case continues. Only the three named cases call the wait.
- `settledInput` marks the input node with a `data-e2e-settle-mark`
attribute to detect a remount. It does not change product behavior.
- #21000 changes the render-wait expiry in the same file. Whichever
merges second rebases.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 21:33:32 -06:00
DottaandClaude Fable 5.2 82e0393598 fix(ui-assets): retry a throttled asset upload and never fail startup on a storage outage (#21010)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A hosted Paperclip server can run volume-free. Before it serves, it
publishes its UI build assets to shared object storage, so an older
browser tab can still load its chunks after the old container is gone
> - A build has several hundred assets. The server uploads them one
after another at startup
> - Object storage throttles a burst with `SlowDown` (HTTP 503). The
upload body is a stream, so the storage client cannot replay the
request, and the error leaves `startServer`
> - The process exits, the host restarts it, and the next start uploads
the same assets again. A workspace stayed down while four starts in a
row failed this way
> - This pull request retries a throttled asset and lets the server
start when storage stays unavailable
> - The benefit is that a storage slowdown cannot keep a server from
starting

## Linked Issues or Issue Description

Refs #20990, #20952 (other startup failures found on the same hosted
workspace).

**What happened?**

A hosted volume-free workspace received a new release. Each start logged
many `An error was encountered in a non-retryable streaming request.`
lines and then `Paperclip server failed to start` with `SlowDown: Please
reduce your request rate.` for a key under `ui-assets/v1/`. The four
starts failed after 44, 42, 79 and 90 seconds, each on a later asset
name.

**Expected behavior**

The server waits and repeats a throttled upload. If shared storage stays
unavailable, the server starts and serves its build from local files.

**Steps to reproduce**

1. Run a server with `PAPERCLIP_SHARED_UI_ASSETS=true` and S3-compatible
storage.
2. Make the storage answer `SlowDown` (HTTP 503) to one `PutObject`
during startup.
3. The server exits with `Paperclip server failed to start`.

**Paperclip version or commit**

`52cdf76bf3e0cdf29037b63bf0f5e99aa464eab7`

## What Changed

- `retainedUiAssets.publish`: each asset is published in a retry loop. A
throttled or server-side storage failure waits (0.5 s to 8 s, seven
waits at most) and repeats the asset with a fresh stream.
- `isTransientStorageError`: recognizes `SlowDown`, throttling, request
timeout and HTTP 429/500/502/503/504 answers.
- `app.ts`: when publication still fails with a transient storage error,
the server logs an error and continues. Other failures, such as a name
collision with different bytes, are still fatal.
- Tests for the retry, for the give-up path, and for the error
classification.

## Verification

- `npx vitest run src/__tests__/retained-ui-assets.test.ts` in
`server/`: 4 tests passed. The two new tests fail on `master` because
`publish` rejects on the first `SlowDown` and takes no `sleep` option.
- Not run locally: the full suite. CI supplies that coverage.
- Not verified: a live start against throttling storage.

## Risks

- A start can take up to about 31 seconds longer for each throttled
asset before it gives up.
- If storage stays unavailable, the server starts without its assets in
shared storage. An older browser tab can then fail to load a chunk after
the old container is gone. The server logs this at error level.
- Collision detection is unchanged.

## Model Used

Claude Fable 5.2 (`claude-fable-5-2`), through Claude Code with tool use
and code execution.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] 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 Fable 5.2 <noreply@anthropic.com>
2026-11-11 21:21:27 -06:00
DottaandClaude Opus 5.5 a9a08d8169 fix(plugins): retry a plugin activation whose worker initialize timed out (#20982)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Plugins, sandbox providers such as Daytona among them, run as worker
processes that the server activates at boot. Activation sends the worker
an `initialize` RPC with a 15-second timeout.
> - When `initialize` timed out, the loader marked the plugin `error`
for the life of the server process. Only the next boot or an operator
retried it.
> - For a sandbox provider, that means every Daytona run on that server
fails. New runs fail with `lease_acquire_failed … plugin is currently
error`. Reattachments fail with "The exact sandbox lease cannot be
resumed by its verified provider". Retry hits the same dead plugin.
> - A worker that is slow or stalled for a moment at boot is transient,
not a broken plugin.
> - This pull request retries an activation whose `initialize` timed
out, with a bounded backoff. Meanwhile the server keeps serving, and
runs that need the plugin see a restarting worker, which setup retries.
Deterministic failures stay a permanent error.
> - The benefit is that a short plugin stall at boot no longer disables
a provider until the next deploy.

## Linked Issues or Issue Description

Refs #20966, #20857

**What happened?**
Chaos wave `w-20261111T232850-s9521` (cell CR4, first version) stopped
the replacement server's Daytona plugin worker while the server was
activating it. The worker's `initialize` RPC timed out after 15 s
(`INITIALIZE_TIMEOUT_MS`). `plugin-loader.ts` then called
`lifecycleManager.markError`, and the plugin stayed `error`. Task t03's
reattachment failed in `resumeRunLease`, and its Retry did not start a
new run. Task t04 failed 18 s after launch with `lease_acquire_failed`
("…plugin is currently error"), before its own fault.

**Expected behavior**
An activation timeout is retried, and the plugin ends `ready` when the
worker answers. Runs that need the plugin meanwhile are retried within
their bounded setup budget. A bad manifest, a worker that exits during
`initialize`, or a version mismatch is still a permanent error.

**Steps to reproduce**
1. Start the server.
2. Stop (SIGSTOP) a plugin worker for more than 15 s while the server
activates it.
3. Resume the worker. The plugin stays `error` until the next boot.

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

## What Changed

- `plugin-worker-manager.ts`: the "Worker initialize failed" error keeps
its cause, the `JsonRpcCallError` with `PLUGIN_RPC_ERROR_CODES.TIMEOUT`.
- `plugin-loader.ts`, `activatePlugin`:
- An activation whose error chain has the TIMEOUT code is retried after
5, 15, 45 and 120 s (`activationTimeoutRetryDelaysMs`).
- Before each retry, the loader tears down the partial runtime and keeps
the plugin `ready`. It sets `lastError` to "Activation timed out;
retrying in Ns (retry n of 4): …" so operators can see the state.
- A retry stops when the plugin is no longer `ready` or its worker
already runs. Shutdown clears pending retries.
- After the last retry, the loader calls `markError` with "Activation
timed out after 4 retries: …". Any other failure calls `markError` with
"Activation failed: …" at once, as before.
- `pluginLoader.requestActivationTimeoutRecovery(plugin)`: re-activates
a plugin in `error` whose `lastError` starts with "Activation timed
out", at most once per minute.
- `app.ts` exposes it through
`workerManager.requestActivationTimeoutRecovery`.
- `environment-runtime.ts`:
- Lease acquisition, for both pinned and key-resolved providers, calls
`requestActivationTimeoutRecovery` for such a plugin and reports
`worker_unavailable` (`PluginWorkerUnavailableError`, which setup
retries) instead of the terminal "plugin is currently error".
- `resumeRunLease` reports a stopped worker as
`PluginWorkerUnavailableError` when the plugin is `ready`, or is
re-activating from a timeout, instead of the generic "cannot be resumed
by its verified provider".
- This covers every plugin kind: activation retry is in the shared
loader. The on-demand path is in the sandbox-provider resolution, which
is the path a run waits on.
- `doc/plugins/PLUGIN_SPEC.md` §12 describes the rule.

## Verification

- Red on master:
`server/src/__tests__/plugin-loader-activation-timeout.test.ts` uses a
real PostgreSQL plugin registry and a bundled image fixture, and all 3
of its tests fail on master. On master, "initialize times out once, then
succeeds" stays `error` and the worker starts only once.
- Green, all 3 pass:
- A timeout followed by success ends `ready`, with a running worker and
no `markError`.
- Timeouts past the bound end `error` with the "Activation timed out
after 2 retries" message, and an on-demand request re-activates the
plugin to `ready`.
- A worker crash during `initialize` is a permanent error, with no retry
and no on-demand re-activation.
- `environment-runtime.test.ts` has new tests:
- A sandbox plugin errored by activation timeouts gives
`PluginWorkerUnavailableError` and requests re-activation. When no
re-activation is in flight, the error stays terminal.
- `resumeRunLease` on a stopped worker of a `ready` or re-activating
plugin gives `PluginWorkerUnavailableError` and sends no provider call.
- These files pass: `environment-runtime`,
`environment-run-orchestrator`, `plugin-loader-error-retry`,
`plugin-environment-driver-ready-recovery`,
`bundled-plugin-registration`, `plugin-lifecycle-restart`,
`plugin-lifecycle-delivery` and `plugin-dev-watcher`.
- `tsc --noEmit` passes for `server`.
- Chaos: cell CR6 (a plugin stalled during activation at boot) runs
before and after this change. The results are in a comment.

## Risks

- A plugin whose `initialize` keeps timing out now takes about 3 minutes
to reach `error`, instead of 15 s. During that time runs that need it
see a restarting worker and are retried within their setup budget.
- On-demand re-activation is limited to one attempt a minute per plugin,
so a stuck plugin cannot crash-loop.
- Nothing changes for deterministic activation failures.

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

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, through Claude Code
with tool use (shell, file edit, code search). Extended reasoning was
on.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 21:18:47 -06:00
DottaandClaude Opus 5.5 776e900b48 fix(native): retire a finished run's idle Runner that still holds the sandbox Runner port before launching (#20995)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs execute through a Runner (`paperclip-runnerd`) inside a
remote sandbox. Daytona reaches every Runner through one fixed preview
port (43127), so a sandbox serves one listening Runner at a time.
> - A warm sandbox is reused by the next run of the same task. That run
often opens a new session, so it neither adopts the previous run's warm
Runner nor retires it. Retirement of another session's warm owner works
only through the in-memory warm registry of the server that launched it.
> - After a server restart (or on another server), that registry is
empty. The previous run's idle warm Runner keeps the port, and every new
launch fails with `runner_ingress_bind_conflict` until the bounded retry
runs out.
> - This pull request finds such a holder on the sandbox before launch
and proves from durable records whether it may be stopped. A finished
run's idle Runner is stopped through its verified process identity. Any
other holder sends the run to the fresh-sandbox fallback from #20863 and
is never stopped.
> - The benefit is that a follow-up run after a restart starts on its
reused sandbox instead of colliding with its predecessor and needing
Retry.

## Linked Issues or Issue Description

**What happened?**
Restart-chaos wave `w-20261111T230537-s6402` (Claude through ACPX,
Daytona warm sandboxes, `--ephemeral-server`, K1/K2/K15 with
`--k15-fresh-home`, seed 6402, master, no injected fault) needed Retry
on 4 of 6 tasks. Each failing run's first Runner failed with
`runner_ingress_bind_conflict: failed to bind listener: Address already
in use`. The harness's sandbox captures name the process holding port
43127 at the time. In every case it was the previous, already
`succeeded` run of the same task, started under the server generation
before the restart:

| Task | Failing run | Port 43127 held by | Holder run status |
| --- | --- | --- | --- |
| t01 | b4769dfd | 395b202d (Runner started 23:11:30) | succeeded
23:12:19 |
| t05 | b6d466fd | 0969ad46 (23:11:29) | succeeded 23:12:18 |
| t02 | 4097fc1f | 527a1659 (23:11:28) | succeeded 23:12:17 |
| t03 | 60d821af | 1c06bf75 (23:11:28) | succeeded 23:12:25 |

**Root cause**
Warm owners are tracked only in the process-local `warmNativeSessions`
map in `server/src/services/native-runtime/native-session-executor.ts`.
`claimWarmNativeInstructionCopy`,
`reserveWarmNativeInstructionDirectory` and the same-session admission
path retire a predecessor only when it is in that map. A restarted
server has an empty map. The follow-up run uses a new session, so
`inspectRemoteWarmSessionRecovery` (same session only) does not adopt it
either. The launch then meets the old Runner on the fixed port.
`stopStaleRemoteRunnerBeforeBootstrap` stops only the run's own Runner,
so the same-run retries collide again.

**Expected behavior**
A finished predecessor's idle warm Runner is retired before the next
Runner launches on the same sandbox, wherever it was launched. A holder
that cannot be proven safe to stop is never stopped, and the run moves
to a fresh sandbox.

## What Changed

- New
`server/src/services/native-runtime/sandbox-runner-port-holders.ts`:
`retireSandboxRunnerPortHolders` lists every other session's Runner on
the sandbox (session roots with a `runner-process.identity`; this run's
own root and moved-aside roots are skipped). It checks each one with the
existing verified process check (`REMOTE_RECOVERY_PROCESS_CHECK_SCRIPT`:
pid, runner id in the command line and, when recorded, the boot
generation). Then, for a live holder:
- Registered in this server's warm registry: it is retired through
`retireWarmPredecessor`.
- Its company-scoped run row is `succeeded` or `cancelled`, for the same
agent, and its Runner state is idle (empty outbox, no pending terminal
delivery, and both saved fences `pendingProviderCleanup` and
`warmTransition` clear): it is stopped through the verified identity
(`TERM`, then `KILL`, each with a bounded wait). An activity log entry
`native.runner_port_holder_retired` is written.
- When the inventory command itself cannot run, the holder state is
unproven, so the run takes the fresh-sandbox fallback.
- Anything else (a live run, a failed or interrupted run that may still
be resumed, an unknown owner, work in the outbox, an unverifiable or
changed identity marker, or a stop that is not confirmed):
`NativePredecessorRetirementUnprovenError`. Before any process start,
the existing #20863 path marks the lease replace-before-reuse and
re-queues the run in the uncharged lane on a fresh sandbox.
- `executePaperclipNativeSessionAttempt` runs this check before
`native.process_start_requested`, and only for a resumed (reused)
sandbox launched through a shell runner without a warm session to reuse
and without a reattach recovery. A created or replacement sandbox cannot
hold another session's Runner, so it adds no command there.

## Verification

- `pnpm --filter @paperclipai/server exec vitest run
src/services/native-runtime/sandbox-runner-port-holders.integration.test.ts`
(embedded Postgres, real processes): two Runners contend for one real
port. The second bind first fails with `EADDRINUSE`, the same failure as
staging. After `retireSandboxRunnerPortHolders` the finished run's
holder has exited and the bind succeeds, with the activity entry
recorded. The commands run through a confined runner that refuses any
path outside the sandbox workspace base. A holder owned by a live run,
by a finished run with work in its outbox, by a cancelled run with
pending provider cleanup, or by no recorded run is never stopped: the
call raises `NativePredecessorRetirementUnprovenError`
(`runner_port_holder_owner_not_settled` /
`runner_port_holder_unknown_owner`) and the port stays held. This run's
own session, moved-aside roots and exited holders are ignored.
- `native-warm-checkpoint-ownership.integration.test.ts`: its fixture
sandbox runner now answers the inventory command with an empty list
(that sandbox has no other Runner); all 9 cells pass.
- `native-session-executor.test.ts`: 1406 of 1406 pass. Server `tsc
--noEmit` passes.
- Chaos results are in a comment below.

## Risks

- A finished run's idle warm Runner is stopped earlier than its idle
timeout. It held no unsaved work (its run is `succeeded` or `cancelled`
and its outbox is empty), and SIGTERM is the same stop as its idle
close. Only that warm reuse is lost.
- Failed and interrupted holders are not stopped, because their run may
still reattach. Such a run goes to a fresh sandbox instead.
- One extra sandbox command before launch on a reused sandbox for a cold
launch, no wait and no file changes. A warm reuse and a reattach skip
it.
- Not changed: a bind conflict from a holder that is not a Runner (no
identity file) still uses the existing same-run bootstrap budget.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, through Claude Code
with tool use (shell, file edit, test runs). Default context window,
extended reasoning on.

## Checklist

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

Refs #20863, #20953, #20972, #20992.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 21:06:12 -06:00
DottaandClaude Opus 5.5 7d9973405c fix(chat): ask for a missing name only when the request and saved brief give none (#21007)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - People talk to their agents in chat, and the chat guidance tells the
agent how Paperclip works
> - #20888 added a rule: ask for a missing name before building
something that must carry it
> - In a chat eval, "Start a Launch project and have Morgan build its
landing page" made the agent create the project and then ask for the
brand name. It did not create Morgan's task
> - The person had named the project, so a question was not necessary
> - This pull request narrows the rule: ask only when the request, the
conversation and the work's saved brief give no name
> - The benefit is one necessary question when a name is truly missing,
and no extra question when the person already named the work

## Linked Issues or Issue Description

Refs #20888

**What happened**

For "Start a Launch project and have Morgan build its landing page", the
agent created the Launch project and asked for the brand name. It did
not create the landing-page task for Morgan.

**Expected behavior**

The agent uses the name the person gave (Launch), creates the project
and the task for Morgan, and returns both links. For "Build a landing
page for my new product." with no name anywhere, the agent still asks
one free-text question for the product name.

**Steps to reproduce**

1. Run the chat eval `tasks.website-without-project` on master.
2. Read the reply: the agent asks for a brand name and creates no task.

## What Changed

- Chat guidance (`AGENT_CHAT_DIRECTIVE`): use the name that the request,
the conversation or the work's saved brief gives. A project the person
names in the request counts. Ask only when none of these gives a name.
Do not take the name from unrelated records such as other existing
projects or the company name. The rule against invented or placeholder
names stays.
- Tests: updated and new text checks in `agent-conversations.test.ts`.

## Verification

- `cd server && npx vitest run
src/__tests__/agent-conversations.test.ts`: 64 passed. The new test
fails on master (red) and passes with this change (green).
- `npx vitest run` for `board-access-denial` and `codex-local-execute`:
passed.
- Paid chat evals `tasks.website-without-project` and
`questions-and-approvals.necessary-question` run on a combined throwaway
branch; results are in a PR comment. The necessary-question fixture has
an unrelated existing "Launch" project, so the guidance says not to take
the name from other projects.

## Risks

- Guidance text only. The model can still ask or not ask in edge cases.
The paid evals check both directions.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 21:00:33 -06:00
DottaandClaude Opus 5.5 89ea418e76 fix(native): start the configured review stage when a worker reports finished work as needs_review (#21005)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Tasks can have an execution policy with a review stage that names an
independent reviewer
> - #20882 made a native worker's done report start that configured
stage
> - A worker can also report finished work as needs_review and list the
review itself as the only blocking remaining work
> - That report skipped the configured-stage check. The generic
"needs_review requires a named reviewer" error made the worker file a
separate review card, and the configured stage never started
> - This pull request treats that report as finished work and gives the
configured-stage guidance first
> - The benefit is that the configured reviewer gates Done whichever way
the worker phrases its finished report

## Linked Issues or Issue Description

Refs #20882

**What happened**

A task had a review stage for a human reviewer. The native worker built
and verified the deliverable. It reported `needs_review` with
`objectiveSatisfied: true`, all criteria satisfied, and one blocking
remaining item: "Independent human review of the prepared website.html
deliverable." The completion feedback skipped the configured-stage check
because that item blocked completion. The generic needs_review error
then told the worker to name a reviewer. The worker added a review
attention request, and Paperclip made a separate "Review requested"
card. `executionState` stayed null, and the configured reviewer was not
assigned.

**Expected behavior**

On a task with a configured review stage, a worker that reports
finished, verified work is told to report done. Paperclip then starts
the configured stage with its reviewer.

**Steps to reproduce**

1. Create a task with `executionPolicy.stages = [{ type: "review",
participants: [{ type: "user", userId }] }]` for a native agent.
2. Have the agent report `needs_review` with a satisfied objective and
the review as its only blocking remaining work.
3. Before this fix, the feedback asks for a named reviewer. The task
gets a separate review card and no stage.

## What Changed

- `native-completion-feedback.ts`: on a task whose configured review
stage would start, a `needs_review` report with a satisfied objective
and satisfied criteria counts as finished work. It gets the
configured-stage guidance before the generic named-reviewer error. A
review needed before the work can continue (objective not satisfied)
keeps its own request path. Approval requests keep their own path.
- `native-completion-feedback.ts`: a done report on such a task that
still lists the review as outstanding gets the same guidance, not the
generic "done report includes unfinished work" error. So the worker is
not sent back to needs_review.
- The guidance now says to report done without listing the review as
remaining work or as an attention request.
- Tests: two new cases in `native-execution-policy-review-stage.test.ts`
use the recorded report shape.

## Verification

- `cd server && npx vitest run
src/services/native-runtime/native-execution-policy-review-stage.test.ts`:
15 passed. The new recorded-shape test fails on master (red) and passes
with this change (green).
- `npx vitest run` for `native-completion-feedback`,
`native-current-wake-completion-feedback` and
`native-execution-policy-review-stage-race`: 42 passed.
- Paid chat eval run on a combined throwaway branch: results are in a PR
comment.

## Risks

- On a configured-review task, a worker that reports a satisfied
objective but has real non-review blocking work now gets the
configured-stage guidance. That guidance says to report done only when
the work is complete and verified, so the task does not close early.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 20:55:24 -06:00
DottaandClaude Opus 5.5 601babe322 fix(chat): let agents read saved trace export status and give chat real company page paths (#20998)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - People talk to their agents in chat, and agents answer from the
company's saved settings
> - A board member can save a company trace export (an OTLP collector or
Braintrust) in Company settings
> - The settings route is for board connection managers only, so an
agent cannot read it
> - In a chat eval, the board saved and tested a collector. The agent
checked only its own config and secrets, and then told the person that
nothing was configured
> - Chat guidance also names company pages as `/<prefix>/...`. In
another eval, the agent copied that placeholder into a link, and the
link opened nothing
> - This pull request gives agents a read-only status of the trace
export, and gives chat the real company prefix in page paths
> - The benefit is that agents answer from saved settings, and the links
they write open real pages

## Linked Issues or Issue Description

**What happened?**
1. A board member saved and tested a trace export destination. The chat
agent was asked to acknowledge the saved configuration. The agent said
that no collector endpoint or credentials existed. Agents get 403 from
`GET /api/companies/{companyId}/observability`, and no other route
reports the saved state.
2. An agent reply linked `/<prefix>/company/settings/instance`. The chat
rendered it as `/%3Cprefix%3E/...`, which is not a real page. The
placeholder came from the chat directive, which names pages such as
`/<prefix>/company/settings/instance/profile`.

**Expected behavior**
1. An agent can read whether the company's trace export is configured,
enabled and credentialed, and the result of its latest test. The agent
cannot read the destination or any credential.
2. The chat directive and API search results that an agent reads give
real company paths, such as `/COM/company/settings/instance/profile`.

**Steps to reproduce**
1. As a board member, save and test an agent observability destination
in Company settings.
2. In chat, ask the CEO agent to acknowledge that the test collector
configuration is saved.
3. See that the agent cannot read the saved settings and denies them.

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

## What Changed

- Added `GET /api/companies/{companyId}/observability/status`. Agents of
the company and all members can read it. It returns `configured`,
`enabled`, `provider`, `credentialsConfigured`, `updatedAt` and the
latest test status. It never returns the destination or a credential.
- Added the shared `AgentObservabilityStatus` type and
`agentObservabilityStatus()` mapper, and registered the route in the
OpenAPI catalog so that `search_api` finds it.
- The chat task markdown now fills the conversation's own company prefix
(from its identifier, for example `COM-1` gives `COM`) into the chat
directives in place of `/<prefix>/`.
- `search_api` results now get the same substitution for the caller's
company. The shared catalog is not changed.
- Pinned the changed shared source in
`tests/runner-e2e/source-sdk-public-inputs.json`.

## Verification

- `cd server && npx vitest run src/__tests__/agent-observability.test.ts
src/__tests__/agent-observability-status-guidance.test.ts
src/services/heartbeat/task-markdown.test.ts
src/__tests__/openapi-routes.test.ts
src/services/native-runtime/paperclip-runner-tool-authority.test.ts`
- Red/green: with the product change removed, 10 of the new or updated
tests fail (the agent status read gets 403, search does not find the
route, and the chat markdown still contains `/<prefix>/`). With the
change, all pass.
- `npx tsc -p server/tsconfig.json --noEmit` passes.
- `node tests/runner-e2e/source-sdk-public-inputs.mjs <HEAD> --check`
reports no change.

## Risks

- Low risk. The new route is read-only and returns no destination and no
credential. The existing settings route keeps its board-only rule.
- The chat prompt text changes for chats with an identifier. The text is
constant for each company, so prompt caching is not affected inside a
conversation.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 20:55:21 -06:00
DottaandClaude Opus 5.5 65ff80a4bb fix(evals): grade chat acknowledgements, saved replies and links on saved state (#21000)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The chat eval catalog runs real chat journeys against real agents
and grades saved product state
> - A review of full cohort 3 found three cases whose oracle passed
wrong behavior, and eight passes whose final evidence saved no rendered
links
> - `companies.agent-observability` passed although the agent denied the
saved collector. `inbox-search.show-my-work` counted links that existed
only in run-stream prose. `instance-operations.restart-service` accepted
a reply whose only link was the `/<prefix>/` placeholder
> - The final evidence read ran on a page that had not rendered the
conversation yet, so it saved no links
> - This pull request grades those cases on saved-state reads, the saved
reply and resolved links, and reads final evidence from the rendered
conversation
> - The benefit is that these cases fail when the product is wrong and
keep their links when it is right

## Linked Issues or Issue Description

Refs #20998
Refs #20999

**What happened?**
- `companies.agent-observability`: the board saved and tested a
collector. Asked to acknowledge it, the agent said there was no endpoint
or credentials. The oracle graded only human-side state, so it passed.
- `inbox-search.show-my-work`: the saved reply said "Answered above"
over nothing. The links the oracle counted came from run-stream prose
that the bubble also shows.
- `instance-operations.restart-service`: the reply's only action link
was `/<prefix>/company/settings/instance`. The oracle did not check
links.
- Eight cohort-3 passes (`access.other-company`, `agents.agent-api-key`,
`assistant-connections.revoke-personal-grant`,
`budgets-costs.resolve-budget`, `recovery.retry-exhausted`,
`steering.stop-versus-pause`, `task-coordination.pause-tree`,
`teams.install-team`) saved a final observation with no links and a
failed grade. The graded reload had the links. The final evidence read
ran after `verifyResult` left the page mid-load, and an expired render
wait from an earlier document made the read instant.

**Expected behavior**
Each oracle fails the observed wrong behavior and passes valid, reworded
behavior. Final evidence holds the links that the conversation renders.

**Steps to reproduce**
Grade the retained cohort-3 observations with the old oracles. They
pass. Grade them with these oracles. The three wrong cases fail.

## What Changed

- `companies.agent-observability` (oracle v4): each acknowledgement turn
must read the saved trace export status successfully. The read is bound
to the company by its redaction-safe `call_api` receipt. A reply must
also render a link to `/<prefix>/company/settings` for the fixture
company. The reply's wording is not matched.
- `inbox-search.show-my-work` (oracle v5): actionable links must be
carried by the saved reply: a Markdown link target in the saved body, or
a task identifier written in it, which the conversation links. New
shared helper `savedReplyLinks`.
- `instance-operations.restart-service` (oracle v3): new check
`reply-links-resolve` rejects any rendered or saved link target that
keeps a template placeholder (`<...>` or `{...}`). New helpers
`markdownLinkTargets` and `unresolvedLinkTarget` in
`chat-rendered-links.ts`.
- Link capture: the final success-path evidence read reopens the
rendered conversation first. An expired render wait now belongs to its
loaded document, not to the Playwright page.
- Updated `chat-catalog-descriptors.json` for the three oracle versions
and source files. No scenario yaml changed, so the TestOps manifest does
not change.

## Verification

- `npx vitest run --config tests/runner-e2e/vitest.config.ts
tests/runner-e2e/chat-catalog-domains/identity-observability.test.ts
tests/runner-e2e/chat-catalog-domains/internal.test.ts
tests/runner-e2e/chat-catalog-domains/privileged-oracles.test.ts
tests/runner-e2e/chat-catalog-domains/privileged-restart-history.test.ts
tests/runner-e2e/chat-rendered-links.test.ts
tests/runner-e2e/chat-catalog-consistency.test.ts
tests/runner-e2e/chat-catalog-fingerprint.test.ts`
- Calibration tests for each case: a valid result, a reworded valid
result, and the broken cohort-3 behavior.
- Red/green: the new render-wait test fails on the old wait logic and
passes with this change.
- `npx tsc -p tests/runner-e2e/tsconfig.json --noEmit` passes. `node
tests/runner-e2e/source-sdk-public-inputs.mjs <HEAD> --check` reports no
change.
- A paid run on a combined branch with the two product fixes is recorded
in a comment on this pull request.

## Risks

- `companies.agent-observability` needs the agent-readable status route
from #20998. Without it, the case fails, which is correct.
- The show-my-work check needs links in the saved reply. Before #20999,
an OpenCode reply that left its answer in the run stream fails this
case.
- The final evidence read can wait up to 30 seconds to reopen the
conversation. The read is still bounded.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 20:49:38 -06:00
DottaandClaude Opus 5.5 50b79d711a fix(runner): keep call_api writes carrying their body (#21002)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native agents use the `call_api` tool when no dedicated tool covers
an API operation
> - #20606 added an optional `saveResponse` flag to the shared
`call_api` schema for GET reads
> - After that change, OpenCode agents with DeepSeek v4.1 flash often
sent write calls without `body`
> - Body-less POST calls reached the routes and failed with 400; agents
then repeated the same call many times
> - This pull request moves the save option to its own read-only tool
and refuses body-less writes before they are sent
> - The benefit is that the write schema returns to its earlier shape,
and a missed `body` gets one clear, actionable error

## Linked Issues or Issue Description

Refs #20606

**What happened?**

In a full chat-eval cohort on master (OpenCode, DeepSeek v4.1 flash),
write calls through `call_api` arrived without `body`. The model's own
reasoning planned a body, but the emitted calls had only `operationId`,
`pathParams`, `query` and `saveResponse`. About 100 refused PATCH calls
listed exactly those arguments (the cohort before #20606 had none with
`saveResponse`). 72 POST calls reached the server with no body and got
400 (0 in the earlier cohort). One case sent the same body-less resolve
call 21 times.

**Expected behavior**

Write calls carry their request fields in `body`. When a body is
missing, the agent gets one clear error before anything is sent, and it
can correct the call.

**Steps to reproduce**

Run the OpenCode chat cases that resolve approvals, edit queued
messages, delegate with context, edit task properties or start preview
services on master after #20606. Count `call_api` writes without `body`
and POST 400 responses.

**Additional context**

The tool schema that OpenCode sends to the model is intact: `body` is
present. A short-context replay with the same schema did not reproduce
the omission (direct provider calls and local OpenCode runs: every write
carried `body`, with or without `saveResponse`). The omission appears in
long sessions, and it correlates with the added read-only flag. This
change removes that flag from the write schema and adds a safety net for
the remaining cases.

## What Changed

- Remove `saveResponse` from the advertised `call_api` schema and
description. The description now says that request fields go only in
`body`, and the `body` description says when it is required.
- Add a separate read-only `save_api_response` tool (`operationId`,
`pathParams`, `query`). It accepts only GET operation IDs, has no body
field, and uses the existing `saveResponse` export path with the same
access, sensitivity, budget and integrity rules.
- Refuse a write before dispatch when its body is missing: PATCH with a
required body (as before), and POST or PUT whose JSON request schema
names required fields. The error names the operation, the allowed
fields, the received arguments, the shape to resend, and that `query`,
`responseText` and `saveResponse` never carry request fields.
- Keep server-side acceptance of `saveResponse` on `call_api`, so saved
calls and receipts from earlier runs stay valid for recovery.
- Regenerate the runner contract, catalog, seeded fixture and manifest.
Update `doc/runner-api-tools.md`.

## Verification

- Red at master sources: the 3 replayed recorded writes (POST
resolve-from-comment, PATCH issue, POST comment, each with `query: {}`
and `saveResponse: false` and no body) were sent to the route instead of
being refused, and the schema test found no `save_api_response` tool. 4
tests failed.
- Green with this change:
`server/src/services/native-runtime/runner-api.test.ts` 2105/2105
passed; `runner-api.integration.test.ts` 61/61 passed;
`runner-bridge-tools.test.ts`, `task-document-key-contract.test.ts` and
`runner-api-artifacts.test.ts` passed; runner
`protocol-action-contracts.test.ts` 70/70 passed; server `tsc --noEmit`
passed.
- `runner-api-artifacts.integration.test.ts` now saves through
`save_api_response` and checks that a write operation ID or a body is
refused. Its download step needs Linux; it fails on macOS at master too,
so CI covers it.
- Paid E2E, OpenCode + DeepSeek v4.1 flash, [run
43138806990](https://github.com/paperclipai/paperclip/actions/runs/43138806990)
on this fix: 5/5 affected cases passed
(questions-and-approvals.reject-and-revise,
steering.edit-queued-message, task-coordination.delegate-with-context,
tasks.edit-properties, workspaces.preview-service). Body-less write
refusals went from 30 to 0, and body-less POST 400s went from 48 to 0,
compared with the earlier cohort on the same cases. The approval resolve
call was sent once (it was sent 24 times before). Every logged write
carried a body; the remaining non-2xx writes are the cases' intended
stale-edit and failing-fixture responses.
- Run-history redaction also covers `save_api_response` arguments; a
regression test fails on the earlier source and passes now.

## Risks

- Agents that learned `call_api` + `saveResponse: true` in a running
session will see a schema refusal and must use `save_api_response`.
Saved calls keep working for recovery.
- The new POST/PUT refusal applies only when the JSON request schema
names required fields, so writes whose body is optional are still sent.
- One more small read tool is advertised when API tools are enabled.

## Model Used

Anthropic Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with tool
use, shell execution and code 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-11 20:46:14 -06:00
DottaandClaude Opus 5.5 2a1620f143 fix(heartbeat): deliver a person's answer that waited behind a turn that closed the task (#20996)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - An agent can ask the person a question on its task, and the person
answers it in the task chat
> - The person can answer while the agent's turn is still running. The
answer then waits for that turn to finish
> - If that turn completes the task, the release step cancels every
waiting wake for the assignee on the done task. The person's own queued
messages reopen the task, but the answer was dropped and no turn
received it
> - A person who answers an agent's question expects the agent to get
the answer. Losing it without a trace is a silent failure
> - This pull request treats an answer saved before the task was
completed as owed input. It reopens the task and delivers the answer
once
> - The benefit is that a person's answer is never dropped because the
agent finished its turn first

## Linked Issues or Issue Description

**What happened?**
The paid chat eval `steering.approval-delivery` showed it. The person
answered the agent's naming question while the agent's turn was running.
The turn completed the task. The answer's own wake was then cancelled
with "Deferred execution wake no longer applies to a terminal task". No
turn received the answer.

**Steps to reproduce**
1. Start a turn for the assignee on a task.
2. While the turn runs, answer the agent's question on that task.
3. Let the turn mark the task done.
4. The answer's wake is cancelled, and no run receives it.

**Expected behavior**
The answer reaches the assignee exactly once. An answer saved after the
task was completed is still dropped, as before.

Refs #20981, #19331

## What Changed

- `wake-queue/application/use-cases.ts`: before cancelling a deferred
assignee wake on a done task, check whether it carries a person's owed
answer. If it does, reopen the task to `in_progress` and promote the
wake. An answer continues the assignee's own execution, so the existing
continuation status gate applies unchanged.
- `wake-queue/adapters/postgres.ts`: add `owedInteractionAnswer`. It
applies only to an undelivered response that a person resolved, that is
not a tool action, and that was saved at or before the task's
`completedAt`. It returns `blocked` when unresolved blockers forbid
`in_progress`. `reopenIssue` takes an optional target status.
- The blocked case cancels the answer wake with an explicit reason and
does not reopen the task, so the release step does not fail. A
regression test reproduces the earlier throw (fix for the Greptile P1).
- `heartbeat-answer-behind-settling-run.test.ts`: an end-to-end
heartbeat test. It is red on master (the answer wake is cancelled and no
run is bound to it) and green here (one run is bound and succeeds). A
control test shows that an answer saved after completion is still
dropped.
- `use-cases.test.ts`: unit cases for an owed answer, a not-owed answer,
a cancelled task, and an agent-requested wake.
- Eval `steering.approval-delivery` oracle v7: also accept the Acorn
document when the worker's settling turn saved it after the answer was
saved. The eval still requires exactly one later turn bound to the
answer. Calibration tests cover the retained shape, reworded text, a
pre-answer save, the wrong name, another agent, a stale revision, and a
missing bound turn.

## Verification

- `cd server && npx vitest run
src/__tests__/heartbeat-answer-behind-settling-run.test.ts
src/modules/wake-queue
src/__tests__/heartbeat-blocked-question-answer.test.ts` (351 passed)
- Red check: with the `server/src/modules` changes reverted, the new
delivery test fails.
- `cd server && npx vitest run
src/__tests__/heartbeat-comment-wake-batching.test.ts
src/__tests__/question-response-delivery.test.ts` passed. In
`issue-queued-comments-routes.test.ts` all 138 tests passed, but a
cleanup hook timed out locally. CI covers this file.
- `npx vitest run --config tests/runner-e2e/vitest.config.ts
tests/runner-e2e/chat-catalog-domains/runtime-approval-queue.test.ts
tests/runner-e2e/chat-catalog-consistency.test.ts` (58 passed)
- `tsc --noEmit` passes for `server` and `tests/runner-e2e`.
- Paid single-case run 43136466619 on 8a4eaab2c0 passed: one turn was
bound to the answer, there were 5 runs within the bound, and the task
was done. The later blocker commit adds only a blocker read on that
path.

## Risks

- Behavior change: a done task reopens when a person's answer was saved
before completion and no run received it. In that case the agent gets
one more turn. Answers saved after completion, cancelled tasks,
tool-action outcomes, and agent-requested wakes do not change.
- If the agent already read the answer by itself, the extra turn sees
the finished work and completes again. That is one extra turn, and the
answer is not repeated.
- Known gap: the answer is still dropped when the task has unresolved
blockers. This happens when a blocker reopens during the turn. Such a
task cannot move to `in_progress`, and an answer turn cannot run on any
other status. In this case the release step cancels the answer wake with
the reason "Queued answer cannot continue a task with unresolved
blockers", keeps the task done, and continues to the next wake. This is
not a regression, because master dropped the answer in every case. But
it does not meet the goal that a person's answer is never lost. It is a
separate follow-up.

## 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>
canary/v2026.1112.0-canary.2
2026-11-11 20:33:48 -06:00
DottaandClaude Opus 5.5 5626a0299c fix(opencode): keep the answer written before the completion call in the saved reply (#20999)
## Thinking Path

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

## Linked Issues or Issue Description

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

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

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

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 20:19:38 -06:00
DottaandClaude Opus 5.5 85bb942aad fix(native): prove an unconnected bootstrap from the sandbox's own Runner state on a volume-free server (#20992)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs execute through a Runner (`paperclip-runnerd`) in a
remote sandbox. When a Runner exits before `session.open`, the run
restarts its bootstrap on the same run.
> - #20972 lets host preparation restart that bootstrap when the Runner
wrote `runner-state.json` but no provider state. It needs a proof that
this run's own interrupted Runner is stopped or gone.
> - That proof is recorded only when the server still has the run's
local Runner controller root. A volume-free server, or a root that held
no identity, skips it. The live chaos wave after #20972 still failed
with `runner_harness_state_mismatch: resumed_unavailable`.
> - This pull request lets host preparation establish the same proof
from durable sandbox evidence when no local proof exists.
> - The benefit is that the restart also works on volume-free servers,
which is how Cloud runs.

## Linked Issues or Issue Description

**What happened?**
Restart-chaos wave `w-20261112T002811-s6402` ran #20972 (Claude through
ACPX, Daytona warm, `--ephemeral-server`, K1/K2/K15 with
`--k15-fresh-home`, seed 6402, `--runner-bind-fault`). 5 of 6 tasks
needed Retry, with 6 `resumed_unavailable` failures. Run `fbb1666d` in
t01: the sandbox held `runner-state.json` for this run with `lifecycle:
connecting`, the same Runner instance and session,
`lastConnectionProtocolVersion: null`, and no provider state. That is
exactly the state #20972 restarts, but its proof was absent.

**Root cause**
`recordRemoteBootstrapRestartProof` is called only inside the
`existsSync(scoped)` block of the restart path in
`server/src/services/native-runtime/native-session-executor.ts`, after
the local controller identity is read. With no local root (volume-free,
a fresh home, or an emptied root), `provenOwnBootstrapRestart` stays
false, so `inspectOwnUnpreparedBootstrap` from #20972 returns nothing
and the resumed branch throws `resumed_unavailable`.

**Expected behavior**
A run with a `bootstrap_incomplete` recovery claim whose sandbox Runner
state proves it never connected restarts its bootstrap without Retry.

## What Changed

- `inspectOwnUnpreparedBootstrap` now runs for any
`bootstrap_incomplete` claim of this run. When no proof was recorded, it
records one only if the sandbox Runner state proves it never connected
(`remoteRunnerStateNeverConnected`: `lastConnectionProtocolVersion:
null`, `lastControllerCommandSeq: 0`, `reconnectCount: 0`, empty outbox
and processed commands, no pending terminal delivery). The exact Runner
process must also be stopped or proven absent by the existing
`stopStaleRemoteRunnerBeforeBootstrap`, which uses the Runner state's
own identity. All earlier conditions from #20972 still apply: same run,
Runner instance and session, a lifecycle other than `suspended`, no
failover backup and no archived rotation snapshot.

## Verification

- Red on master (#20972 merged): `pnpm --filter @paperclipai/server exec
vitest run src/services/native-runtime/native-session-executor.test.ts
-t "bootstraps only an untouched provider session"`. The new
`interrupted_bootstrap_unprepared_unrecorded` cells failed: resumed
lease `Error: runner_harness_state_mismatch: resumed_unavailable`,
created lease no restart.
- Green: those cells pass. `_connected` (a Runner state that connected,
no local proof) and `_unproven` (the stop is not proven) keep the
refusal. The full file passes (1409 of 1409). Server `tsc --noEmit`
passes.
- Chaos, same cell with `--runner-bind-fault`. Wave
`w-20261112T002811-s6402` (#20972 only): 1 of 6 passed, 6
`resumed_unavailable`. Wave `w-20261112T010800-s6402` (this change): 3
of 6 passed, 2 `resumed_unavailable`. In this wave, run `0a080e83`
restarted its bootstrap three times on the same sandbox: three
`.bootstrap-restart-*` roots were moved aside. The baseline wave on
master before #20972 (`w-20261111T230537-s6402`, no injected fault)
passed 2 of 6.

## Risks

- The new path runs only for a `bootstrap_incomplete` claim of this run
when the full harness check fails, so the normal path is unchanged and
adds no wait before `turn.start`.
- A Runner state that ever connected, received a command or holds outbox
data keeps today's refusal. The moved-aside root is kept for inspection.
- Not fixed here, and seen in both waves: every Runner on a sandbox
listens on one fixed port (43127 in all captures). A live Runner of
another run or session on the same sandbox therefore makes each relaunch
fail with `runner_ingress_bind_conflict`, until the bounded retry is
exhausted (`native_session_retry_exhausted`). A later attempt can still
end with `resumed_unavailable` or `resumed_not_suspended`. That needs
its own fix in predecessor retirement or port selection.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, through Claude Code
with tool use (shell, file edit, test runs). Default context window,
extended reasoning on.

## Checklist

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

Refs #20972, #20928, #20953, #20863.

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 20:07:02 -06:00
DottaandClaude Opus 5.5 faa78dd818 fix(agents): keep a managed instructions bundle managed when a restarted volume-free server saves another setting (#20997)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agent settings are saved through `PATCH /api/agents/:id`. On a
volume-free server, a save is refused when the agent's instructions are
external, because files outside the managed root do not survive a
restart
> - A managed bundle saves its instruction paths as absolute paths under
the home of the server that wrote them. A replacement volume-free server
can start with a new home
> - After such a restart, any `adapterConfig` change re-derived the
bundle mode from the saved `instructionsFilePath`. That path is outside
the new managed root, so the bundle was read as `external` and the save
failed with 422 "Volume-free servers require managed agent instructions"
> - This pull request keeps a managed bundle managed when the save keeps
its saved `instructionsFilePath`, and rewrites its paths under the
current managed root
> - The benefit is that a board user can change one setting (for example
`idleTimeoutMs`) after a restart, and a real switch to external
instructions is still refused

## Linked Issues or Issue Description

**What happened?**
In restart-chaos wave `w-20261112T012841-s7000` (volume-free mode,
Daytona, Codex, restarts K1 and K15 with a fresh server home), the board
sent `PATCH /api/agents/:id` with
`{"adapterConfig":{"idleTimeoutMs":301001}}`. All 3 tasks of the W2C
workload got 422 "Volume-free servers require managed agent
instructions".

**Expected behavior**
A partial `adapterConfig` change of an agent with managed instructions
succeeds on a restarted volume-free server.

**Steps to reproduce**
1. Create an agent with a managed instructions bundle on a volume-free
server.
2. Restart the server with a new `PAPERCLIP_HOME`.
3. Send `PATCH /api/agents/:id` with
`{"adapterConfig":{"idleTimeoutMs":301001}}`.
4. The server returns 422.

**Why Codex looked different from Claude**
It is not the adapter. The Claude wave that passed
(`w-20261112T010036-s6801`) had no faults, so its PATCH ran on the
server that wrote the bundle. The Codex wave sent its PATCH after K1 and
K15 restarts with a new home. The new test fails the same way for a
Claude agent after a fresh-home restart, and passes for both adapters on
the original server.

**The PATCH contract**
`adapterConfig` merges into the saved config unless the request sets
`replaceAdapterConfig: true` (validator in
`packages/shared/src/validators/agent.ts`, route in
`server/src/routes/agents.ts`). The UI full-config save sends
`replaceAdapterConfig: true` with the whole config. The merge was
already correct. The fault was in
`syncInstructionsBundleConfigFromFilePath`, which the route applies to
the effective (merged) config: it classified the bundle only from the
absolute file path and the current managed root. The contract does not
change.

## What Changed

- `server/src/services/agent-instructions.ts`
(`syncInstructionsBundleConfigFromFilePath`): when the saved bundle is
managed and the effective config keeps the saved `instructionsFilePath`,
keep it managed with the root that `deriveBundleState` already resolves
(the current server's managed root) and the saved entry file. A
different `instructionsFilePath` is classified as before.
- `server/src/__tests__/agent-volume-free-config-patch.test.ts`: route
tests with embedded Postgres for a Codex agent and a Claude agent: a
one-setting change after a fresh-home restart, the same change on the
original server, and a real switch to an external path, which is still
refused.
- `doc/DEVELOPING.md`: describe how a volume-free server keeps a managed
bundle across a new home.

## Verification

- Red on `origin/master` (`9dfdeaad86`), `cd server && npx vitest run
src/__tests__/agent-volume-free-config-patch.test.ts`:
- `× changes one codex setting after a fresh-home restart and keeps
managed instructions`
- `× changes one claude setting after a fresh-home restart and keeps
managed instructions`
- `AssertionError: {"error":"Volume-free servers require managed agent
instructions"}: expected 422 to be 200`
- The 4 other cases passed (the original server, and the refused
external switch).
- Green with the fix: the same file passes 6/6, together with
`agent-instructions-service`, `agent-instructions-routes`,
`ephemeral-server-contract` and `agent-configuration-save` (65 tests).
- The restart-chaos W2C workload (`tests/restart-chaos/rewake.ts`)
already sends this PATCH after K1/K15 restarts and caught the fault as
`T:config_changed_before_wake`. No new chaos cell is necessary.

## Risks

- Low risk. The change applies only when the saved bundle is managed and
the save keeps the saved `instructionsFilePath`. Then the paths are
rewritten under the current managed root, which is the root that bundle
reads already use.
- The volume-free guard is unchanged. A save that sets an
`instructionsFilePath` outside the managed root is still refused (tested
for both adapters).

## Model Used

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

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 20:06:51 -06:00
DottaandClaude Opus 5.5 1949ac6788 test(chaos): fail the directory-lock crash point with the lock's own busy error (#20988)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness can fail one recovery step on purpose
(`--fail-recovery-step`), through a test-only crash point in the server
> - The `directory-lock` step fails
`agent_directory.operation_lock.before_acquire`, the per-agent Postgres
lock that a volume-free server takes around remote agent-directory work
> - The real lock path throws `AgentDirectoryOperationBusyError` when
another run holds the lock. Preparation turns that error into a deferral
and retries later
> - The crash point threw a plain 409 `conflict()` with the same
message, so the deferral never matched and the injected fault failed the
run. A volume-free wave on 2026-11-12 showed this: the run failed with
`native_continuation_requires_reconciliation` and needed Retry, where
real contention would have been deferred
> - This pull request makes the crash point throw the lock's own error
class
> - The benefit is that the `directory-lock` chaos cell tests the
product's real handling of a busy agent directory

## Linked Issues or Issue Description

**What is the problem?**
The test-only crash point
`agent_directory.operation_lock.before_acquire` throws a different error
class from the lock it stands in for.

**What is the expected behavior?**
A run that meets the injected failure takes the same path as a run that
meets real lock contention.

**Steps to reproduce**
Run a volume-free Daytona wave with `--fail-recovery-step
directory-lock:1`, workload W2 and a K2 restart. The recovering run
fails with `native_continuation_requires_reconciliation`; the stack
shows the crash point inside `prepareCopy`, under
`prepareWithAgentDirectoryDeferral`, which only defers
`AgentDirectoryOperationBusyError`.

## What Changed

- New test
`server/src/__tests__/agent-directory-lock-crash-point.test.ts`.
- `server/src/services/agent-directory-working-copies.ts`: the crash
point's `fail` returns `new AgentDirectoryOperationBusyError()` (same
409 status and message) instead of a plain `conflict()`.

## Verification

- `server/src/__tests__/agent-directory-lock-crash-point.test.ts` arms
the point in fail mode through a control file (as restart chaos does)
and checks that the injected failure is an
`AgentDirectoryOperationBusyError` that
`prepareWithAgentDirectoryDeferral` turns into a deferral. It fails on
master (the injected error is a plain `HttpError`) and passes with this
change.
- Server typecheck passes.
- The crash point is disarmed unless a chaos wave arms it, so production
behavior does not change.
- Live check: the next volume-free `directory-lock:1` chaos wave (W2,
K2, Codex) shows whether the recovering run is deferred instead of
failing. I will add the result to this PR.

## Risks

- Low risk. The change is in a test-only crash point that is disarmed in
production. The README step table already documents the error as the 409
"Agent directory operation is already active".

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 20:06:36 -06:00
DottaandClaude Fable 5.2 52cdf76bf3 fix(ephemeral): start a volume-free server over harness backups a volume-free server left unsaved (#20990)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A hosted Paperclip server can run volume-free: its durable state is
in the database and object storage, and its local disk is replaced with
every deployment
> - At startup, a volume-free server runs a readiness gate that refuses
to start when it finds data that still needs migration off a volume
> - One of those findings is a harness backup that never reached durable
storage. A volume-free server that stops in the middle of a run always
leaves some
> - The gate then refuses every later start, the process exits, and the
host restarts it into the same refusal. The workspace stays down until a
person repairs it
> - This pull request lets a server start over such backups when its
filesystem carries the volume-free owner marker
> - The benefit is that a volume-free server recovers from an
interrupted run by itself

## Linked Issues or Issue Description

Refs #20952 (the same crash loop for Runner controller files; this is
the next check in the same gate).

**What happened?**

A hosted volume-free workspace stopped while runs were in progress.
Every later start logged `Paperclip server failed to start` with
`Volume-free server data is not eligible:
native_harness_backups_require_migration`, and exited. The host
restarted the container and the server exited again. The workspace did
not serve any request for more than two hours.

**Expected behavior**

A volume-free server starts after an interrupted run. Restart recovery
handles the runs whose local harness backup was lost with the old
container.

**Steps to reproduce**

1. Run a volume-free server (`PAPERCLIP_EPHEMERAL_SERVER=true`) with
native Runner tasks in progress.
2. Stop the server process before the harness backup of an active
session reaches durable storage.
3. Start the server again. It refuses to start with
`native_harness_backups_require_migration`.

**Paperclip version or commit**

`87fe4d979355232570428deba480b47e1812a26c` (contains #20952).

## What Changed

- `assertEphemeralServerData`: when the only reason is
`native_harness_backups_require_migration` and the filesystem carries
the volume-free owner marker (`volumeFreeControllerStateOwned`), the
gate removes that blocker, logs a warning, and lets the server start.
- A filesystem without the marker is refused as before. All other
blockers are unchanged.
- `readEphemeralServerDataEligibility` is unchanged, so the migration
and enrollment callers still see the blocker.
- Added `ephemeral-server-startup-run-state.test.ts`.

## Verification

- Red: with the change to `ephemeral-server-contract.ts` reverted,
`starts a volume-free server over harness backups a volume-free server
left unsaved` fails (1 failed, 2 passed).
- Green: `npx vitest run
src/__tests__/ephemeral-server-startup-run-state.test.ts
src/__tests__/ephemeral-server-data.test.ts` in `server/`: 2 files, 8
tests passed.
- Not run locally: the full suite. CI supplies that coverage.
- Not verified: a live volume-free start. The hosted workspace that
showed the failure will receive this change through the normal release.

## Risks

- A server on a marked volume-free filesystem now starts with unsaved
harness backups. Those backups were on a disk that no longer exists, so
the refusal protected nothing. The affected runs depend on restart
recovery.
- The marker is the only evidence that there is no volume. A filesystem
that still is a volume and does not carry the marker keeps the refusal.
- The gate does not repair or delete the stale lease stamps. The warning
repeats at each start until restart recovery settles those runs.

## Model Used

Claude Fable 5.2 (`claude-fable-5-2`), through Claude Code with tool use
and code execution.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] 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 Fable 5.2 <noreply@anthropic.com>
2026-11-11 20:02:27 -06:00
DottaandClaude Opus 5.5 5d1bb5192f fix(retry): carry the original wake to every re-queued run (#20989)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A run serves one wake: a comment, an answer, an assignment, or a
one-shot task monitor check (`issue_monitor_due`).
> - When a run fails, Paperclip often starts a successor run by itself:
the bounded transient retry, the uncharged `interrupted_preparation`
re-queue, workspace or connection waits, missing-comment repair,
capacity retries and recovery successors. Each successor links to the
failed run through `retry_of_run_id`.
> - Most of these re-queues replace the wake reason with their own (for
example `transient_failure_retry`). The agent then does not know which
wake it must handle.
> - #20970 fixed this for the board Retry only. A no-fault chaos wave
showed the same loss on an automatic retry: the monitor run failed
before provider work, the automatic retry succeeded, and the agent did
not treat it as the monitor check. The monitor was already consumed, so
the task rested `in_progress` with nothing pending.
> - This pull request covers all re-queue paths at one shared point: the
continuation builder that every issue dispatch uses
(`buildExecutionContinuation`, called from `heartbeat/task-context.ts`).
It walks the re-queue chain to the first run that was not a re-queue and
records that run's wake reason as `trigger.retriedReason`. The wake
prompt already shows that field (#20970).
> - The benefit is that every successor of a failed run handles the same
wake as the run it replaces.

## Linked Issues or Issue Description

**What happened?**
No-fault restart-chaos wave on master `87fe4d9793`, volume-free mode
(`w-20261112T010036-s6801`, Claude, task t01, workload W2). The monitor
wake started run `78e27ef9`. It failed before provider work
(`native_predecessor_retirement_unproven`). The
`interrupted_preparation` re-queue (attempt 1/12) started run `0cd387a0`
on a fresh sandbox, and that run succeeded. The agent reported that no
board comment arrived and that the monitor wake was not visible. It
yielded `response_wake` again. The monitor was `triggered` with no
`nextCheckAt`. The issue rested `in_progress` with `canRetry=false`, no
live run, no wake and no monitor. The harness graded `R1
rested_not_done` and `R3 missing_retry`.

**Expected behavior**
A successor of a failed run tells the agent which wake it re-delivers. A
re-queued monitor check is handled as the monitor check.

**Steps to reproduce**
1. Wake a native agent with `issue_monitor_due` on a task.
2. Make the run fail before provider work, for example when the previous
run's sandbox session cannot be retired.
3. Let the `interrupted_preparation` re-queue run.
4. Before this change, the successor's continuation trigger has the
re-queue reason only, and its prompt does not name the monitor check.

**Additional context**
Follow-up to #20970.

Refs #20970
Refs #20935
Refs #20863

## What Changed

- `server/src/services/execution-continuation.ts`: new
`requeuedWakeReason`.
- It starts at the current run's `retry_of_run_id` column. If that is
empty, it uses the context `retryOfRunId`, or the source run of a board
Retry.
- It follows the chain within the same company and issue, at most 32
links, until it finds a run with no re-queue link. It returns that run's
wake reason.
- A `trigger.retriedReason` already saved on a run in the chain is
reused.
- `trigger.retriedReason` is set only when the original reason differs
from the current reason and is not itself a Retry reason. A re-queue
that keeps the original wake reason (`preserveWakeContext`) is
unchanged.
- This one point covers every successor that links with
`retry_of_run_id`: bounded transient retries, `interrupted_preparation`
(#20863), workspace and AI-connection waits, missing-comment repair
(`run-lifecycle.ts`), native capacity retries, safe replacement, and the
board Retry.
- Tests:
- `server/src/__tests__/heartbeat-native-predispatch-retry.test.ts`: the
assembled heartbeat service runs a monitor-due wake, a
predecessor-retirement failure before provider work, and the automatic
`interrupted_preparation` re-queue. It asserts the successor's saved
continuation and wake prompt.
- `server/src/services/execution-continuation.test.ts`: a two-link chain
of automatic re-queues, and a re-queue that keeps the original reason.

## Verification

- Red on master: `cd server && npx vitest run
src/__tests__/heartbeat-native-predispatch-retry.test.ts -t "delivers
the monitor check"`. The successor's trigger had `sourceRunId` = the
failed monitor run, but no `retriedReason`.
- Green: `heartbeat-native-predispatch-retry.test.ts`,
`execution-continuation.test.ts` and `heartbeat/retries.test.ts` pass
(78 tests).
- `npx tsc --noEmit -p server` passes.
- Chaos: the existing no-fault W2 cell in volume-free mode reproduces
this when the monitor run fails before provider work. I did not rerun a
wave.

## Risks

- Low risk. The change adds one optional trigger field to more wakes. It
changes no admission, status or authority path.
- The walk costs at most one indexed primary-key read per chain link,
only for runs that are re-queues.
- The walk follows only same-company, same-issue runs. Old saved runs
work, because they already store `wakeReason` and the `retry_of_run_id`
column.
- The resting state "agent yielded `response_wake` after the monitor was
consumed" is still possible when an agent really waits. #20935 shows it
on the run projection as "Waiting for a reply". The ownership
reconciler's I7 check still counts it as `no_owner_no_wake`. This PR
does not change I7.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code with tool use (shell, file edits, local 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-11 19:52:21 -06:00
8818402e7a feat(inbox): route requests to people and record personal outcomes (#20959)
## Thinking Path

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

## Linked Issues or Issue Description

**Subsystem affected**

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

**Problem or motivation**

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

**Proposed solution**

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

**Alternatives considered**

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

**Roadmap alignment**

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

**Additional context**

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

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

OpenAI GPT-6 through Codex. Used repository tools, code execution, and
browser checks. This session does not expose a more specific runtime
model ID or its context-window size.

## Checklist

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

---------

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

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

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-11-12 01:23:59 +00:00
Nicky Leach 45170b0cac Reuse Runner plugin capability checks (#20987) 2026-11-11 17:21:07 -08:00
DottaandClaude Opus 5.5 42fd790e80 fix(evals): grade approval delivery on the saved answer, not on which queued wake is listed (#20981)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The chat eval catalog runs real chat journeys against real agents
and grades the saved product state
> - The `steering.approval-delivery` case checks that a person's answer
to an agent's question stays separate from editable queued messages and
reaches the agent once
> - The task composer lists one pending wake at a time. It shows
editable ordinary messages before a saved card response, by design since
#19331
> - When the chat lead relays the person's answer as its own task
comment, the composer lists that relay. The saved answer waits on its
own wake and is not listed
> - The oracle required the answer and the relay in one listing. The
product never produces that shape, so the case timed out every time the
lead relayed
> - This pull request grades the case on the saved answer and on its
delivery, and accepts either valid listing
> - The benefit is that the case measures the promise in its expected
outcome and still fails on a lost, replayed, or editable answer

## Linked Issues or Issue Description

**What happened?**
The paid chat eval `steering.approval-delivery` (OpenCode, DeepSeek
flash) did the answer, edit, reorder, and discard steps. It then timed
out while it waited for the answer to show in the queue.

**Steps to reproduce**
Run the case. Let the chat lead relay the answer as its own task
comment. After the person's two messages are discarded, the queue lists
only the relay. The answer is saved as `answered` and keeps its own
pending wake.

**Expected behavior**
The case passes when the answer stays saved and unchanged, cannot be
edited through the queue, and reaches exactly one later agent turn.

Refs #19331, #20899

## What Changed

- `runtime-approval-queue.ts`: add `approvalResponseListing`. After the
discards, accept the answer listed alone and locked, or only the chat
lead's relay listed. Reject any person message or an editable answer.
- Keep the edit attempt through the queue route for both listings.
Require it to be refused and the saved answer to stay unchanged. Check
the locked card in the UI when the answer is listed.
- Add `classifyApprovalSuccessors`. Grade delivery on saved run state:
exactly one later worker turn bound to the answer, with the question ID
and Acorn in its native input. Allow at most one other turn that carries
only the lead's relay. Reject any other turn.
- Accept the Acorn document from the bound turn or the relay turn. Still
require the earlier work to be kept, all runs succeeded, the queue
empty, and the task done.
- Oracle `approval-delivery.v6`. Run bound 5 with a minimum of 4, to
allow the one relay turn.
- Calibration tests: the retained relay shape passes, the original
listed shape passes, and these fail: no bound turn, two bound turns, a
person message left behind the relay, an unexpected worker turn, two
relay turns, a recorded listing that disagrees with the queue, and a
listed answer whose controls were not seen as locked.

No YAML changed. The expected outcome already promises retention and
once-only delivery, not a queue listing.

## Verification

- `npx vitest run --config tests/runner-e2e/vitest.config.ts
tests/runner-e2e/chat-catalog-domains/runtime-approval-queue.test.ts
tests/runner-e2e/chat-catalog-consistency.test.ts` (46 passed)
- `npx tsc -p tests/runner-e2e/tsconfig.json --noEmit` (clean)
- `node tests/runner-e2e/source-sdk-public-inputs.mjs <sha> --check` (no
change)
- One paid single-case run of this branch. The result is added below in
a comment.

## Risks

- Low risk. The change is in eval code only. No product behavior
changes.
- The grader now allows one extra worker turn for the lead's relay. A
turn that carries any other input still fails the case.

## Model Used

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

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 19:13:43 -06:00
DottaandClaude Opus 5.5 4b24352712 test(restart-chaos): stall the plugin during reattachment (CR4) and at launch (CR5) (#20966)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness (`tests/restart-chaos/`) injects faults
into a real Paperclip server and Daytona sandboxes, and grades each
task.
> - Class R cell CR3 stalls the Daytona plugin worker for 45 seconds in
the middle of a tool call. It found the failure that #20940 fixed.
> - #20945 fixed the same stall during reattachment and at launch, but
only unit tests cover those paths.
> - This pull request adds two cells: CR4 stalls the plugin right after
a K1 restart, while the run reattaches, and CR5 stalls it while the run
launches.
> - The benefit is that a wave now exercises the recovery and launch
paths, not only unit tests.

## Linked Issues or Issue Description

Refs #20909, #20940, #20945

**What happened?**
CR3 stalls the plugin only in the middle of a tool call. The
reattachment reads (saved state, identity marker, process check) and the
launch reads (runnerd launch call, identity read) had no chaos coverage
for a plugin stall.

**Expected behavior**
The harness can stall the plugin during reattachment and during launch,
and it grades the task as it grades CR3.

**Steps to reproduce**
1. Run a wave with `--faults CR4,CR5`. Before this change, the harness
rejects these fault kinds.

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

## What Changed

- `CR4`: a K1 restart (SIGKILL, no outage hold). When the replacement is
healthy, the harness finds its Daytona plugin worker (it waits up to 60
seconds for the worker to fork) and stops it for the stall.
- `CR5`: the CR3 stall, fired 3–8 seconds after the task is assigned
(`CR5_FIRE_WINDOW_MS`), so that it covers the run's launch. T1's
`fault_crossed_tool` check does not apply to CR5.
- `--cr3-stall-seconds` sets the stall for CR3, CR4 and CR5.
`class_r_fault_applied` requires that each of their stalls ended.
- `runPluginStall` takes `findWorkerWithinMs` for a replacement server
whose worker has not forked yet.
- The planning, matrix, supervisor signals, README and `class-r.test.ts`
are updated.

## Verification

- `npx vitest run --config tests/restart-chaos/vitest.config.ts` passes
(50 files, 618 tests before the additions). `class-r.test.ts` passes 27
tests, including the new tests for CR4/CR5 planning, the launch fire
window, signals, the worker wait, `class_r_fault_applied` and the CR5 T1
exemption.
- `tsc --noEmit -p tests/restart-chaos/tsconfig.json` passes.
- One wave on master with a matching Daytona image: the results are in a
comment on this PR.

## Risks

- Harness only. There is no product code change.
- CR5's coverage of the launch call depends on timing. The stall starts
3–8 seconds after assignment and lasts at least 41 seconds. That covers
the start of a cold run, but it can also hit plugin calls before the
launch, such as sandbox acquisition. The wave's server log shows which
plugin calls stalled.

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

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, through Claude Code
with tool use (shell, file edit, code search). Extended reasoning was
on.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 18:53:52 -06:00
DottaandClaude Opus 5.5 5cd5adfccc fix(native): link a person-owned agent blocker to the agent's open question whatever owner kind the model reports (#20969)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - #20931 lets a person's answer continue a task that its agent blocked
while it waited for that answer
> - That link is recorded only when the blocker's unblock descriptor is
bound to the board
> - Codex reports such a wait as `paperclip_block` with `owner: {"kind":
"user", "name": "Board"}` and scope `task_wide`. The evidence classifier
treats only `kind: "board"` (not a PRP owner kind) as board-owned, so
the arbiter binds this person-owned blocker to the reporting agent, and
no link is recorded
> - So every Codex W2Q task still loses the answer
(`issue_not_in_progress`) and needs Retry
> - This pull request classifies a `user` owner as a person
(board-bound) and records the link from the reported owner kind, not
from the spelling of the owner
> - The benefit is that the person's answer reaches the agent whatever
owner shape the model uses

## Linked Issues or Issue Description

Refs #20931

**What happened?**
Restart-chaos wave w-20261111T230717-s5201 on master 9c56e546 (Daytona
warm, Codex, no fault): all 3 W2Q tasks (t01, t05, t09) failed. Each
continuation after the answer (runs 8864fa72, 814b2cd0, 757c1c8f) was
cancelled with `issue_not_in_progress (current status: blocked)`. Each
agent blocked with `owner: {"kind": "user", "name": "Board"}`, scope
`task_wide` (reason codes `missing_required_answer`,
`awaiting_codename_answer`). The decision was `task_wide_blocker_bound`
with the unblock owner `{agentId}`, so `questionsAwaitedByAgentBlock`
recorded no `awaitingInteractionIds`.

**What did you expect to happen?**
The answer continues the task, as it does for a board-bound blocker.

**Steps to reproduce**
1. A Codex agent asks a question; the person comments before answering.
2. The agent calls `paperclip_block` with a `user` owner and scope
`task_wide`.
3. The person answers. The continuation is cancelled with
`issue_not_in_progress`.

## What Changed

- `evidence-classifier.ts`: new `nativeBlockerOwnerKind` and an optional
`ownerKind` on the blocker assessment (`agent`, `user`, `system`,
`external`, legacy `board`, or `unspecified`). A `user` owner is now
board-owned, like `board`: a person resolves it, so it is not bound to
the reporting agent.
- `status-arbiter.ts`: both agent-reported blocker decisions
(`task_wide_blocker_bound`, `current_track_blocker_waiting`) carry
`blockerOwnerKind`.
- `blocked-question-answer.ts` and the status committer:
`questionsAwaitedByAgentBlock` records the agent's own pending,
undismissed questions when the reported owner is one a person resolves:
`user`, `external`, legacy `board`, or no recognized kind. `agent` and
`system` owners and dependency blocks
(`durable_dependency_blocker_bound`) stay excluded. A decision without
`blockerOwnerKind` keeps the previous board-bound rule. All other guards
are unchanged: asked by this agent, answered by a person, decision still
current, `statusVersion` match, guarded checkout.
- `doc/execution-semantics.md` describes the rule.

## Verification

- `server/src/__tests__/heartbeat-blocked-question-answer.test.ts` now
builds the block decision with the real `classifyNativeEvidence` and
`arbitrateNativeStatus` from the exact Codex report shape, commits it
with `commitNativeStatusDecision`, and delivers the answer through
`heartbeat.wakeup`.
- Red on master (product hunks removed, test kept): `user` task-wide
fails because the unblock owner is `{ agentId }` instead of `board`;
`external` task-wide fails with `status: cancelled, errorCode:
issue_not_in_progress`.
- Green: 10/10. `user` task-wide, `external` task-wide and `user`
current-track are delivered. `agent` and `system` owners still cancel
with `issue_not_in_progress`, as do the earlier negative cases.
- `cd server && pnpm exec vitest run
src/services/native-runtime/status-arbiter.test.ts
src/services/native-runtime/evidence-classifier.test.ts
src/services/native-runtime/historical-task-questions.test.ts
src/modules/run-dispatch`: 257 passed. `recovery-observability`,
`heartbeat-issue-liveness-escalation`, `native-completion-feedback`,
`paperclip-control-plane-port`: all passed.
- `cd server && pnpm exec tsc --noEmit -p tsconfig.json`: passes.
- Chaos: see the comment below.

## Risks

- A task-wide blocker that names a person (`user`) no longer binds its
unblock descriptor to the reporting agent and no longer wakes that agent
about its own blocker. The board owns it, as for a current-track
blocker.
- An `external` owner can now link to the agent's open question. If that
blocker was about something else, the answer resumes the agent and it
reports the blocker again.
- Upgrade: `ownerKind` and `blockerOwnerKind` are optional. A decision
built by an older release keeps the previous rule. No schema change.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), with tool use and code execution
in an agent harness.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 18:50:00 -06:00
DottaandClaude Opus 5.5 dfd2a6efca fix(native): restart a follow-up the closing server cut before it attached, instead of reattaching to its predecessor's Runner (#20979)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native runs on a remote sandbox keep a warm Runner between turns. A
follow-up run takes over its settled predecessor's provider session and
Runner.
> - When a deploy stops the server, a follow-up that is still preparing
fails with `local_runtime_api_listener_unavailable`. The executor saves
this failure as a same-run bootstrap retry (#20786). The saved failure
proves that the run did no provider work of its own.
> - After the restart, restart recovery saw a checkpoint and a provider
session, and chose `reattach_remote_runner`. But the checkpoint and the
Runner identity belong to the predecessor. The Runner identity check
then failed with `native_remote_recovery_lease_mismatch:
stage=runner_verification field=identity_run_id`. The run ended terminal
and the task needed Retry.
> - This pull request recognizes that durable shape in restart recovery
and restarts the run as a fresh bootstrap on its warm session. It does
not try to reattach to a Runner that belongs to a different run.
> - The pull request also stops the queue from claiming a queued run
after shutdown closed run admission during the queue scan.
> - The benefit is that a deploy during a follow-up's preparation no
longer ends the run terminal with a Retry.

## Linked Issues or Issue Description

Refs #20786, #20928, #20852, #20904

**What happened?**
In three Codex runs across two Daytona restart-chaos waves (warm
sandboxes, CR1 and K15 with overlap), a warm follow-up run
(`automation`, `retryOfRunId` = its predecessor) started 0–2 s before
its server got SIGTERM. Its bootstrap failed with
`local_runtime_api_listener_unavailable` and saved a `bootstrap_retry`
with `providerSessionEstablished: false`, `providerEventsExist: false`.
After the restart, `native.recovery.transition` chose
`reattach_remote_runner`. `verifyRemoteRunnerReattachment` then threw
`identity_run_id`, because the warm Runner's identity still named the
predecessor. The run ended with
`native_continuation_requires_reconciliation`. In every sandbox capture
the Runner was cleanly suspended, with no stderr.

**Expected behavior**
The run did no provider work and never wrote its own Runner identity.
The next server must restart it without a Retry.

**Steps to reproduce**
1. Finish a remote warm native run, so that its provider session and
Runner identity are saved.
2. Start a follow-up run. Admission carries the predecessor's provider
session and Runner process identity into the follow-up.
3. Send SIGTERM while the follow-up prepares, so that it fails with
`local_runtime_api_listener_unavailable`.
4. Restart the server. Restart recovery chooses
`reattach_remote_runner`, and the run fails with `identity_run_id`.

## What Changed

- `native-restart-recovery.ts` (`claimNativeRestartRecoveries`): add the
`remoteFollowUpNeverAttached` proof. It is true only when all of these
are true:
- the run is a failed, settled `bootstrap_retry` whose original failure
is `local_runtime_api_listener_unavailable`, with
`providerSessionEstablished: false`, `providerEventsExist: false`, no
successor and no lease owner;
- the run is on a prepared remote sandbox workspace with the current
preparation fencing version, it is not a plugin task, and it has no
Runner controller checkpoint and no staged attachment of its own;
- its session checkpoint is only the carried admission checkpoint (no
cursor, turn, result, terminal or pending request), and a settled
earlier run of the same session saved that exact provider session;
- its Runner process identity is absent or is exactly the identity of a
settled predecessor;
- it has no provider event, no Runner-originated event, and no
`native.process_identity_recorded` event of its own.
With that proof, the run gets a `bootstrap_incomplete` claim with the
reason `remote_follow_up_never_attached`, and it passes the same
current-admission check as the other settled bootstrap retries.
- `verifyRemoteRunnerReattachment` is unchanged. It still refuses a
Runner that names a different run.
- `heartbeat/queue.ts` (`startNextQueuedRunForAgent`): check run
admission again before each claim. A run that a scan reaches after
shutdown closed admission stays queued for the next server.
- Tests:
- `native-runner-restart-recovery.integration.test.ts`: eight cases for
the staging shape. Three restart (inherited Runner identity; inherited
identity plus a Runner request; a Runner request with no identity). Five
stay held (own recorded Runner identity; a Runner-originated event; a
carried active turn; a provider session that no settled run saved; a
saved failure that established a provider session).
- `queue.test.ts`: a queued run stays queued and is not executed when
admission closes during the scan.

### How this relates to #20852 and #20904

- #20852 hands a preparing run to the next controller and persists
launch intents, so that preparation itself survives a deploy. It does
not change restart recovery classification for a settled
`bootstrap_retry`, and it does not touch the run-admission gate. This PR
covers runs that already failed and saved a `bootstrap_retry` (including
state that older releases saved), so the two are complementary.
- #20904 changes startup readiness for native-controller preservation
obligations. It does not touch either code path in this PR.

### Finding about the "draining server" admission

In both waves the follow-up was claimed before the process observed
shutdown. In CR1 (seed 9511) the run was created at 21:54:04.695Z and
started at 21:54:04.854Z; the server logged "shutdown signal received"
at 21:54:05 (the harness sent SIGTERM at 21:54:03.256Z, under load
average 10). In the K15 overlap (seed 9513) the runs started at
23:31:10.9Z and 23:31:11.5Z, and the old server got SIGTERM at
23:31:13Z. During the overlap the old server is fully live and has no
signal that a newer deploy serves traffic. So the existing admission
gate (`closeProcessRunAdmission`) was not bypassed. The only gap in the
gate was the check-then-claim window inside
`startNextQueuedRunForAgent`, which this PR closes. Holding work on a
superseded but not yet signaled server needs a new "superseded deploy"
signal; this PR does not add one.

## Verification

- Red on master (`23909077ad`): `node cli/node_modules/tsx/dist/cli.mjs
packages/db/src/test-postgres-command.ts -- node
node_modules/vitest/vitest.mjs run --project @paperclipai/server
server/src/services/native-runtime/native-runner-restart-recovery.integration.test.ts
-t "closed the runtime listener before it attached"`: the three restart
cases failed with `"kind": "reattach_remote_runner"` (the staging
disposition); the five held cases passed.
- Red on master: `queue.test.ts -t "closes admission during the queue
scan"` failed (`expected [ { …(81) } ] to deeply equal []`).
- Green with the fix: both filters pass.
- `native-runner-restart-recovery`, `native-runner-restart-preparation`,
`native-startup-retry` and `native-controller-recovery` integration
files: 267 passed, 14 skipped.
- `queue.test.ts`: 89 passed.
- `cd server && pnpm typecheck`: passed.
- Chaos waves: see the PR comments for the reruns of seeds 9511 (CR1)
and 9513 (K15 overlap).

## Risks

- The new proof applies only to the closed-listener `bootstrap_retry`
that #20786 writes, on a remote sandbox, with a carried session that a
settled predecessor saved. Every other shape keeps its earlier
classification.
- A `bootstrap_incomplete` claim sends the run through the existing
bootstrap path, which verifies the prior owner of the session root
before it starts. The s9205 follow-ups already use this path with an
inherited Runner identity.
- Upgrade safety: no persisted shape changes. The proof reads only
fields that the current and recent releases write. A row saved without
these facts keeps the earlier verdict.
- No wait or file work is added before `turn.start`. The new queries run
only in restart recovery.
- The queue change only stops claiming after admission closed. Runs stay
queued for the next server, the same as the existing entry check.

## 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>
canary/v2026.1112.0-canary.1
2026-11-11 18:36:45 -06:00
DottaandClaude Opus 5.5 66344b76ae fix(native): restart a bootstrap whose Runner exited before writing provider state on a resumed sandbox (#20972)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs execute through a Runner (`paperclip-runnerd`) inside a
remote sandbox. A warm Daytona sandbox is reused by later runs.
> - When a Runner exits before `session.open`, the run does a bounded
same-run bootstrap retry. Durable evidence proves that no provider
session and no provider event exist.
> - The retry fails when the Runner wrote `runner-state.json` but exited
before it wrote its provider state. Host preparation then calls the
sandbox state "unavailable" and refuses with
`runner_harness_state_mismatch: resumed_unavailable`. The task needs
Retry.
> - This pull request reads the Runner state on its own when the full
check fails, but only for a proven bootstrap restart of the same run. A
non-suspended Runner state of this run is then the run's own interrupted
bootstrap, and the existing restart path moves it aside and claims a new
session root.
> - The benefit is that a Runner that exits before `session.open` on a
reused sandbox recovers without a person. Saved state, a verified backup
and every other mismatch keep their refusal.

## Linked Issues or Issue Description

**What happened?**
Restart-chaos wave `w-20261111T230537-s6402` (Claude through ACPX,
Daytona warm sandboxes, `--ephemeral-server`, K1/K2/K15 with
`--k15-fresh-home`, seed 6402) needed Retry on 4 of 6 tasks, with the
same signature after each restart kind. The first attempt of each run
reused the warm sandbox. Its Runner failed with
`runner_ingress_bind_conflict: failed to bind listener: Address already
in use` and exited with `lifecycle=connecting`. Recovery logged
"Restarting an incomplete native runner bootstrap on the same heartbeat
run". The second attempt reused the environment and failed with
`runner_harness_state_mismatch: resumed_unavailable`. The earlier wave
`w-20261111T212429-s6402` had one run with the same error code.

**Root cause**
`inspectRemoteHarnessState` in
`server/src/services/native-runtime/native-session-executor.ts` checks
the persistence directories, `runner-state.json` and the provider state
file in one shell test. A Runner that exits at bind time wrote
`runner-state.json` but no provider state, so the one test fails and the
result is `incompleteReason: "unavailable"` before the Runner state is
read. The restart path (`restartableBootstrapState`) accepts only
`not_suspended` and `provider_unopened`, so it is skipped.
`claimUntouchedRemoteSession` then cannot create the session root that
the first attempt created, and the resumed branch throws
`resumed_unavailable`. On a created lease the same state fell through
without a restart.

**Expected behavior**
A proven, interrupted bootstrap of the same run restarts on the same
sandbox. The run finishes without Retry.

**Steps to reproduce**
Run the s6402 cell above, or add `--runner-bind-fault` (this pull
request) to force the Runner exit on every run's first launch.

## What Changed

- `server/src/services/native-runtime/native-session-executor.ts`: when
the full harness check fails during host preparation,
`inspectOwnUnpreparedBootstrap` reads `runner-state.json` alone. It
returns `not_suspended` for this run only when all of these are true:
this run has a proven bootstrap restart (`bootstrap_incomplete` recovery
plus the stop proof), no failover backup exists, no archived rotation
snapshot is in use, and the Runner state names this run, this Runner
instance and this session with a lifecycle other than `suspended`.
Otherwise the result stays `unavailable`, as before. The existing
restart path then moves the session root aside (it is kept, not deleted)
and claims a new one.
- Chaos coverage: `--runner-bind-fault` in `tests/restart-chaos` gives
every server of the wave
`PAPERCLIP_TEST_RUNNER_BIND_FAULT=first_launch`. With `NODE_ENV=test`
only, the remote Runner launcher starts a short-lived listener on the
Runner's listen port before a Runner's first launch on that server, so
the Runner exits with a real `runner_ingress_bind_conflict`. Documented
in `tests/restart-chaos/README.md`.

## Verification

- Red on master: `pnpm --filter @paperclipai/server exec vitest run
src/services/native-runtime/native-session-executor.test.ts -t
"bootstraps only an untouched provider session"` failed 2 of the new
cells. Resumed lease: `Error: runner_harness_state_mismatch:
resumed_unavailable` (the staging error). Created lease: no restart
happened (`expected [] to have a length of 1 but got +0`).
- Green: the same cells pass. The negative cells keep
`resumed_unavailable`: no restart proof, a Runner state of another run,
a `suspended` Runner state with missing provider state, and a failover
backup present.
- `native-session-executor.test.ts`: 1406 of 1406 pass. A new test
proves the bind fault holds the listen port (a second bind gets
`EADDRINUSE`) and fires once per Runner instance.
- `tests/restart-chaos`: `runner-bind-fault.test.ts`,
`question-handoff.test.ts`, `shape-cells.test.ts` pass. Server and
harness `tsc --noEmit` pass.
- Chaos wave: see the comment below for the before/after counts.

## Risks

- The new read runs only for a proven bootstrap restart of the same run
with a failed full check, so the normal path has no extra command and no
wait before `turn.start`.
- A partly saved session (`suspended` Runner state without provider
state), a Runner state of another run and any failover backup keep
today's refusal. The moved-aside root is kept for inspection.
- A Runner that started under an older server and is restarted by this
version takes the same path, because it reads only files that older
Runners already write.
- The bind fault is test instrumentation. It is inert unless
`NODE_ENV=test` and the variable are both set.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, through Claude Code
with tool use (shell, file edit, test runs). Default context window,
extended reasoning on.

## Checklist

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

Related: #20928 (restart a Runner that exited before session.open),
#20874, #20863, #20953.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 18:36:33 -06:00
DottaandClaude Opus 5.5 afcf34c9da fix(evals): form repair-cost-recording's saved-usage debt and grade it by saved state (#20895)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The chat feature evals check that the chat agent helps a person with
real product state
> - The `budgets-costs.repair-cost-recording` case checks that the agent
sends the person to the saved-cost retry control and then reports the
true accounting result
> - The grader checked the agent's replies with keyword regular
expressions, and the scripted person gave the agent the harness's own
accounting readback
> - Keyword checks reject valid wording and accept wrong answers, so the
case did not measure the promise
> - The setup also never formed: the prerequisite run always settled, so
no "Ready to record" saved usage existed
> - This pull request forms that state with a temporary trigger on the
case-owned database (no product change), and replaces the two keyword
checks with checks of saved state, the agent's own product calls, and
rendered links
> - The benefit is a case that can run, and a grader that does not
depend on wording and that still fails the relevant broken behavior

## Linked Issues or Issue Description

**What happened**
The case `budgets-costs.repair-cost-recording` graded the chat replies
with keyword regular expressions (`honest-post-retry-accounting-result`
and `guides-accounting-without-rerun`). The scripted follow-up message
also told the agent the harness's accounting result. Separately, the
setup never formed in paid runs: the prerequisite native run settles as
an unpriced cost event (`missing_price`), not as a `retryable`
saved-usage row. Accounting retries every 5 seconds, so a retryable row
only exists while accounting keeps failing.

**Expected behavior**
The grader accepts any wording. It requires that the guidance before the
retry links the Costs page, that the post-retry run reads saved cost
data before it answers, and that the agent does not write cost, budget
or accounting records or start work.

**Steps to reproduce**
1. Read `gradeAccounting` in
`tests/runner-e2e/chat-catalog-domains/identity-accounting.ts` on
master.
2. Reword a valid reply (for example "Head to Costs and press the button
there; the work stays as it is.").
3. See that the keyword checks reject it.

Refs #20600

## What Changed

- New `tests/runner-e2e/chat-accounting-fault.ts`, following
`chat-archive-storage-fault.ts`: a temporary trigger on the case-owned
database refuses cost-event writes for the one prerequisite task. The
product's own accounting transaction rolls back, so the finished run
keeps its saved usage pending and the product's automatic retries keep
failing. Refused writes are counted in a rollback-proof sequence. It is
allowed only when this case is the single selected execution, and it
never writes product rows.
- Setup: the case creates the prerequisite task unassigned, arms the
trigger, assigns the task, and waits until Costs shows the genuine
`retryable` row. The trigger is closed on a setup failure and at
cleanup.
- Act: the trigger is dropped right before the person clicks "Retry cost
recording". Automatic recovery can record it first; the saved accounting
read afterwards classifies either result (an unpriced event still shows
in data quality, so it counts as unresolved).
- `chat-archive-storage-fault.ts`: export its owned-database resolver
for reuse (no behavior change).

- `identity-accounting.ts`: remove the reply keyword checks for the
repair case.
- Add `initial-guidance-links-saved-cost-retry`: a reply from before the
retry has a rendered link to the Costs page (`/<prefix>/activity/costs`
or the `/<prefix>/costs` redirect).
- Add `post-retry-reply-reads-saved-accounting`: the post-retry chat run
made a successful read of a company cost report, or of the cost summary
of the prerequisite task (bound by its redacted input digest).
- Add `no-agent-accounting-fabrication-or-rerun`: no successful
chat-agent call writes cost, finance, budget or accounting records or
starts run or task execution. A denied attempt is not a write.
- `observe` collects the chat agent's redacted `call_api` uses and
results (`leadApiCalls`).
- The scripted follow-up now reports only the toast that the page
showed. That toast says "Costs recorded." even when the usage still has
no price.
- Own `oracleVersion`
(`paperclip.chat-catalog.repair-cost-recording.structural.v3`) and
source files in code and in `chat-catalog-descriptors.json`.
- Calibration tests: reworded valid replies pass; a missing or late
Costs link, a run link only, a foreign link, no fresh read, a read in
the wrong run, a denied read, an unrelated read, another task's cost
summary, and agent cost or execution writes fail.

## Verification

- `pnpm exec vitest run --config tests/runner-e2e/vitest.config.ts
tests/runner-e2e/chat-accounting-fault.test.ts
tests/runner-e2e/chat-archive-storage-fault.test.ts
tests/runner-e2e/chat-catalog-domains/identity-accounting.test.ts
tests/runner-e2e/chat-catalog-contract.test.ts
tests/runner-e2e/chat-catalog-consistency.test.ts`: all pass. The
embedded-Postgres test drives the product's `accountRunCost`,
`reconcileRunCosts`, data-quality report and board retry against the
trigger: only the faulted task's run stays `retryable`, other debt in
the company settles, and the retry records it once the trigger is
dropped.
- `npx tsc --noEmit -p tests/runner-e2e`: no errors in the changed
files.
- `node tests/runner-e2e/source-sdk-public-inputs.mjs <sha> --check`:
unchanged.
- Paid run still needed:
`chat-features.runner-opencode.local.budgets-costs.repair-cost-recording`.

## Risks

- Low risk. The change affects only this eval case.
- The new read check needs the chat agent to have company cost-read
access. Agents cannot read `costs/data-quality` (board only), so the
agent uses the cost reports or the task cost summary.
- The trigger runs DDL on the case-owned database only; the same
isolation checks as the archive storage fault apply, and it is dropped
before the retry and removed at cleanup.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 18:29:54 -06:00
DottaandClaude Opus 5.5 a75d7df9c7 fix(evals): gate steering approval-delivery and steer-active on real product state (#20899)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The chat feature catalog runs real native agents to prove the chat
and task-steering promises.
> - Two steering cases failed in setup before they could measure the
product.
> - `steering.approval-delivery` waited for a worker run to stay running
while its question was pending. The product parks a native turn as soon
as a durable question is saved, so that state never occurs.
> - `steering.steer-active` waited for the queue to say steering is
"available". The OpenCode harness reports that it cannot steer, so the
queue says "unsupported".
> - This pull request gates each case on the real product state and
grades the behavior that state promises.
> - The benefit is that both cases now measure their user promise on the
evaluated profile, without faked runs or faked steering.

## Linked Issues or Issue Description

**What happened**
Both cases failed during setup on `runner-opencode`.
`steering.approval-delivery` stopped with "The actual question fixture
did not reach its running pending-input boundary": the worker's
`request_human_input` call parked its turn (run succeeded,
`reportedWorkDisposition: yielded`, continuation `response_wake` for the
question). `steering.steer-active` timed out waiting for
`steeringDisposition: "available"`; the OpenCode driver declares
`steering: false`, so the queue reports `"unsupported"` and the page
shows Interrupt instead of Steer.

**Expected behavior**
`steering.approval-delivery` should treat the saved yield and the
pending question as the pending-input state, queue ordinary comments and
the bound answer behind a real active worker turn, and prove one
continuation receives the answer. `steering.steer-active` should grade
same-turn steering when the harness can steer, and grade the honest
fallback when it cannot: the message stays visibly queued, nothing is
recorded as steered, and one normal turn consumes it.

**Steps to reproduce**
1. Dispatch
`chat-features.runner-opencode.local.steering.approval-delivery` on
master.
2. See the setup stop after the worker run succeeds with a yielded
result.
3. Dispatch `chat-features.runner-opencode.local.steering.steer-active`
on master.
4. See setup time out while `GET /api/issues/:id/queued-comments`
reports `steeringDisposition: "unsupported"`.

## What Changed

- `runtime-approval-queue.ts`: gate the question on the parked turn
(`pendingInputYield`: succeeded, yielded, `response_wake` keyed to the
question). An ordinary human message then wakes a real second Morgan
turn that waits at the task gate. Ordinary comments and the bound answer
queue behind that turn. The run count goes from 3 to 4.
- `runtime-approval-queue.ts`: remove the reply keyword checks
(`acornNamingDirection` and the "I have answered" regex). Grade exactly
one saved answer path instead: the human response control, or an
attributed canonical response.
- `runtime-active-steering.ts`: accept either definitive capability.
Unsupported branch: no enabled Steer control, a visible Interrupt
control, a steer attempt with the person's session refused with `409
steering_unsupported`, no `issue.queued_comment_steered` activity, the
exact message still queued, and one continuation that consumes it and
registers a rendered blue `website.html`.
- `runtime-active-steering.ts`: remove the reply keyword checks and the
`/blue/` match on the delivery document. The rendered page proves the
color.
- Update calibration tests in both directions, the workspace setup test,
and the descriptors (`oracleVersion` bumps, `expectedRunCount` 4 for
approval-delivery). The scenario YAML is unchanged.

## Verification

- `pnpm exec vitest run --config tests/runner-e2e/vitest.config.ts
tests/runner-e2e/chat-catalog-domains/runtime-active-steering.test.ts
tests/runner-e2e/chat-catalog-domains/runtime-approval-queue.test.ts
tests/runner-e2e/chat-catalog-domains/runtime-approval-workspace.test.ts
tests/runner-e2e/chat-catalog-contract.test.ts
tests/runner-e2e/chat-catalog-consistency.test.ts`: 87 passed.
- `npx tsc --noEmit -p tests/runner-e2e`: no errors in the changed
files.
- `node tests/runner-e2e/source-sdk-public-inputs.mjs <HEAD> --check`:
unchanged.
- The live proof is a paid run of
`chat-features.runner-opencode.local.steering.approval-delivery` and
`chat-features.runner-opencode.local.steering.steer-active`.

## Risks

- approval-delivery now depends on a human message waking the worker
while its question is pending. The product does this
(`supersedeOnUserComment: false` keeps the card open, and comments wake
the assignee unless the task is done or cancelled). If the worker does
not run the gate on that turn, setup fails with a named label.
- steer-active on OpenCode now measures the honest unsupported fallback,
not same-turn delivery. A steering-capable profile still runs the
same-turn branch.
- Low risk to the product: only eval code changes.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 18:28:44 -06:00
Nicky LeachandPaperclip 38501b1170 Add turn-scoped attachment downloads to native Runner (#20956)
## Thinking Path

> - Paperclip manages AI agents and their work.
> - Native agents open input images with a file reader.
> - Plugin Runner tasks use an authenticated Runner connection.
> - Runner needs a way to fetch an authorized file from the tenant app.
> - The tenant grants access to the exact attachment for the active run.
> - Runner downloads and verifies it before it returns a local path.

## Linked Issues or Issue Description

Refs #19740. Builds on the attachment authorization and command
transport added in #20939.

## What Changed

- Add the PRP v3 `runner.attachment.download` command for native
workspace providers.
- Issue short-lived grants for exact authorized wake and historical
attachments. Recheck the active run, source, size, and SHA-256 at the
tenant endpoint.
- Download current-wake inputs before provider dispatch. Download
historical files when the authorized chat reader asks for them. Preserve
SSH and sandbox command staging as a fallback.
- Run HTTPS transfers off the Runner control loop. Report completion
with one PRP event per transfer. Drop pending results on cancellation
and clear files at turn settlement, authority rotation, and restart.
- Grant restricted Codex sessions read-only access to the attachment
directory.

## Verification

- 69 targeted server attachment tests passed. They cover wake and
historical downloads, authorization failures, and command fallback.
- 22 focused historical attachment tests passed, including grant
redemption under a held database lock.
- Six focused Rust attachment tests passed, including a source-pinned
Codex sandbox check. The sandbox could read the staged file but could
not modify it or read adjacent Runner state.
- The real Runner transport test confirmed one durable command and one
completion event for a transfer. Restart-continuation tests confirmed
fresh attachment paths appear before dispatch.
- Server and Runner TypeScript checks, Rust formatting, and PRP
generated schema checks passed.
- Required GitHub CI passed on `7a33752bd7`. [CI
run](https://github.com/paperclipai/paperclip/actions/runs/43122967837)
- Greptile scored the same revision 5/5 with no unresolved review
threads.
[Review](https://github.com/paperclipai/paperclip/pull/20956#issuecomment-6808582134)

## Risks

- Saved provider and Runner identities keep their existing formats. A
previous Runner falls back to command staging where the target supports
it. A restart continuation refreshes temporary attachment paths before
dispatch.
- Plugin Runner tasks require a compatible Runner ROM and outbound HTTPS
access to the configured tenant origin.
- Live agent-computer qualification remains a release check.

## Model Used

OpenAI GPT-6 through Codex, with repository inspection, shell tools, and
local test execution. Exact variant and context window are unavailable
in this session.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-11 16:28:02 -08:00
DottaandClaude Opus 5.5 ba5ba54439 fix(evals): name unmet completion-gate readiness fields and grade the gate read, not reply words (#20887)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The chat catalog eval `tasks.completion-with-gate` checks that the
CEO does not force Done while an independent human review is still
required
> - Its setup waits for a real Morgan implementation that awaits the
configured review stage
> - The setup timed out and did not say which state field was wrong
> - The retained evidence shows a product defect: the native completion
path never started the configured review stage (fixed in #20882)
> - This pull request makes the readiness timeout name each unmet field,
accepts valid product continuation runs, and replaces reply keyword
grading with a structural check
> - The benefit is that the eval measures the gate, and its setup
failures are self-explaining

## Linked Issues or Issue Description

**What happened**

The eval failed with "Timed out waiting for Real prior Morgan
implementation awaiting independent review". Morgan uploaded
`website.html` and reported needs_review. The task became in_review
through a generic review card, but `executionState` stayed null, so the
poll (pending stage for Riley) never matched. The run finished at 229 s
of the 240 s setup window. The grader also matched reply words with
regular expressions.

**Expected behavior**

Setup reaches the configured review stage through supported product
paths. A timeout names each unmet field. The grader proves the outcome
from saved state and run receipts, not reply wording.

**Steps to reproduce**

1. Dispatch
`chat-features.runner-opencode.local.tasks.completion-with-gate` on
master.
2. Read `server.log` and `snapshots/chat-final-run-ledger.json`:
`authoritativeDecision: in_review`, `finalizationReasonCode:
actionable_attention_pending`, no executionState.

## What Changed

- `internal-task-completion.ts`: `unmetPriorDeliveryReview` lists each
unmet readiness field. The poll uses it for `accept` and
`timeoutDetail`.
- Setup and grader accept one or more succeeded Morgan runs (a product
continuation is valid). Any failed, cancelled or timed-out worker run is
an unmet readiness condition, so setup stops before the paid CEO turn.
The delivery must come from one of these runs.
- The `truthful-outstanding-review` reply regex check is replaced by
`checked-actual-completion-gate`: the CEO's chat run read the task (`GET
/api/issues/{id}` or `get_task`, matched by record-bound receipt) and
replied. The saved pending stage, unchanged policy and absence of a
forced Done still prove the outcome.
- Run bound 2 → 3, setup window 240 s → 330 s, attempt 540 s → 660 s.
`oracleVersion` is `paperclip.chat-catalog.completion-with-gate.v2`. The
descriptor is updated.
- Cherry-picks both #20882 commits so the paid run exercises the product
fix.

## Verification

- `pnpm exec vitest run --config tests/runner-e2e/vitest.config.ts` on
`internal.test.ts`, `internal-comment-reference.test.ts`,
`internal-edit-ownership.test.ts`, `chat-catalog-contract.test.ts`,
`chat-catalog-consistency.test.ts` (184 tests) and 12 other suites that
reference the case (133 tests).
- Calibration: two reworded valid replies pass. A missing gate read, an
empty reply, a forced Done and a failed worker run fail. The retained
product state (in_review, no executionState) produces named unmet
fields.
- `node tests/runner-e2e/source-sdk-public-inputs.mjs <HEAD> --check`:
unchanged.
- `npx tsc --noEmit -p tests/runner-e2e`: no new errors in the changed
test files. The local worktree has unbuilt packages, so CI supplies the
full typecheck.

## Risks

- The reply's meaning is no longer machine-checked. A person must read
the reply in the paid run.
- The larger run bound and windows accept slower valid runs. Readiness
still fails on any failed worker run.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 18:17:43 -06:00
DottaandClaude Opus 5.5 87fe4d9793 fix(workspaces): retry a workspace_finalize record write that loses a deadlock (#20971)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - When a run ends, Paperclip records a `workspace_finalize` workspace
operation. Dependent work reads that record to know if the workspace is
safe to use.
> - The record insert has foreign keys to `execution_workspaces`,
`companies`, `heartbeat_runs` and `issues`. Postgres takes a key-share
lock on each parent row, in trigger order: workspace, company, run,
issue.
> - The native finalizer and other writers lock the issue row and then
the run row (the order that #20819 documents). When such a transaction
holds the issue and then asks for the run, while the finalize insert
holds the run and waits for the issue, Postgres detects a deadlock and
aborts one side.
> - In restart-chaos wave `w-20261111T205259-s6301` (task t01, run
`e7129ec7`) the insert lost. The log says "failed to record
workspace_finalize=failed operation; dependents may remain gated". The
failed-finalize record was lost.
> - The insert's lock order comes from the foreign keys, so the code
cannot reorder it without a new explicit lock order that other writers
(for example the remote-workspace release path, workspace then run)
could invert again. A deadlock victim is safe to run again, because its
transaction rolled back completely.
> - This pull request retries the finalize record write on lock timeout,
deadlock and serialization failure (55P03, 40P01, 40001), with the same
bounded policy that #20850 added for the provider-pack admission write.
> - The benefit is that a short lock conflict can no longer delete the
record that releases dependent work.

## Linked Issues or Issue Description

**What happened?**
Run `e7129ec7` failed its native session recovery
(`runner_harness_state_mismatch: backup_without_reusable_lease`). The
failure path called `recordWorkspaceFinalize("failed")`. The insert into
`workspace_operations` failed with `PostgresError: deadlock detected`
(40P01). The server logged that dependents may remain gated. The run
then settled as failed with `native_session_interrupted`; the next
`finalizeNativeRun` call logged `native_finalization_missing` because
that run had no proposed result.

**Expected behavior**
A transient deadlock does not lose the finalize record. The write runs
again and the record is saved.

**Steps to reproduce**
1. Transaction A: lock the issue row `FOR UPDATE`.
2. Start a `workspace_finalize` record for a run on that issue. Its
insert takes key-share on the workspace, company and run rows, then
waits for the issue.
3. Transaction A: lock the run row `FOR UPDATE`.
4. Postgres reports a deadlock. Before this change the insert is the
victim and the record is lost.

**Additional context**
The new test reproduces this with two real transactions on embedded
Postgres. It gives transaction A a longer `deadlock_timeout`, so the
insert is always the victim, as on the chaos run.

Refs #20850
Refs #20819

## What Changed

- New `server/src/services/transient-lock-contention.ts`:
`postgresErrorCode`, `isTransientLockContention` and
`retryTransientLockContention` (5 attempts, 100 ms × attempt backoff,
only 55P03, 40P01 and 40001).
- `workspaceOperationService.createRecorder().recordOperation` wraps its
`workspace_finalize` state writes (the running-row insert and the
terminal update) in that retry. Each attempt is a new transaction, or a
new savepoint when the service runs inside a caller's transaction.
- `native-provider-pack-admission.ts` uses the shared helper. Its
behavior and its exported `nativeProviderPackAdmissionFailureCode` name
do not change.
- New test in
`server/src/__tests__/workspace-operations-reconciliation.test.ts`: a
real two-transaction deadlock between an issue-then-run locker and the
finalize insert.

## Verification

- Red on master: `cd server && npx vitest run
src/__tests__/workspace-operations-reconciliation.test.ts -t "loses a
deadlock"` failed with `deadlock detected`, code `40P01`, on the same
`insert into "workspace_operations"` query as the chaos log.
- Green: the same test passes; I ran it 3 times. The full file and the
provider-pack admission tests pass (49 tests).
`native-result-commit-work.test.ts` passes.
- `npx tsc --noEmit -p server` passes.
- I stopped `pnpm verify` after it selected 2533 test files; required CI
supplies broad coverage.
- Chaos: no new cell. The existing W2 × K1 cell (Codex, Daytona warm)
reached this path when the restart fault broke native session recovery.
I did not rerun a wave; both wave slots were busy.

## Risks

- Low risk. Only the `workspace_finalize` state writes retry, and only
on the three contention codes. Other errors throw as before.
- A deadlock victim's transaction is fully rolled back, so the retried
insert (same operation id) and the retried update are idempotent.
- The worst added delay is about 1 s of backoff across 5 attempts, on
the failure path only.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code with tool use (shell, file edits, local 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-11 18:09:57 -06:00
DottaandClaude Opus 5.5 1880526603 fix(runner): never start an OpenCode session without its Paperclip tools (#20964)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native OpenCode agents use Paperclip tools through a local MCP
bridge in the runner
> - OpenCode 1.18 connects each MCP server one time per process. It does
not retry
> - One failed `initialize` or `tools/list` at startup marks the
Paperclip server "failed" for the full session
> - The agent then works with only built-in tools. In one chat it told
the person "Switched the working design to blue", but no saved state
supported this claim
> - This pull request makes the runner confirm the Paperclip tools while
the session loads, reconnect them, restart OpenCode if necessary, and
fail the session by name if the tools stay unavailable
> - The benefit is that an OpenCode turn never starts without its
Paperclip tools, so the agent cannot silently claim actions that it
could not do

## Linked Issues or Issue Description

Refs #20886, #20921.

**What happened**
In chat eval `steering.steer-active` (private paid run 43086418488, on a
commit with #20886 and without #20921), two of three OpenCode sessions
started without Paperclip tools. One was the CEO chat (fresh session).
One was the worker task after its saved session resumed in a new
OpenCode process. The CEO agent ran `opencode mcp list` and saw
`paperclip ✗ failed — Failed to get tools`. It then sent a request to
the bridge without the bearer token and got HTTP 401. That 401 came from
its own probe, not from OpenCode. The run still succeeded with the reply
"Switched the working design to blue …", because a plain chat reply
counts as the result.

**Expected behavior**
Every native OpenCode session starts with the Paperclip MCP tools. If
the tools cannot be loaded, the session fails with a named reason, and
the turn does not start without tools.

**Steps to reproduce**
1. Start OpenCode 1.18.34 with a remote MCP server whose first
`tools/list` request fails (for example, the server resets the socket
one time).
2. `GET /mcp` returns `{"paperclip":{"status":"failed","error":"Failed
to get tools"}}`. OpenCode keeps this status for the life of the
process.
3. `POST /mcp/paperclip/connect` returns `true`, and then `GET /mcp`
returns `connected` (about 8 ms). We measured this with the real 1.18.34
binary.

This is the same class as #20886: `Connection: close` made the one-shot
startup connect fail more often (about 9 of 25 sessions), and #20921
removed the trigger. Any other transient failure at startup still
produced a toolless session. This change removes the class.

## What Changed

- `opencode-server-driver.ts`: while a session opens (fresh, resumed or
recovered), the driver reads OpenCode's `GET /mcp`. If the `paperclip`
server is not `connected`, it calls `POST /mcp/paperclip/connect` up to
two times. If the server is still not connected, it throws
`paperclip_tools_unavailable`. The existing startup retry restarts
OpenCode for this error (three attempts in total). After that, the
session fails visibly.
- The check runs in parallel with session creation or resume. The added
work on a healthy start is one local HTTP request.
- `fake-opencode-server.mjs`: the fixture now serves `GET /mcp` and
`POST /mcp/paperclip/connect`. It connects to the real bridge with the
configured token, and it can simulate failed connects.
- New `paperclip-tools-startup.test.ts`: reconnect after one failed
connect, restart after a process stays failed, reconnect on a saved
session that resumes in a new process, and a named failure when the
tools never load. All four tests fail on master (master opens a toolless
session).

## Verification

- `pnpm exec vitest run
src/drivers/opencode/paperclip-tools-startup.test.ts` (in
`packages/paperclip-runner`): 4 passed. Red on master: 4 failed.
- All OpenCode driver, proxy, live runnerd OpenCode and native-runtime
server suites that use the fixture: 29 files, 567 tests passed.
- `node --test scripts/contracts/contract-fingerprints.test.mjs`: 11
passed. `tsc --noEmit -p packages/paperclip-runner`: clean.
- Real OpenCode 1.18.34 probe (failure, then `connect`, then
`connected`) as in the steps above.
- Paid chat eval run 43117909094 on this change (steering.steer-active
and three other OpenCode chat cases). Result is in a comment on this PR.

## Risks

- Low. A healthy start adds one local `GET /mcp` request, in parallel
with session creation.
- If OpenCode cannot load the Paperclip tools after reconnects and
restarts, the session now fails with `paperclip_tools_unavailable`.
Before, it ran without tools. This is intended.
- No persisted run, session or pack state changes. A session saved by
the previous release resumes through the same open path and gets the
same check.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), agentic coding with
tool use (shell, file edits, GitHub CLI).

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 18:07:38 -06:00
DottaandClaude Opus 5.5 23909077ad fix(retry): re-deliver the failed run's wake when the board retries it (#20970)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - When a run fails, the board shows a Retry button. The Retry starts a
new run with the wake reason `task_retry`.
> - A failed run can serve a wake that happens once, for example a
scheduled task monitor check (`issue_monitor_due`). The monitor is
consumed when it fires.
> - The Retry run did not tell the agent which wake it retries. The
agent saw only `task_retry` and the earlier interruption.
> - In the restart-chaos harness, the agent followed its brief ("act
only on a board comment or a scheduled monitor check"), decided that the
Retry was not the monitor check, and changed nothing. The run succeeded,
but the task stayed `in_progress` with no run, no wake and no monitor.
The person clicked a Retry that worked mechanically but lost the work it
was for.
> - This pull request records the retried run's own wake reason on the
Retry's continuation trigger (`trigger.retriedReason`) and shows it in
the wake summary.
> - The benefit is that a Retry re-delivers the same wake that failed.
Dotta's rule: if a Retry button shows up, it still needs to work.

## Linked Issues or Issue Description

**What happened?**
Restart-chaos wave `w-20261111T230537-s6402` (Claude, Daytona warm,
volume-free server mode, restart fault K1), task t03 (workload W2). The
phase-2 monitor wake started run `60d821af`. That run failed with
`native_session_interrupted`. The board Retry was accepted (HTTP 202).
Retry run `a5a3958e` started 2 s later and succeeded. Its context had
`wakeReason: task_retry` and `executionContinuation.trigger = { reason:
"task_retry", sourceRunId: "60d821af…" }`, with no reference to
`issue_monitor_due`. The agent's summary: "This wake was a task_retry
resuming the previously interrupted run, not a new board comment or
scheduled monitor check, so per instructions I changed nothing." The
issue stayed `in_progress` with `executionRunId = null`, monitor status
`triggered` and no `nextCheckAt`. The harness graded
`R3:retry_did_not_complete` and U1 `no_owner_no_action 182s`.

**Expected behavior**
A Retry of a failed run re-delivers that run's wake. A Retry of a failed
monitor check is handled as that monitor check.

**Steps to reproduce**
1. Give an agent a task that acts only on a scheduled monitor check.
2. Let the monitor fire, and make the monitor run fail (restart fault K1
during the run).
3. Click Retry on the task.
4. Before this change, the Retry run's prompt says only `reason:
task_retry`, and the agent does not do the monitor work.

**Additional context**
The same signature (`R3:retry_did_not_complete:W2C:K0`, Codex, monitor
rewake run `bac90594` failed `native_session_interrupted`, Retry run
`cfb24146` succeeded without the phase-2 marker) is in wave
`w-20261111T192658-s5201` task t06. That case was attributed to the
response-wake loop that #20935 fixed. Its shape matches this cause: the
Retry run served the monitor wake but did not do the monitor work. That
wave kept no database, so I could not read its run context directly.

Refs #20935

## What Changed

- `buildExecutionContinuation` adds `trigger.retriedReason` when the
current wake is a Retry (`task_retry` or `retry_failed_run`). The value
is the source run's own wake reason. A Retry of a Retry inherits the
first non-retry reason from the source run's saved continuation.
- `ExecutionContinuationEnvelope.trigger` gets the optional
`retriedReason` field.
- `renderPaperclipWakePrompt` adds `- retried wake: <reason> (this Retry
re-delivers the wake the failed run did not finish; handle it as that
wake)` to the wake summary for fresh and resumed sessions.
- New test in `server/src/services/execution-continuation.test.ts` on
embedded Postgres: a failed `issue_monitor_due` run, a Retry of it, a
Retry of that Retry, and a non-retry wake.

## Verification

- Red on master: `cd server && npx vitest run
src/services/execution-continuation.test.ts -t "carries the failed run"`
failed with `trigger` = `{ reason: "task_retry", sourceRunId }` and no
`retriedReason`.
- Green: the same file passes (45 tests).
- `tsc --noEmit` passes for `packages/shared`, `packages/adapter-utils`
and `server`.
- `pnpm verify` exits 2 (a shared type changed), so required CI supplies
broad coverage.
- Chaos: the existing cell W2 × K1 (Claude, Daytona warm) reproduces
this when the fault lands in the monitor rewake run. W2C (Codex) shows
the same shape. I did not rerun a wave for this PR.

## Risks

- Low risk. The change adds one optional field and one prompt line. It
changes no admission, lock, status or authority path.
- The field is computed only for Retry wakes. Other wakes are unchanged.
- Runs saved by an older version have no `retriedReason`. A Retry built
from them reads the source run's `wakeReason`, which older runs already
saved.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code with tool use (shell, file edits, local 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-11 18:04:19 -06:00
DottaandClaude Opus 5.5 2539004201 fix(ephemeral): start a restarted volume-free server over the Runner controller files it wrote itself (#20952)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Cloud servers run volume-free (`PAPERCLIP_EPHEMERAL_SERVER=true`).
Postgres and object storage hold the durable state. A startup gate
refuses legacy volume data that was not migrated.
> - The gate reads one family of data from disk: Runner controller files
under `runtime/paperclip-runner/durable-sessions/`. It treats a
controller file that the database does not prove as legacy data.
> - A volume-free server also writes these files during normal work. A
warm follow-up restores the previous run's checkpoint as a full
`durable.control-plane-state.v2` file. The file stays full until the new
controller registers its database checkpoint writer.
> - When the server crashes in that window and restarts on the same
filesystem, the gate finds its own file and refuses to start. Every
later start refuses the same way. This is a full outage that no Retry
can fix.
> - This pull request gives the gate a durable marker. A volume-free
server claims an empty state directory before any controller can write.
Files in a claimed directory belong to a volume-free server and do not
block startup.
> - The benefit is that a crash restart of a volume-free server starts
again. Genuine legacy data keeps its protection, and the refusal now
names the run, task and file.

## Linked Issues or Issue Description

**What happened?**
The first restart-chaos waves in volume-free server mode (harness flag
`--ephemeral-server`, #20930) found this outage. Wave
`w-20261111T212429-s6402` (Claude through ACPX, Daytona warm sandboxes)
did a fresh-home deploy and then a `kill -9` of that server. The
replacement server exited with `Volume-free server data is not eligible:
native_controllers_require_migration`. A second start at +608 s exited
the same way. The server stayed down.

**Root cause**
`hasLocalNativeControllerObligations` in
`server/src/services/native-runtime/native-controller-migration.ts`
scans every Runner session in the state directory. The kept instance
shows the cause. The session's newest run `4a4d7ca1` was `running`
(admitted 3 s before the kill). Its controller root held the full
`paperclip.runner.durable.control-plane-state.v2` file of its warm
predecessor `9c13b7f8`. A controller writes that full file when it opens
and replaces it with the `external-control-plane-state.v1` marker only
in `setCheckpointPersistence`. `preservedActiveCheckpoint` accepts only
the marker, so the gate returned `true`. A Codex wave with the same
fault sequence passed because no warm follow-up was in this window at
the kill. Claude W2 cells start a new warm follow-up every 15 to 35 s,
so the window is open much more often.

**Can staging hit it?**
Yes, with a restart that keeps the container. Railway documents that a
manual Restart "restarts the process within the deployment's container",
so the filesystem stays. Railway does not document whether an automatic
crash restart reuses the container. A redeploy makes a new container, so
it does not hit this.

**Expected behavior**
A volume-free server starts again after any restart. State that it wrote
itself is disposable, because the database checkpoint is the durable
record. Data that a volume-backed server wrote keeps its current
protection.

## What Changed

- Add
`server/src/services/native-runtime/volume-free-controller-state.ts`.
`claimVolumeFreeControllerState(true)` writes `.volume-free-owner.json`
atomically (temporary file, fsync, rename, directory fsync) when the
state directory is absent or empty. It returns `owned` for a directory
that has the marker and `unclaimed` for a directory with other files.
`claimVolumeFreeControllerState(false)` removes the marker.
- `server/src/index.ts` calls the claim right after the volume-free
configuration check. This is before any controller or recovery can
write. A server started without volume-free mode releases the claim,
because it may write files that exist only on its disk. An `unclaimed`
result logs a warning.
- `hasLocalNativeControllerObligations` ignores the marker entries. It
returns no obligation for a claimed directory. It now delegates to
`findLocalNativeControllerObligation`, which returns the run, task and
file (or `database_checkpoint`).
- `assertEphemeralServerData` adds that detail to the
`native_controllers_require_migration` refusal. The error says which run
and file the database does not prove and why startup refuses.
- Document the marker in `doc/DEVELOPING.md` ("Runner controller state
on a restarted volume-free server").

## Verification

- Red, before the gate reads the marker: `pnpm --filter
@paperclipai/server exec vitest run
src/services/native-runtime/native-controller-migration.integration.test.ts
-t volume_free_owner` failed with `AssertionError: expected true to be
false` at the first gate check. The fixture is a running warm run whose
root holds a full `durable.control-plane-state.v2` file, the same shape
as the incident.
- Green: the same test passes. It checks: claimed directory gives no
obligation; release gives the obligation back; a non-empty unclaimed
directory stays blocked; the refusal names the run; the controller file
is never changed or removed.
- `ephemeral-server-data.test.ts`, `ephemeral-server-contract.test.ts`
and all 57 cells of `native-controller-migration.integration.test.ts`
pass. Server `tsc --noEmit` passes.
- Probe on a copy of the kept instance database and the killed server's
state directory: the unmodified gate returned an obligation for run
`4a4d7ca1` (source file
`.../7dbf2d5c.../control-plane/control-plane-state.json`).
- Chaos: the s6402 shape (`--ephemeral-server --adapters claude
--workloads W2 --faults K1,K2,K15 --k15-fresh-home --seed 6402`) runs on
this branch merged with #20930. The result is added in a comment.

## Risks

- A filesystem that a volume-free server claimed and that a pre-marker,
volume-backed server then wrote is not detected. This needs a downgrade
and a mode change on one disk. A current server in volume mode removes
the marker, which closes the normal path.
- A volume-free server on a directory written before this change (no
marker) keeps today's refusal. A new deploy makes a new container, so
staging claims the empty directory on first start after this deploy.
- Other scanners of the state directory filter on the 64-hex session
pattern, so the marker file does not change them.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, through Claude Code
with tool use (shell, file edit, test runs). Default context window,
extended reasoning on.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 17:59:57 -06:00
DottaandClaude Opus 5.5 d1e4109df1 fix(runner): keep a connected adopted Runner through a failed liveness probe (#20965)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - After a controller restart, the controller adopts the remote Runner
that kept running in the sandbox. A monitor then checks the Runner's
liveness every 250 ms through the environment plugin.
> - #20940 and #20945 made a stalled plugin probe "unknown" for the
launched Runner's monitor, reattachment and launch. The adopted Runner's
monitor still failed the run on the first probe that failed without an
answer.
> - A short plugin stall after adoption therefore failed a healthy run
with `native_adopted_runner_identity_unverifiable`.
> - This pull request uses the evidence that is available: while the
adopted Runner holds its authenticated connection to the controller, a
liveness check that fails without an answer does not fail the run, for
at most the reconnect grace.
> - The benefit is that a healthy adopted Runner survives a short plugin
stall. A check that reports the process gone still fails the run at
once.

## Linked Issues or Issue Description

Refs #20940, #20945

**What happened?**
`#watchAdoptedRunner` in
`packages/paperclip-runner/src/live/runnerd-codex-transport.ts` called
`failTransport("native_adopted_runner_identity_unverifiable: runner
liveness could not be revalidated")` on the first `isAlive()` rejection.
For a remote adopted Runner, `isAlive()` is a plugin
`environmentExecute` RPC. A plugin stall makes it reject, even while the
Runner holds its authenticated connection and works normally.

**Expected behavior**
A rejection is not an answer. While the Runner holds its authenticated
connection, the monitor keeps checking. The bound is the reconnect
grace. A check that returns `false` (the process is gone) still fails
the run at once.

**Steps to reproduce**
1. Adopt a remote Runner after a controller restart.
2. Stall the plugin worker, so that the liveness probe rejects.
3. Within 250 ms the run fails with
`native_adopted_runner_identity_unverifiable`.

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

## What Changed

- `#watchAdoptedRunner`: on an `isAlive()` rejection, the monitor
records when verification was first lost. It does not fail the transport
while the control plane has an authenticated Runner connection and less
than `runnerReconnectGraceMs` has passed. A successful check resets the
clock.
- Unchanged: an `isAlive()` result of `false` fails the run at once. A
rejection with no Runner connection fails it as before. A transport with
no reconnect grace (local adoption) fails it as before. The close path
still uses `isAlive()` directly.
- `doc/execution-semantics.md` describes the rule.

## Verification

- Red on master: `npx vitest run
src/live/runnerd-codex-transport-adoption.test.ts -t "liveness probe
rejects while"` (in `packages/paperclip-runner/`). It failed with
`native_adopted_runner_identity_unverifiable: runner liveness could not
be revalidated`.
- Green: the test passes. The real adopted runnerd keeps its connection,
`isAlive()` rejects for 1.5 seconds (6 monitor ticks), the run still
answers `thread/read`, no signal is sent, and the runner is alive.
- `runnerd-codex-transport-adoption.test.ts` passes (24 tests),
including "retains authenticated adopted-runner ownership when a close
probe rejects". `tsc --noEmit` passes for `packages/paperclip-runner`.

## Risks

- A remote adopted Runner whose liveness cannot be verified, but which
stays connected, keeps running for up to 7 minutes before the run fails
closed. Its own authenticated connection is the evidence during that
time, and no exit is reported.
- The change touches `packages/paperclip-runner/src`, so it changes the
Daytona runner image content ID.

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

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, through Claude Code
with tool use (shell, file edit, code search). Extended reasoning was
on.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 17:49:13 -06:00
DottaandClaude Opus 5.5 aaf76a3ca6 fix(native): start the configured review stage when a native worker reports done (#20882)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Tasks can have an execution policy with a review stage that names an
independent human reviewer
> - The issue route starts that stage when the assignee moves the task
to done or in_review
> - Native runs do not use that route. The native finalizer writes the
task status through the issue service, which does not run the stage
transition
> - So a native worker that reports verified work as done closes the
task without the required review. A worker that reports needs_review
moves the task to in_review without the configured stage or its reviewer
> - This pull request makes the native completion path start the
configured review stage, as the route does
> - The benefit is that a configured independent review gates Done for
native workers too

## Linked Issues or Issue Description

**What happened**

A task had an execution policy with one review stage for a human
reviewer. A native worker (OpenCode on the Paperclip runner) finished
the work and reported it. When the worker reported done with verified
evidence, the task became done and the review stage never started. When
the worker reported needs_review, the task became in_review with a
generic review card, `executionState` stayed null, and the configured
reviewer was not assigned. The chat eval `tasks.completion-with-gate`
timed out in setup because the task never reached the pending review
stage.

**Expected behavior**

Verified work on a task with a configured human review stage goes to
in_review with that stage pending, the stage reviewer assigned and the
worker kept as the return assignee. The task becomes done only after the
reviewer approves.

**Steps to reproduce**

1. Create a task assigned to a native agent with `executionPolicy.stages
= [{ type: "review", participants: [{ type: "user", userId: <reviewer>
}] }]`.
2. Let the agent finish the work and report done with verified evidence.
3. Read the task. Before this fix it is `done` and `executionState` is
null.

## What Changed

- `server/src/services/issue-execution-policy.ts`: add
`completionReviewStageEntry`. It applies the full policy transition that
the route applies for `status: "done"` and returns `starts` (with the
stage patch), `none` or `rejected`.
- `native-run-finalizer.ts`: a done decision on a task whose policy
starts a stage becomes in_review with reason
`execution_stage_review_required`. When the policy refuses the
transition, the decision is in_progress with reason
`execution_stage_transition_rejected` and a recovery note. It is never
done. Deliverable checks still run first.
- `status-decision-committer.ts`: for a stage-entry decision, apply the
stale-owner check, start the stage from the locked task in the same
status update (this also clears a monitor that the new assignee cannot
use), and wake an agent participant with `execution_review_requested` or
`execution_approval_requested`, as the route does. If the policy
changed, the saved decision is stale and finalization retries.
- `native-completion-feedback.ts`: on such a task, tell the worker to
report done so the configured stage requests the review. Reject
needs_review with no request or with any review request. Approval
requests keep their own path, because they can be governed action cards.
- New regression test `native-execution-policy-review-stage.test.ts`
(embedded Postgres, 6 tests).

## Verification

- `cd server && npx vitest run
src/services/native-runtime/native-execution-policy-review-stage.test.ts`
(6 tests): no stage (done), human stage (in_review, reviewer assigned,
monitor cleared), agent stage (in_review, reviewer woken), refused live
review cycle (in_progress, not done), and feedback including mixed and
approval requests. The stage tests fail without the finalizer change
(the task becomes done).
- Related suites pass: `historical-task-questions`,
`native-completion-feedback`, `native-run-finalizer-telemetry`,
`native-terminal-status-projection`, `status-arbiter`,
`task-monitor.integration`, `__tests__/issue-execution-policy` (247
tests).
- Server `tsc --noEmit` shows no errors in changed files. The local
worktree has unbuilt packages, so CI supplies the full typecheck.

## Risks

- The agent-reviewer wake is a system workflow wake, like the route's.
Unit tests prove the wake row. An end-to-end native agent review after
this wake is not yet exercised in a paid eval.
- A decision saved with a new reason and resumed by an older release
projects in_review or in_progress without the stage. The task does not
close, so the older release fails safe.
- Agents on tasks with a configured review stage now get a rejection
when they report needs_review with no request or with a review request.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 17:43:58 -06:00
DottaandClaude Opus 5.5 827bd21017 fix(recovery): retry lost-lease preparations instead of holding them for reconciliation (#20936)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Run recovery restarts work that stops for infrastructure reasons,
and keeps saved files safe
> - A legacy Runner run can lose its controller lease while it prepares
the workspace, before any provider work starts (`controller_lease_lost`
with `executionRecovery: { kind: "bootstrap", providerWorkStarted: false
}`)
> - Such a run has no unsaved provider state, but recovery still blocked
the task: either a saved-files hold that ends in `source_unavailable`,
or an immediate "No live execution path" block after one failed
continuation retry
> - Recovery must work without a person, and a shown Retry must work
> - This pull request lets lost-lease preparations use the existing
interrupted-preparation settlement, and lets the bounded stranded-task
sweep retry a continuation that failed before provider work
> - The benefit is that these tasks retry automatically, and tasks that
an older release already blocked recover after deploy

## Linked Issues or Issue Description

Refs #20924 (fixes the lease renewal starvation that caused the lost
leases; this PR fixes what recovery did afterwards).

**What happened?**
A staging burst of lost controller leases stopped many legacy Runner
runs while they prepared the workspace. No provider work started. Most
of the affected tasks then became blocked. One shape kept a saved-files
hold (`legacy_execution_requires_reconciliation`). Its file recovery
ended `exhausted` with `source_unavailable`, because the workspace had
already retired the prepared epoch back to the last save. The hold never
settled. The other shape was a continuation retry that also lost its
lease. The release path blocked the task at once with "No live execution
path", although `controller_lease_lost` is a transient cause with a
3-attempt budget in the stranded-task sweep.

**Expected behavior**
A run that fails before provider work starts retries automatically,
within a bounded budget. It never needs reconciliation. If the prepared
workspace is gone, the task continues from the last committed save.

**Steps to reproduce**
1. Start a legacy Runner run with a remote workspace.
2. Make the controller lose its lease while the run prepares (before
provider dispatch).
3. Let restore recovery record a saved-files hold, and let the workspace
retire the prepared epoch.
4. See that the hold stays `resolved/blocked` with `source_unavailable`,
and the task stays blocked.

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

**Agent adapter(s) involved**
Not adapter-specific (core bug). Seen with `paperclip_runner` (Daytona
sandbox).

## What Changed

- `server/src/services/cancelled-native-startup.ts`:
`isInterruptedNativePreparationCandidate` accepts a failed
`controller_lease_lost` run with the bootstrap certificate
(`providerWorkStarted: false`), the same as a certified `process_lost`
run. The error label alone still grants nothing. This lets the existing
`legacy_interrupted_preparation_hold` settlement, terminal-time proof,
status receipt, and `interrupted_preparation` retry lane apply to these
runs.
- `server/src/modules/wake-queue/*`: the release snapshot carries
`retryableBeforeProviderWork` (bootstrap certificate plus a
`transient_infra` continuation classification). When an automatic
continuation retry failed for such a cause, release returns `released`
instead of blocking. The stranded-task sweep then retries with its
existing backoff and 3-attempt cap, and blocks only after that budget is
spent. Non-retryable causes, uninvokable agents, and failures after
provider work still block at once.
- `doc/execution-semantics.md`: documents both rules.
- No migration. Holds saved by the running release are settled by the
existing ownership reconciler after deploy.

## Verification

- Red then green, in the staging shape (failed `controller_lease_lost`,
`renewal_deadline` lease-loss evidence, `restore_failed` with
`workspaceRestoreRecovery`, exhausted `verify_execution` with
`source_unavailable` / `use_alternative_strategy`, retired prepared
epoch):
- `src/__tests__/interrupted-preparation-hold-settle.test.ts`: before
the fix, both new cases fail (`settled: 0`). After the fix, the hold
settles as `false_positive`. With the status receipt, the task returns
to `in_progress` and one `interrupted_preparation` successor is queued.
For a hold saved by an older release without a receipt, the execution
gate clears and one Retry starts a run. A lost lease without the
no-provider-work certificate stays held.
- `src/modules/wake-queue/domain/policy.test.ts` and
`application/use-cases.test.ts`: before the policy change, the two new
release cases fail (the task is blocked). After it, a pre-provider
lease-loss retry failure is released to the sweep, and other failures
still block.
-
`src/modules/wake-queue/adapters/retryable-before-provider-work.test.ts`:
classification of the release snapshot fact.
- `pnpm verify` and server `tsc --noEmit` pass locally.

## Risks

- Lost-lease preparations now use the `interrupted_preparation` retry
lane. That lane is uncharged but bounded at 12 retries, and it does not
revive a chain whose failure budget is spent.
- A released continuation waits for the stranded-task sweep (30 s
reconstruction, plus backoff) instead of a direct requeue. The sweep's
budget bounds the chain.
- Settlement still needs every existing proof: the controller is gone or
settled, leases are released with stop receipts, there are no execution
events or native coordinator, and the prepared epoch is retired or the
files are repaired.

> 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, in Claude Code with tool
use (shell, file edits, browser reads of staging with GET requests
only).

## 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.1112.0-canary.0
2026-11-11 17:30:32 -06:00
DottaandClaude Opus 5.5 ba905c7254 fix(evals): name each team member's readiness and grade configure-team structurally (#20879)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The chat catalog eval `teams.configure-team` checks that chat gets
an installed team ready member by member
> - Its setup installs Product Engineering, removes the QA member's
account, and requires a real probe split: QA missing an account, the
senior coder ready
> - The QA probe returned a generic `opencode_hello_probe_failed` error,
so the setup stopped with "probes do not establish the required
missing-versus-ready account split"
> - The cause is a product classification gap: OpenRouter answers an
empty key with HTTP 401 "Missing Authentication header", which the
OpenCode probe did not treat as authentication (fixed in #20878,
cherry-picked here)
> - This pull request makes the setup blocker name each member's
observed readiness, and replaces reply keyword checks with structural
checks
> - The benefit is that the eval can reach the chat journey and grades
it without matching words

## Linked Issues or Issue Description

Refs #20878

**What happened**
`teams.configure-team` failed in setup. The QA member's probe was `fail`
with `opencode_hello_probe_failed`. The senior coder passed. The setup
error did not say which member was in which state.

**Expected behavior**
The QA member's probe reports the missing-authentication check, so the
split forms and the chat journey runs. When the split does not form, the
setup error names each member's observed status and checks.

**Steps to reproduce**
1. Run `chat-features.runner-opencode.local.teams.configure-team` on
master.
2. Read `snapshots/chat-catalog-team-initial-probes.json`.
3. QA shows `fail` with `opencode_hello_probe_failed:error`; the setup
stops.

## What Changed

- Cherry-pick of #20878: the OpenCode probe classifies a provider 401 as
`opencode_hello_probe_auth_required`.
- The setup blocker now lists each member as `name (expected missing
account | expected ready | other member): status [non-info checks]`.
- Grader: remove the member-name substring check and the "team is ready"
regex. Identification is the rendered link to the missing member's own
page, which the case already required. The followup is graded by a
non-empty reply from its own succeeded run, not by the words
"ready/configured/verified/account". Fresh per-member probes, preserved
working configuration and revisions, and the attributed repair stay
unchanged.
- Calibration: reworded and non-English valid followups pass; missing
handoff, empty followup, and failed followup run fail. A setup test with
the recorded failure shape checks the new error text.
- `oracleVersion` bumped to `paperclip.chat-catalog.team-configure.v3`;
`parse.ts` added to `sourceFiles`.

## Verification

- `pnpm exec vitest run --config tests/runner-e2e/vitest.config.ts
tests/runner-e2e/chat-catalog-domains/external-team-configure.test.ts
tests/runner-e2e/chat-catalog-domains/external-team-configure-prerequisite.test.ts
tests/runner-e2e/chat-runtime-profile.test.ts
tests/runner-e2e/chat-catalog-contract.test.ts
tests/runner-e2e/chat-catalog-consistency.test.ts` passes.
- `node tests/runner-e2e/source-sdk-public-inputs.mjs <HEAD> --check`
reports no change.
- `node --test
tests/runner-e2e/chat-full-catalog-testops-manifest.test.mjs` passes.
- The paid run
`chat-features.runner-opencode.local.teams.configure-team` is still to
run.

## Risks

- The removed "team is ready" regex failed open. A false whole-team
claim in the reply now needs human reading of the transcript. The
structural checks still reject a missing handoff, a missing repair, and
any member that fails its fresh probe.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 17:26:01 -06:00
DottaandClaude Opus 5.5 b7446969d7 fix(evals): give choose-capable-target a saveable computer and grade it structurally (#20881)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The chat catalog evals check that agents answer real user questions
through the product.
> - The case `execution-environments.choose-capable-target` asks "Which
computer can run Morgan with Claude Code?" while environment reads are
board-only.
> - Its setup saved a built-in fake sandbox as the "unsupported"
computer. The product refuses to save fake sandboxes (since #4449), so
setup failed with 422 before any agent run, and the test API hid the
reason.
> - No computer that the product can save is advertised as unable to run
Claude Code, so the original contrast cannot be built through supported
APIs.
> - This pull request changes the contrast to advertised support versus
runtime health, grades the reply by structure instead of keywords, and
makes setup failures show the product's validation reason.
> - The benefit is that the case can measure its user promise, and a
future setup 422 says why.

## Linked Issues or Issue Description

**What happened**

In the full chat catalog run, setup failed with `POST
/api/companies/<id>/environments returned 422; body withheld`. A focused
server test shows the reason: "Built-in fake sandbox environments are
reserved for internal probes and cannot be saved." The fixture payload
was `driver: "sandbox", config: { provider: "fake" }`.

**Expected behavior**

Setup builds its computers only through supported APIs, the agent run
starts, and the grader checks the user promise without reply keywords. A
setup validation failure states the product's reason.

**Steps to reproduce**

1. Run
`chat-features.runner-opencode.local.execution-environments.choose-capable-target`
on master.
2. Setup posts a fake sandbox environment and gets 422.
3. `cd server && pnpm exec vitest run
src/__tests__/environment-routes.test.ts -t "rejects persisted fake
sandbox environments"` shows the same rule.

## What Changed

- `runtime-capability-choice.ts`: setup saves an SSH computer on a
reserved `.invalid` host (no key, never probed) beside the local fixture
computer. Production capabilities advertise Claude Code on both. The
`enableEnvironments` experimental setting is still set only through
`PATCH /api/instance/settings/experimental` and restored in cleanup.
- Grader: removed the reply regex and keyword checks. New checks: agent
gets 403 on capabilities and probe; the reply renders a link to
`/<prefix>/company/settings/instance/environments` (or a target's edit
page); the reply has no board-only SSH settings (host, user, path);
targets (including status and metadata) and the worker are unchanged;
exactly one chat turn and no tasks. `verifyResult` clicks the rendered
link and checks that the page shows both computers and loads the same
capabilities.
- `execution-environments.yaml`: context changed from "one target
supports the harness and another does not" to support versus health,
with the reason. Recorded in `chat-full-catalog-testops-manifest.json`
`changedFields`, pinned in the manifest test, and the inventory digest
is updated.
- `oracleVersion` is now
`paperclip.chat-catalog.runtime-capability-choice.v2` in code and in
`chat-catalog-descriptors.json`; `sourceFiles` adds
`internal-support.ts`, `server/src/routes/authz.ts` and
`server/src/services/environment-config.ts`.
- `api.ts`: cookie-session 400/409/422 failures now include the
product's `error` message, at most 240 characters, with request string
values (8 or more characters), the session cookie and long token-like
strings redacted. `details` and all other statuses stay withheld.
- Includes a cherry-pick of #20880 (agent environment denials link the
environment settings page). Without it, the agent has no route to give.

## Verification

- `pnpm exec vitest run --config tests/runner-e2e/vitest.config.ts` on
`runtime-capability-choice.test.ts`, `runtime-computers.test.ts`,
`chat-catalog-contract.test.ts`, `chat-catalog-consistency.test.ts`,
`api-private-transport.test.ts`, `api.test.ts`,
`api-response-reading.test.ts`: 73 passed.
- Grader calibration: valid and reworded replies with the settings link
pass. Missing, unprefixed, wrong-prefix, cross-origin and wrong-page
links fail. Replies with the SSH host, user or path fail. Unsupported
capability, reachable host, 200 boundary, changed target or worker, an
extra run and a created task fail.
- `node --test
tests/runner-e2e/chat-full-catalog-testops-manifest.test.mjs`: 10
passed.
- `node tests/runner-e2e/source-sdk-public-inputs.mjs <HEAD> --check`:
unchanged.
- `npx tsc --noEmit -p tests/runner-e2e`: no errors in changed files.
- Paid run still needed:
`chat-features.runner-opencode.local.execution-environments.choose-capable-target`.

## Risks

- Premise change: the case no longer has a saved computer that is
advertised as unable to run Claude Code, because the product cannot save
one. The "avoid an incompatible target" clause stays in the expected
outcome, but no saved target exercises it.
- The reply's claims about health are not graded by words. A reviewer
must read the paid run's reply.
- The 400/409/422 reason can show product text that the test API hid
before. It is bounded and redacted.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 17:25:51 -06:00
DottaandClaude Opus 5.5 c45f54ee74 fix(native): stop or release unclosable idle warm Runners on environment delete, and say why a close failed (#20957)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs keep a warm Runner in a Daytona sandbox between turns of
a task, so the next turn starts fast
> - Each idle warm Runner has an in-memory owner in the controller.
Deleting an environment closes these owners before it destroys the
sandboxes
> - In every warm chaos wave today, some owners never closed. The server
logged only `{"failed":2}`. Environment delete returned 409 forever, and
all sandboxes of the wave stayed `started` until their 60-minute TTL
> - On staging, that is leaked sandboxes (cost) and owners that block
the work after them
> - This pull request logs and reports the exact close error, stops an
unsettled idle Runner when its environment is deleted, and releases an
owner whose sandbox is already destroyed
> - The benefit is that environment delete finishes or names the exact
run and error, and every warm chaos wave now grades a clean shutdown

## Linked Issues or Issue Description

**What happened?**
After warm Daytona chaos waves (W2, W2Q, W2C cells, `--faults K0`) on
Codex, Claude and OpenCode, the server logged `idle native session
cleanup failed before controller shutdown {"idleSessions":{"failed":2}}`
with no reason. `DELETE
/api/environments/:id?destroyReusableSandboxLeases=true` returned 409
"warm native runner did not shut down cleanly" on every retry. Eight
sandboxes per wave stayed `started`.

**Root cause.**
`closeIdleWarmNativeSessions`
(`server/src/services/native-runtime/native-session-executor.ts`)
dropped the close error (`catch {}`) and counted it. The owners that
failed are the W2Q owners. A W2Q task parks a question and is answered
by a later run on a fresh sandbox. With the reason logged, a wave shows
two causes for the same owner:

1. The owner's close cannot settle (`native_session_close_unrecoverable:
runner did not durably suspend before checkpoint`, the governed-wait
shape from #20844). Only a successor reset could force a stop
(`discardUnsettledSession`). An idle sweep or environment delete had no
forced stop.
2. The owner's sandbox lease was already destroyed by the
pending-cleanup sweep after the successor moved to a fresh sandbox
(#20863). Every close then fails with `Daytona sandbox lease <id> is no
longer active.`, because the stop RPC has no sandbox to reach. The owner
stays registered for ever.

The Runner close memoizes its rejection, so every later attempt fails at
once with the same error. This is not the "controller sequence must be
contiguous" exit (#20946): no close failure in these waves names a
sequence mismatch.

**Expected behavior**
An idle warm owner with no turn in flight never blocks environment
delete for ever. Every failed close names its run and its error.

**Steps to reproduce**
1. Run `tests/restart-chaos/main.ts --env daytona --daytona-warm
--adapters claude --workloads W2,W2Q,W2C --faults K0 --concurrency 8
--tasks 8`.
2. Read `<wave>/sandbox-sweep.json`: `environmentDeleteBlocked` is set
and the sandboxes are `started`.

## What Changed

- `closeIdleWarmNativeSessions` now returns `failures: [{ runId,
issueId, environmentId, providerLeaseId, reason }]`. The reason is the
close error with its code, its settlement record and its cause. It also
logs `idle warm native sessions did not close` with that list. The
idle-timeout close logs its error too.
- `closeWarmNativeSessionsForEnvironment` takes `forceStopUnsettled` and
`db`. The environment delete route sets both. When the graceful close
fails, Core retires the owner as #20844 does: one confirmed Launcher
`stop`, the credential snapshot, `complete`, then instruction
collection. A failed forced stop is added to the reason.
- When retirement still fails and the owner's sandbox lease has a
`destroyed` termination receipt, Core releases the owner. It does not
contact the sandbox. It logs `warm native session's sandbox was already
destroyed; released its owner` with the close error.
- The environment delete 409 now names each run and its reasons, and
`details.nativeRunnerSessions.failures` lists them.
- Idle timeout, controller restart and tenant sleep do not force a stop.
Their conversation can still be resumed, so they only log the reason.
- Chaos harness: new check `warm-shutdown.ts`, "no leaked sandboxes /
clean warm shutdown". Every warm wave (`--daytona-warm` or `--lifecycle
warm`) runs it. It fails the wave on a blocked delete, on a sandbox left
running after cleanup, or on a warm session that did not close at
environment delete or controller shutdown. It writes
`<wave>/warm-shutdown.json` and a section in `SUMMARY.md`.
- Docs: `doc/plugins/PLUGIN_RUNNER_EXECUTION.md` and the chaos README.

## Verification

- RED on master: `native-session-executor.test.ts -t "stops an idle warm
owner whose close cannot settle"` fails: `expected { closed: 0, busy: 0,
failed: 1 } to match object { closed: 0, failed: 1, failures: [...] }`
(a bare count, no reason). `environment-routes.test.ts -t "names the
warm runner session"` fails: the 409 does not name the run. On the first
fix commit, `-t "sandbox lease is gone"` fails: the lease state is never
read, and the owner stays.
- GREEN: those tests pass. The executor suite, environment routes, run
control, recovery, heartbeat process recovery, instance settings, the
idle-sleep, eviction, release-resume, restart-recovery and
warm-checkpoint integration suites pass (11 files, 2471 tests).
`tests/restart-chaos` unit test `warm-shutdown` passes. Server and
harness `tsc --noEmit` pass.
- Daytona warm waves (Claude, W2/W2Q/W2C, K0, 8 tasks). The runner image
`sha256:e04d9ce4…` matches 7e8901aa60, not current master, so each wave
ran this change cherry-picked onto 7e8901aa60:
- `w-20261111T210049-s6111` (first commit only): the close reason is now
logged: `close failed: Daytona sandbox lease 192606aa… is no longer
active.` The delete stayed 409 with that reason. The new check failed
the wave correctly.
- `w-20261111T222826-s6112` (all commits): two W2Q owners were released
with `sandbox was already destroyed`. The environment was deleted. 0
sandboxes left running. The warm-shutdown check passed. No 409 from a
warm session.
- Not verified: a wave where the graceful close fails and the forced
Launcher stop succeeds on a live sandbox (that path is covered by the
unit test). The wave's task failures (`T:first_run_succeeded`,
`R3:retry_needed`) are other open defects. They are also on master.

## Risks

- Environment delete now stops an idle Runner whose close did not
settle. The delete already has the operator's consent to destroy those
sandboxes. The workspace-export and restore-source guards on sandbox
destruction are unchanged. Instruction files are collected after the
confirmed stop, as in #20844.
- An owner is released without contact only when a lease for the same
company, environment and sandbox has a valid `destroyed` termination
receipt. A `stopped` receipt is not enough, because a stopped sandbox
can be resumed.
- `closeIdleWarmNativeSessions` results have a new `failures` field.
Callers that read only the counts are unchanged.
- No persisted state changes. Warm owners live only in controller
memory, so older saved state and mid-run deploys are not affected.
- The new harness check can fail a warm restart-fault wave that leaves a
warm session unclosed at controller shutdown. That is a real leaked
Runner.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, agentic coding with
tool use (shell, file edits, test runs, chaos waves) 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
- [ ] I will address all Greptile and reviewer comments before
requesting merge

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 17:12:21 -06:00
DottaandClaude Opus 5.5 068d814735 test(chaos): run waves in Cloud's volume-free server mode; make W2 phase 1 yield (#20930)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness (`tests/restart-chaos/`) runs real agent
tasks against an isolated control plane and injects faults
> - Cloud servers run in the volume-free mode
(`PAPERCLIP_EPHEMERAL_SERVER=true`, durable agent files, run logs in
object storage)
> - The harness never set that mode, so every wave ran a different
server mode from staging. Paths that exist only in that mode were never
tested. One of them is the per-agent directory operation lock, so
`--fail-recovery-step directory-lock` could never reach its step with
any workload
> - The re-wake workloads W2 and W2C also lost coverage: an agent that
reported phase 1 with disposition `done` closed the task, and the
harness never sent the re-wake (four of eight Claude cells on
2026-11-11)
> - This pull request adds `--ephemeral-server` and makes the W2/W2C
brief ask for a `yielded` phase-1 report
> - The benefit is that chaos waves can run in staging's server mode and
the re-wake cells test the re-wake on every provider

## Linked Issues or Issue Description

**What is the problem?**
The restart chaos harness cannot run the control plane in the
volume-free mode that Cloud and staging use. The `directory-lock`
recovery step (`agent_directory.operation_lock.before_acquire`) is
reached only for disposable remote copies, which need
`PAPERCLIP_EPHEMERAL_SERVER=true`. W2 and W2C cells also stop before the
re-wake when the agent reports phase 1 as `done`.

**What is the expected behavior?**
A wave can opt in to the volume-free mode on Daytona, and every restart
of the wave keeps that mode. The W2 and W2C cells reach the re-wake.

**Steps to reproduce**
Run `--fail-recovery-step
agent_directory.operation_lock.before_acquire:1` on Daytona with any
workload: the check `recovery_step_failed_once` reports "never reached".
Run W2 on Claude: the first run ends with the task `done` and
`rewake_sent` is "never sent".

## What Changed

- `assertIsolatedServerEnvironment` (`tests/runner-e2e/harness-env.ts`)
takes `expected.generatedSecretsMasterKey`. It admits
`PAPERCLIP_SECRETS_MASTER_KEY` only when that parameter is set and
equals the value exactly. `PAPERCLIP_SECRETS_MASTER_KEY_FILE` stays
refused. The check runs over the final server environment.
- `--k15-fresh-home` implies `--ephemeral-server` and defaults
`--storage` to `s3`. A fresh instance home without the volume-free mode
loses the agents' instruction bundles, which staging keeps in Postgres
and object storage.
- `--ephemeral-server` (`config.ts`, `types.ts`, `main.ts`,
`supervisor.ts`, new `ephemeral-server.ts` with its unit test
`ephemeral-server.test.ts`): it needs `--storage s3` and `--env daytona`
and refuses K14. Every server of the wave gets
`PAPERCLIP_EPHEMERAL_SERVER`, `PAPERCLIP_DURABLE_AGENT_FILES`,
`PAPERCLIP_SHARED_UI_ASSETS`, the wave's Postgres as
`PAPERCLIP_DATABASE_LISTEN_URL`, one stable
`PAPERCLIP_SECRETS_MASTER_KEY` (also written to the config key file) and
`RUN_LOG_S3_*` in the wave's object store.
- `rewake.ts`: the W2/W2C phase-1 brief asks for `paperclip_finish` with
disposition `yielded` and a `response_wake` continuation.
- W2/W2C tasks are created in `backlog` with their assignee, and a board
comment starts phase 1 (`startComment` in `workloads.ts`, used by
`observer.ts`). A passive board wait needs a comment-started run;
without it the yield queues an immediate continuation and phase 1 wakes
itself. The start comment is posted once (saved-comment check plus a
stable `clientRequestId`), and the brief keeps phase 2 out of blocking
`remainingWork`.
- Volume-free mode drops the repository workloads (B1, B5, F1, R1) from
the default list and refuses them when listed: their fixture is a
`local_path` project source, which a volume-free server refuses.
- `README.md`: a section for the new flag and a note on the brief.
- `shape-cells.test.ts`: option parsing and refusals for the new flag.

## Verification

- `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts
tests/restart-chaos/shape-cells.test.ts` passes (16 tests).
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos` passes.
- Live proof wave (Daytona, Codex, W2, K1 and K15 with a fresh home,
`--storage s3 --ephemeral-server`): wave `w-20261111T205259-s6301` (2
tasks). The first server and both replacements (K15 with a fresh home,
then K1) started and reached ready in the volume-free mode. The server
process had `PAPERCLIP_EPHEMERAL_SERVER=true`,
`PAPERCLIP_DURABLE_AGENT_FILES=true` and `RUN_LOG_S3_BUCKET` set. Both
W2 tasks left phase 1 `in_progress` (yielded), received the re-wake and
ended `done`. The wave also found product faults (reported separately,
not fixed here): a re-wake after K1 failed with
`runner_harness_state_mismatch: backup_without_reusable_lease` and
needed Retry.
- `pnpm exec vitest run --config tests/runner-e2e/vitest.config.ts
tests/runner-e2e/support.test.ts` passes, including the new master-key
admission test (a matching wave key passes; an inherited key, a
mismatched key and `PAPERCLIP_SECRETS_MASTER_KEY_FILE` are refused).

## Risks

- Low risk. The flag is opt-in and the harness is test-only. The brief
change alters the W2/W2C prompt, so results from older W2/W2C seeds are
not directly comparable.

## Model Used

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

## Checklist

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

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

---------

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