Commit Graph
2812 Commits
Author SHA1 Message Date
DottaandClaude Opus 5.5 ad2d123dad fix(recovery): count a current response-wake wait as a visible next action in I7 (#20994)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The ownership reconciler checks invariant I7: an agent-assigned
`in_progress` issue must have a visible next action. A live run, a
pending wake, an open recovery action or a saved monitor deadline
counts.
> - #20935 added a parked wait. When a native agent yields
`response_wake` and no response source is admitted, the arbiter commits
`response_wake_waiting_for_input`. The task stays `in_progress`, and the
run shows "Waiting for a reply".
> - I7 did not know this wait. A task that really waits for the board
was reported as `no_owner_no_wake`.
> - This pull request makes I7 count that wait as a visible next action
while it is still current. I7 still reports the issue when the wait is
stale.
> - The benefit is that a genuinely parked task is not reported as a
silent stall, and a stale wait is still reported.

## Linked Issues or Issue Description

**What happened?**
The ownership reconciler reported `I7_visible_next_action` /
`no_owner_no_wake` for an issue whose latest applied decision was
`response_wake_waiting_for_input`. That is the visible "Waiting for a
reply" state from #20935.

**Expected behavior**
A current, committed response-wake wait is a visible next action. The
issue is reported again when a user comment arrives after the waiting
run started, or when that decision is no longer the issue's latest
decision.

**Steps to reproduce**
1. Commit a native run whose arbiter decision is
`response_wake_waiting_for_input`, and make it the issue's latest
decision.
2. Wait past the 5-minute next-action grace window.
3. Run an ownership reconciliation pass. Before this change, I7 reports
the issue.

**Additional context**
Follow-up to #20989 and #20935. The conditions match
`hasCommittedNativeResponseWakeWait`, which recovery already uses for
the same wait.

Refs #20935
Refs #20989

## What Changed

- `server/src/services/recovery/ownership-invariant-report.ts`:
`checkVisibleNextAction` excludes an issue with a parked response-wake
wait. The wait must meet all of these conditions:
- The issue's `lastStatusDecisionId` is an applied
`response_wake_waiting_for_input` decision.
  - Its native finalization is `committed`.
  - Its run succeeded and belongs to the current agent assignee.
  - The issue has no user assignee and is not hidden.
  - No user comment arrived after that run started.
- New test in
`server/src/__tests__/ownership-reconciliation-report.test.ts`:
  - A current wait is not reported.
  - A later user comment makes I7 report the issue.
  - A decision that is no longer current also makes I7 report it.

## Verification

- Red on master: `cd server && npx vitest run
src/__tests__/ownership-reconciliation-report.test.ts -t "response-wake
wait"`. I7 reported the waiting issue.
- Green: the full `ownership-reconciliation-report.test.ts` and
`environment-pending-cleanup-queue-owner.test.ts` pass (43 tests). `npx
tsc --noEmit -p server` passes.
- Restart-chaos harness, for reviewers: the harness U1/R1 grading should
treat a sample as waiting, not ownerless, when the issue's latest run
shows the execution projection phase `waiting_for_answer` ("Waiting for
a reply"). It should still fail when a user comment is newer than that
run. The harness reads I7 through `tests/restart-chaos/invariants.ts`,
so I7 changes with this PR. The U1 sampler does not read the projection
yet, and this PR does not change it.

## Risks

- Low risk. The change is in a report-only check. It writes nothing and
changes no admission, status or recovery path.
- If the conditions are too wide, a stalled task is hidden. The
conditions are the same as recovery's durable-wait check, and the test
proves that a stale wait is still reported.

## 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-12 05:07:38 -06:00
DottaandClaude Opus 5.5 f30005440b fix(recovery): reconnect a live remote Runner with its admitted skills instead of re-downloading them (#21064)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agent turns run in a remote Runner sandbox. A server upgrade
detaches each running turn, and the new server must reconnect to its
Runner (restart recovery).
> - A reconnect runs the full turn preparation before it contacts the
Runner. On a new server, skill preparation downloads every saved skill
version again, under one lock for the whole company.
> - After an upgrade, many reconnects wait behind each other's
downloads. The Runner stops answering after about 2 minutes, so a late
reconnect loses the turn.
> - This pull request lets a reconnect to the same run's live remote
Runner use the exact saved skill versions, without downloading them and
without the lock.
> - The benefit is that skill preparation no longer adds to the
reconnect delay, and it does not grow with the number of reconnects.
Normal turn starts do not change.

## Linked Issues or Issue Description

**What happened?**
After an automatic staging upgrade, runs that the new server reached
late failed. The measured time from detach to reconnect was 114–152 s
for the failed runs. Turn preparation took 30–130 s, and skill
preparation alone took 46 s on one run.

**Expected behavior**
A reconnect to a live Runner reaches it quickly. Its speed must not
depend on how many other runs prepare skills at the same time.

**Steps to reproduce**
1. Run a remote Runner turn for each of many tasks in one company.
2. Upgrade the server while those turns run, so the new server starts
with an empty skill cache.
3. See that each reconnect waits in
`heartbeat.prepare.configuration_skills` for the company skill cache
lock and downloads the skills again.

**Paperclip version or commit**
master at 4228bcf89c

**Additional context**
Root cause: `withRuntimeSkillEntries` holds the company-wide skill cache
lock (a Postgres advisory lock) while it materializes each saved skill
version. A new server has an empty skill cache, so each reconnect
downloads its skills again, one run at a time. The Runner already has
these files, and the reconnect path does not read them.

## What Changed

- `company-skills.ts`: new `admittedVersionsOnly` option. It still
checks that each saved version exists and that the agent can read the
skill. It does not materialize the files or take the company skill cache
lock.
- `heartbeat/live-runner-adoption.ts`: `adoptsLiveRemoteRunner` is true
only for a `reattach_remote_runner` claim for this run, with the claim's
own lease owner, on a native run.
- `heartbeat/execution-configuration.ts`: uses the option only when all
of these are true: the reconnect gate is true, the run has its saved
native input with a runtime context, and the run has its saved skill
versions. In that case it times the phase as
`configuration_skills_adopted` and adds one lifecycle run event
(`skillPreparation: "reused_admitted"`).
- `heartbeat.ts`: passes the gate to execution configuration.
- `native-runtime/runtime-context.ts` and `native-session-executor.ts`:
on the same reconnect to a remote Runner filesystem, the Runner
controller describes the Runner's skills by their admitted digests. It
rehydrates the pinned skill files (which takes the same lock) only if it
must launch a Runner process and stage them. All other starts rehydrate
as before.
- `doc/execution-semantics.md`: describes the new behavior.

Not in this PR (by design): a separate "dispatch-only" reconnect path,
and a new startup scheduling lane. The native executor needs about 40
values from preparation (runner environment, MCP token, managed AI
credentials, the reattached execution target, run output). Connector and
instruction preparation also feed the session fingerprint. That cut is
not mechanical, so this PR removes the largest measured single cost
only.

Other `heartbeat.prepare.*` costs that stay on the reconnect path (use
post-upgrade timings to choose the next cut):
- `configuration_secrets`, `configuration_connector_assignments`,
`configuration_connection_instructions`,
`configuration_connector_skills`/`_delivery`,
`configuration_agent_identity`, `configuration_ai_runtime`
- `environment_selection`, `workspace` (retained lease resume and
realization), `session`, `task_context`, `run_identity`
- `github_environment`, `github_launchers`, `run_output`,
`runtime_services`, `instructions`, `runtime_selection` (including
`runtime_selection.skill_assets` and `runtime_selection.workspace_sync`)
- The wait from detach to "run started" (queue and controller-preparing
waits) is outside `heartbeat.prepare.*`.

## Verification

- New test `reconnects to a live remote Runner with its admitted skills
without re-downloading them or waiting on the cache`
(`server/src/__tests__/heartbeat-execution-configuration.test.ts`):
- It admits a run, deletes its skill cache (like a new server), and
holds the company cache lock.
- The reconnect preparation finishes while the lock is held. It returns
the same key, runtime name and version. The session fingerprint is
identical. The cache is not created again.
- Normal preparation of the same saved selection still waits for the
lock and materializes the files ("normal start unchanged").
- Red on master source: `A live Runner reattach must not queue behind
skill downloads: expected null not to be null`. Green with this change.
- New test in `managed-skill-content.test.ts`: with the cache lock held
and an empty cache, `prepareRuntimeContextSource` for a reconnect
returns at once with the admitted context. Its staging step and a normal
start still wait for the lock and then rehydrate the same digests.
- New `server/src/services/heartbeat/live-runner-adoption.test.ts`: the
gate is false for a normal start, a scheduled resume, a dead-Runner
resume, an incomplete bootstrap, another run's claim, a different lease
owner, and a legacy run.
- `npx vitest run
src/__tests__/heartbeat-execution-configuration.test.ts
src/__tests__/skill-version-cache.test.ts
src/services/native-runtime/native-runner-restart-recovery.integration.test.ts
src/services/heartbeat/live-runner-adoption.test.ts`: 289 passed, 14
skipped. After the controller change: restart-recovery, diskless
restart, runtime skill preservation, managed skill content, session
executor and execution configuration suites: 1686 passed, 14 skipped.
- `tsc --noEmit -p server/tsconfig.json`: pass.
- `pnpm verify` asks for broader coverage (exit 2). Required CI supplies
it.

After deploy, a reconnect run shows
`heartbeat.prepare.configuration_skills_adopted` and the
`reused_admitted` event, and no `skills.cache.acquire` or
`skills.version.materialize` phases.

## Risks

- Low. The option applies only to a same-run reconnect of a retained
remote Runner that has a saved runtime context and saved skill versions.
Every other start keeps the current path.
- Access revocation and deleted versions still fail the reconnect, as
before. A failure to download the files on this server no longer fails a
live reconnect, because the Runner already holds them.
- Nothing new is saved, so rows from older releases, two live servers,
and a deploy during a reconnect behave as before.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code with tool use (code search, editing, shell, tests).

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 03:58:01 -06:00
DottaandClaude Opus 5.5 424e2c6488 fix(recovery): keep live owners' runs through an upgrade and continue replaced preparations in the restart lane (#21046)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - An upgrade replaces the server. The old and the new server serve
together for several minutes. A legacy run proves that its owner is
alive by renewing a 60-second lease on its `heartbeat_runs` row.
> - #21019 added an uncharged `server_restart` retry lane for runs that
a restart or replacement ended. On the first upgrade that shipped it, no
run used that lane. The new server also failed many runs as
`process_lost` while the old server that owned them was still serving.
> - Each of these runs then spent one failure retry. A run that was
already the last automatic retry was held for manual reconciliation, so
the task needed a person.
> - An upgrade is routine. It must not cost a task its work or a retry,
and it must not leave a task waiting for a manual Retry.
> - This pull request sends a run that a replacement stopped during
preparation to the `server_restart` lane. It also stops the reaper from
failing a run while the server process that owns it is proven alive.
> - The benefit is that a task continues by itself through an upgrade,
also when the database is slow during the swap.

## Linked Issues or Issue Description

Refs #21019, #21042

**What happened?**
A hosted staging server was replaced by an automatic upgrade. The new
server started at 06:18:46Z, became ready at 06:26:17Z, and the old
server received SIGTERM at 06:27:34Z. Between 06:23Z and 06:26Z the new
server failed 44 runs as `process_lost` ("Process lost -- server may
have restarted; retrying once",
`processLossDiagnostic.controllerReplaced: true`). The old server still
owned these runs and was still serving. Every reaped run was a Runner
run in `executionStage: preparing` with no process recorded. Each
successor was scheduled with `scheduledRetryReason: transient_failure`
and `executionRetryAccounting.failureRetries: 1`. No run on the server
has `retryReason: server_restart` or
`executionRetryAccounting.serverRestarts`.

The run evidence also shows why the leases lapsed. The old server's
lease renewals waited 19 to 50 seconds on the database
(`controllerLeaseLoss.renewalPendingMs`) while its timers fired on time
(`deadlineLateMs` 0 to 17 ms). #21042 fixes that renewal stall. This
pull request fixes how the reaper and the retry scheduler treat these
runs.

**Expected behavior**
- A server never fails a run as lost while the server process that owns
it is alive and serving.
- A run that a replacement really ended continues once, automatically,
without spending the failure budget.

**Steps to reproduce**
1. Seed a legacy `paperclip_runner` run in `executionStage: preparing`,
owned by another server boot, with a lapsed run lease.
2. Run `reapOrphanedRuns()` on the new server.
3. See that the successor is `transient_failure` with `failureRetries:
1`. When the chain already spent its failure budget, see that no
successor is created and the run is held for reconciliation.
4. Give the owner a live controller instance lease (the old server is
still serving) and run `reapOrphanedRuns()` again: the run is failed as
`process_lost` anyway.

**Root cause**
- `usesServerRestartLane` (`server/src/services/heartbeat/retries.ts`)
left every run with the bootstrap evidence `providerWorkStarted: false`
to the `interrupted_preparation` lane. But `scheduleBoundedRetryForRun`
gives that lane only to an `interrupted` run (or a native preparation).
The reaper writes the same evidence on a `failed`/`process_lost` run
that it stops during preparation. That run fell through to the charged
`transient_failure` lane. The #21019 tests used runs in `dispatching`,
so they did not catch it.
- `legacyExecutionNeedsReconciliation` held every run whose failure
budget was spent before it checked the bootstrap evidence. A run that a
replacement stopped before dispatch did no provider work, but it was
held for reconciliation, and the task waited for a person.
- The orphan reaper (`recoverActiveExecutions`,
`server/src/services/heartbeat/recovery.ts`) treated a lapsed run lease
as proof that the owner was gone. It checked the shared owner prover
only for "gone", never for "alive". During the swap the old server's
renewals were slow, so its leases lapsed while it was alive.

**Paperclip version or commit**
Staging Core `51d77648bc` (first build with #21019). The behavior is the
same on master `148d2b629c`.

## What Changed

- `retries.ts`: `usesServerRestartLane(run, preparationLaneTakesRun?)`
keeps the `interrupted_preparation` exception only when that lane really
takes the run. `scheduleBoundedRetryForRun` passes its own verdict.
Without one, only an `interrupted` run qualifies. A `process_lost` run
that a replacement stopped during preparation now continues in the
`server_restart` lane.
- `legacy-execution-recovery.ts`: a replaced preparation
(`isReplacedPreparation`) does not need reconciliation, also when its
failure budget is spent. The `server_restart` bound limits it.
- `recovery.ts` (orphan reaper):
- When a run's lease lapsed but the shared owner prover proves its owner
process alive (for example, the owner's controller instance lease is
live), the reaper leaves the run with that owner. The owner stops and
settles the run itself (`controller_lease_lost`, uncharged
`interrupted_preparation` lane). The reaper recovers the run once the
owner is gone or stopped. It also recovers the run when the owner has
not settled it within `LEGACY_LIVE_OWNER_SETTLE_GRACE_MS` (5 minutes)
after the lease lapsed.
- A setup that a replacement stopped before dispatch uses the
`server_restart` bound for its retry decision, whatever the adapter.
- `legacy-controller-lease.ts`: add `proveLegacyController(run)`, which
returns the shared prover's verdict. `legacyControllerProvenGone` uses
it, and the reaper computes the proof once per run.
- `server-replacement-interruption.ts`: add
`isReplacedPreparation(run)`: bootstrap evidence `providerWorkStarted:
false`, a recorded controller process stamp, and a server-replacement
interruption. A row saved by an older release without the stamp keeps
the charged lane (the existing documented rule).
- `legacy-execution-recovery.ts` (`settleInterruptedNativeBootstrap`): a
reconciliation hold that an older release saved on a replaced
preparation is settled once its owner is proven gone, even when the
failure budget is spent. The run continues in the `server_restart` lane.
- `legacy-controller-lease.ts`, `recovery/ownership-registry.ts`: the
`legacy_interrupted_preparation_hold` kind judges a finished run's hold
by its recorded controller process
(`finishedLegacyControllerOwnerStamp`). Revocation by the orphan reaper
swaps in a random boot id, so before this change a reaped preparation's
hold had owner "none" and was never settled.
- `recovery/service.ts`, `heartbeat.ts`: the stranded-task sweep reports
exhaustion from the scheduler's own `retry_exhausted` result, so a spent
`interrupted_preparation` lane blocks the task even when the synchronous
budget check reads an unspent restart bound.
- `doc/execution-semantics.md`: document these rules.
- No schema change. Nothing changes on the path that starts a run or a
turn (no new waits and no file work).

## 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"):
- "continues a first attempt / a run that was already the last automatic
retry that a replaced server left preparing, in the restart lane". This
is the production shape: a `paperclip_runner` run in `preparing`, two
servers sweep at the same time. Master: `transient_failure` with
`failureRetries: 1`, and no successor for the spent chain. Now: one
`server_restart` continuation, due at once, with the failure count
unchanged.
- "leaves a preparing/dispatching run whose lease lapsed with its owner,
then recovers it once the owner stops". The owner's controller instance
lease is live and two servers sweep. Master: `process_lost` at once.
Now: the run stays `running` with its owner. After the owner stops, it
becomes `process_lost` and a `server_restart` continuation is scheduled.
- "recovers the run when its live owner has not settled it within the
grace".
- `interrupted-preparation-hold-settle.test.ts`: "releases an older
release's hold on a preparation a server replacement stopped with its
failure budget spent" for an unrevoked and a reaper-revoked lease (red
without the settlement and owner-stamp changes) and "blocks the task
when a replaced preparation's lane is spent even though the restart
bound is not" (red without the sweep change).
- Related suites: heartbeat process recovery, retry scheduling, orphaned
active lease sweep, recovery deadline (bootstrap and ordinary
bootstrap), legacy continuation authority, undispatched execution wait,
agent status recovery, legacy workspace restore recovery, legacy
execution recovery, legacy controller lease, server replacement
interruption, heartbeat recovery, provider failure classification,
reconciliation run read, and wake-queue policy.
- `tsc --noEmit` for `server`.

## Risks

- A run whose owner is alive but no longer drives it now stays `running`
for up to 5 minutes after its lease lapsed, and then the reaper recovers
it. The owner's own sweep still recovers a run that the owner itself
lost the handle for (this boot is never deferred).
- The guard trusts the shared owner prover. That prover reads "alive"
only from a live or grace controller instance lease, or from a live PID
in the same PID space. A server whose instance lease cannot renew for 60
seconds stops itself, so a stuck owner cannot hold runs forever.
- More runs continue in the uncharged lanes. The `server_restart` bound
(6 in 60 minutes) still stops a loop, and the stop is shown on the task.
- Rows saved by older releases: a `process_lost` row with the bootstrap
evidence and an old-boot owner now continues in the `server_restart`
lane instead of being held. These runs did no provider work.

> 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, with tool use (code
search, shell, test execution, read-only log and API inspection) 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-12 02:18:39 -06:00
DottaandClaude Opus 5.5 4e519bd428 fix(chat): send writes with an empty responseText and point card declines to resolve-from-comment (#21050)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Chat agents call the Paperclip API through `call_api` and find
routes with `search_api`
> - #21035 added a pre-send refusal for writes that carry
`responseText`, and new summaries for the question cancel and withdraw
routes
> - Full chat cohort 5 (OpenCode with DeepSeek v4.1 flash) showed that
both changes went too far
> - The refusal blocked a valid write that had an empty `responseText`,
and the summaries hid the route that records a person's decline
> - This pull request narrows the refusal and corrects the guidance text
> - The benefit is that valid writes go through on the first try, and
agents settle a declined card instead of leaving it pending

## Linked Issues or Issue Description

Refs #21035, #21002

**What happened?**
- inbox-search.restore-archive: DeepSeek sent `responseText: {}` with a
valid body on `POST /api/issues/{id}/inbox-archive`. The refusal from
#21035 rejected 24 POSTs in 3 minutes. The model did not drop the key,
and the run was cancelled at the 240s window. In cohort 4 the same POSTs
went through on the first try.
- questions-and-approvals.reject-and-revise: the person said "Do not
publish it; just make a draft I can review." The CEO quoted the cancel
summary ("an agent withdraws its own pending card with POST
.../withdraw"), saw that the person created the publish card, and left
the card pending. The plan decision did not settle. In cohorts 3 and 4
the CEO declined the card with `resolve-from-comment` (resolver policy
`anyone`).

**Expected behavior**
- A write with an empty or default `responseText` is sent once. A write
whose `responseText` sets a window is still refused before it is sent.
- The guidance says that withdraw is only for the agent's own obsolete
card, and that a person's explicit decline or answer goes through
`resolve-from-comment` when the card's resolver policy allows.

**Steps to reproduce**
Run the two chat cases above with OpenCode and DeepSeek v4.1 flash on
master.

## What Changed

- `call_api` (`assertRunnerApiDispatchable`): the check reads the raw
`responseText` value. `{}` or `{ offsetBytes: 0 }` on a write is dropped
and the write is sent. A `responseText` with `limitBytes` or a later
offset is still refused. `saveResponse: false` on a write was already
accepted; a test now covers it.
- OpenAPI: the cancel and withdraw summaries say that withdraw is only
for the agent's own obsolete card, and that a decline or answer on the
person's explicit instruction uses `POST .../resolve-from-comment` when
the resolver policy allows. The `resolve-from-comment` summary says it
also records a decline of a card the person created.
- `AGENT_CHAT_DIRECTIVE`: a person's explicit decline in chat answers a
pending confirmation with reject, whoever created the card. Withdraw
never records the person's decision.
- `doc/runner-api-tools.md`: describes the narrowed refusal.

## Verification

- `server/src/services/native-runtime/runner-api.test.ts`: new test
sends `POST /api/issues/{id}/inbox-archive` with a valid body and
`responseText: {}`, `{ offsetBytes: 0 }`, `saveResponse: false`, or
both. The request is sent exactly once. Red before the fix (3 of 4 cases
refused), green after. The refusal test for a `responseText` with
`limitBytes` stays.
- `server/src/__tests__/openapi-routes.test.ts`: new test for the
cancel, withdraw and resolve-from-comment summaries. Red before, green
after.
- `server/src/__tests__/agent-conversations.test.ts`: new directive test
for the decline rule. Red before, green after.
- Also passed: `runner-api.integration.test.ts`,
`native-case-tools.integration.test.ts`, server `tsc --noEmit`. SDK
public input pins: `--check` reports no change.
- Paid run (OpenCode + DeepSeek v4.1 flash) on this branch for
inbox-search.restore-archive, questions-and-approvals.reject-and-revise,
gateways-profiles.rotate-token and budgets-costs.repair-cost-recording:
4 of 4 cells passed on 3f20fb2bcc (run 43177772183); details in a PR
comment.

## Risks

- A write with a populated `responseText` is still refused, as in
#21035. Only the empty and default shapes change. A dropped empty
`responseText` changes nothing, because the server already removed it
from writes before sending.
- The directive grows by two sentences, which adds a small number of
prompt tokens per chat turn.

## 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
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 02:09:14 -06:00
DottaandClaude Opus 5.5 3c058c37e6 fix(retry): continue a connected task from saved work when its exact chat request cannot be repeated (#21038)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Tasks that come from a connected chat (for example GitHub pull
request reviews) show a Retry button when a run fails
> - The task projection offers Retry, but the Retry route sends a
connected task to the chat retry owner, which only replays the exact
failed chat request
> - When that exact request cannot be replayed (the attempt already
saved a result, its output was published, the latest run did not start
from a chat message, or the requester's access changed), the route
returns 409 and tells the person to check the connection by hand
> - A Retry button that is shown must work, and recovery must not ask
the person to inspect anything
> - This pull request makes a person's Retry continue the task from its
saved work when the exact chat replay is refused, the same way a new
message on the task would
> - The benefit is that the projection and the route agree, and blocked
connected tasks become retryable after deploy with no migration

## Linked Issues or Issue Description

No public issue exists. This is the bug description, in the fields of
`.github/ISSUE_TEMPLATE/bug_report.yml`.

**What happened?**

On a hosted deploy, five blocked tasks that came from a connected GitHub
conversation showed Retry (`interruption.canRetry: true`). Each click on
Retry returned 409 `chat_failed_run_retry_not_authorized`. The message
was "The exact failed chat request can no longer be retried safely.
Check its current connection, access, and recovery state." or "This
attempt already recorded a result. Send a new message on the task to
continue from the saved work." The latest runs were legacy sandbox setup
failures, native runs that needed reconciliation, a run that started
from an issue monitor instead of a chat message, and a native run that
already saved its result. Root cause: `admitTaskRetry` in
`server/src/services/task-retry.ts` called the chat retry owner for
every connected task and passed its exact-replay refusal straight to the
person. The projection cannot predict that refusal.

**Expected behavior**

A shown Retry works. If the exact chat request cannot be repeated, Retry
continues the task from its saved work, the same way a new message on
the task would. Users do not get operator instructions.

**Steps to reproduce**

1. Bind a task to a restricted chat conversation.
2. Make its latest run fail so that the chat retry owner refuses the
exact replay. For example, use a run whose source is not a chat message,
or a native run with an accepted result.
3. Click Retry (`POST /api/issues/:id/retry`).
4. The route returns 409 `chat_failed_run_retry_not_authorized`.

**Paperclip version or commit**

`master` at 5a7378af5 (the deployed revision), reproduced on 51d77648bc.

**Deployment mode**

Hosted (cloud) instance with Daytona remote sandboxes and a GitHub chat
connection.

**Agent adapter(s) involved**

Not adapter-specific (core bug). Seen with `paperclip_runner` legacy and
native runs.

## What Changed

- `server/src/services/task-retry.ts`: a person's Retry runs the exact
chat replay inside a savepoint. On a
`chat_failed_run_retry_not_authorized` refusal, the savepoint rolls back
and the same Retry receipt stays an ordinary board continuation. The
clicking person is the responsible user, the run starts a fresh session
(`forceFreshSession`) from saved work, and no chat action, chat
authority, or output replay is created. Retry that Paperclip requests by
itself (`initiatedBy: restart_recovery | provider_limit_reset`) keeps
the exact-replay requirement. Other errors (403, lock contention,
database errors) still propagate.
- `server/src/services/chat-channels/failed-run-retry.ts`: plain refusal
wording. Runs saved by an earlier release with the old diagnostic are
still recognized by the pre-provider check.
- Tests: new red/green cases in `server/src/services/task-retry.test.ts`
(four refusal reasons on a chat task whose latest run came from an issue
monitor, plus automatic Retry still refused). Updated two real-service
route cases in `server/src/__tests__/chat-channels.integration.test.ts`
(accepted native result on an inherited GitHub task, and an unknown chat
source) from expecting 409 to expecting a board continuation without a
chat replay.
- A person's Retry on that person's own saved board turn (`source:
issue.comment.reopen`) that cannot be certified for replay also
continues from saved work, without replaying the saved message.
Automatic Retry still gets 409 `saved_board_message_not_replayable`.
- A person's Retry when no chat retry owner is wired also continues from
saved work. Automatic Retry still gets 409.
- The fallback is not used while the conversation holds saved or
unadmitted chat input for the task. That input keeps its own admission
path and the existing refusal.
- Docs: `doc/execution-semantics.md` and `doc/connections/GITHUB.md`.

### 409 paths in the task Retry route after this change

| Path | Who sees it | Result |
| --- | --- | --- |
| `chat_failed_run_retry_not_authorized` from the chat retry owner |
Person | Continues from saved work (board continuation) |
| `saved_board_message_not_replayable` | Person | Continues from saved
work |
| No chat retry owner wired | Person | Continues from saved work |
| Any of the three above while saved or unadmitted chat input is pending
| Person | 409. The pending input continues through its own admission
and delivery. |
| Any of the three above | Automatic Retry (restart recovery, provider
limit reset) | 409, recorded by the hook. No button. |
| `task_retry_busy` (chat authorization lock held) | Person | 409 with
`retryable: true` after 10 internal attempts. A new click works. |
| Idempotency key reused for a different task or user | Client error |
409. The UI makes a new key for each click. |

## Verification

- Red before the fix: `npx vitest run src/services/task-retry.test.ts -t
"connected task"` failed 4/4 with `Error: refused` (409). The board-turn
and missing-owner cases failed with "This saved message cannot be
retried safely..." and "Retry is unavailable for this connected
conversation." `src/__tests__/chat-channels.integration.test.ts -t
"continues an inherited GitHub task"` failed with `expected 202
"Accepted", got 409 "Conflict"`.
- Green after the fix: `src/services/task-retry.test.ts`,
`src/__tests__/durable-chat-wakeup.test.ts`,
`src/services/budget-resume-continuation.test.ts`: 160/160 passed.
- Full `src/__tests__/chat-channels.integration.test.ts`: 1949/1951
passed. The 2 local failures (`binds Slack tools...`, `materializes
direct external-chat finals...`) read `.claude/skills/slack/SKILL.md`
from the parent checkout of a nested local worktree (ENOENT). They do
not touch Retry and depend on the local checkout layout.
- `npx tsc --noEmit -p server/tsconfig.json`: passed.
- `pnpm verify` exited 2 (a doc file is a global CI input), so required
CI supplies broad coverage.
- Staging was inspected with GET requests only. No POST was sent to
staging.

## Risks

- Behavior change: a person's Retry on a connected task can now start a
board continuation where it was refused before. It uses the same
authority as a new message from that person, and dispatch keeps all
normal gates (task access, executor containment, recovery holds,
unadmitted chat input).
- The continuation does not publish the earlier result again and does
not reuse the chat requester's authority.
- No migration. Existing blocked tasks become retryable at the next
click.

> 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, 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-12 01:27:54 -06:00
DottaandClaude Opus 5.5 0dfbbdeb31 fix(chat): route company creation, decline retries and question cancellation correctly (#21035)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Chat agents follow the chat directive and discover API routes with
search_api and call_api
> - Full chat cohort 4 (OpenCode with DeepSeek v4.1 flash) showed four
guidance gaps
> - The missing-name rule overrode the board-only company-creation
boundary; a repeated request after a decline did not post a new approval
card; search_api showed the board-only question cancel route as open to
agents (and withdraw as board-only); write calls carried `responseText`
beside or instead of `body`
> - This pull request fixes the guidance, the route authorization
metadata and the pre-send refusal
> - The benefit is that agents take the supported path the first time
instead of asking for the wrong input, refusing a valid retry, or
calling a route that returns 403

## Linked Issues or Issue Description

**What happened?**
- onboarding.new-company: asked to "set up a new company for my website
business", the CEO cited the missing-name rule (#21007) and asked for a
business name with a question card. Creating a company is a board
action, so the agent should send the person to the creation flow.
- app-permissions.ask-before-action: the person declined a write card
and then repeated the same request. The CEO asked the person to tell it
to resubmit instead of posting a new approval card. The gateway already
allows a fresh request after a new human message
(`isNewHumanIntentAfterToolRejection`), but the decline text said "Do
not retry the same call".
- questions-and-approvals.multi-part-question: search_api listed `POST
/api/issues/{id}/interactions/{interactionId}/cancel` as
`board_or_agent`. The route rejects agents. The agent route,
`/withdraw`, was listed as board-only although its route admits the
card's creator agent and the task's assignee.
- Several cases: write calls through call_api carried `responseText`
with no body (for example "Received arguments: operationId, pathParams,
responseText").

**Expected behavior**
The agent links the company-creation flow; it posts a fresh approval
card for a repeated request; search_api shows the true authorization of
cancel and withdraw; a write with `responseText` is refused before it is
sent, with the same guidance #21002 gave `saveResponse`.

**Steps to reproduce**
Run the four chat cases above with OpenCode and DeepSeek v4.1 flash on
master.

Refs #21002, #21007

## What Changed

- `AGENT_CHAT_DIRECTIVE`: the missing-name rule applies to work the
agent creates itself and never comes before a board-only boundary. For a
new company the agent links [Create organization](/onboarding) (no
company prefix) and repeats the business details. A repeated request
after a declined approval card is an explicit retry: make the same call
so a fresh card is posted. A clarification question card puts candidate
links in its description, because labels show as plain text.
- Decline instructions in `tool-action-delivery.ts` and
`tool-gateway.ts` say that a later request from the person for the same
action is an explicit retry.
- OpenAPI: `POST /api/issues/{id}/interactions/{interactionId}/cancel`
is board-only and its summary names the `/withdraw` route for agents.
`/withdraw` is no longer marked board-only, which matches its route.
- `call_api`: a new write that carries `responseText` is refused before
it is sent; the message says `responseText` never carries request fields
and names the allowed body fields. Older saved calls still validate for
recovery. `doc/runner-api-tools.md` is updated.

## Verification

- `server/src/__tests__/agent-conversations.test.ts`: new directive
tests for the board-only boundary order, the decline retry and card
links.
- `server/src/__tests__/openapi-routes.test.ts`: cancel is `board`,
withdraw is `board_or_agent`, the cancel summary names withdraw.
- `server/src/services/native-runtime/runner-api.test.ts`: a write with
`responseText` is refused before sending (three window shapes), the same
write without it is sent once, and saved calls still validate.
- `tool-gateway.test.ts`, `tool-gateway-service.test.ts`,
`issue-thread-interaction-routes.test.ts`,
`tool-action-replay-intent.test.ts`: passed. `server` `tsc --noEmit`:
passed.
- Paid run (8 cells, OpenCode + DeepSeek v4.1 flash) together with the
eval PR: see the PR comment for results.

## Risks

- A model that used to send a write with `responseText` and a valid body
now gets one refusal and must resend without it. Nothing is sent before
the refusal, so there is no duplicate side effect.
- The directive grows by a few sentences, which adds a small number of
prompt tokens per chat turn.
- Agents that read `/withdraw` as board-only before now see that they
can withdraw their own card; the route already enforced who may.

## 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
- [ ] 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-12 00:27:35 -06:00
DottaandClaude Opus 5.5 940d681974 fix(heartbeat): hold a queued native run while its task's previous session finishes cleanup (#21031)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runner runs keep a provider session per task. When a run
stops, its session close continues in the server process
> - The runner refuses every new session of the task until that close
settles. A close that cannot prove the old runner suspended needs a
stopped-process proof before it is released
> - A reassignment stops the old assignee's run mid-turn. The new
assignee's run was dispatched one second later into that gate, and it
failed with `native_session_cleanup_quarantined`
> - Only a Retry continued the task. Recovery must work without a
person, and the needed-retry rate must be 0%
> - This pull request keeps such a run queued until the close settles or
its old owner is proven stopped
> - The benefit is that the new run starts by itself after cleanup,
without a failed run or a Retry

## Linked Issues or Issue Description

**What happened?**
In the paid chat eval `tasks.edit-properties` (and
`tasks.assignment-conflict` in an earlier cohort), the CEO reassigned a
task while the old assignee's native run was mid-turn, with a Paperclip
tool call in flight. The reassignment cancelled that run. Its session
close then failed with `native_session_close_unrecoverable: runner did
not durably suspend before checkpoint`, and the runner kept an
operator-required cleanup gate for the task. The new assignee's run had
started 1 second after the cancel. The runner's admission waited for the
in-flight close. When the close escalated, admission refused the run
with `native_session_cleanup_quarantined`. No run was queued again.

**Steps to reproduce**
1. Run a native task and stop its run while its session close cannot
settle at once.
2. Queue a run of another agent for the same task.
3. The run is dispatched into the cleanup gate and fails before it
launches.

**Expected behavior**
The run waits for the cleanup, and starts once the close settles or the
old owner is proven stopped. It never fails, and no Retry is needed.

## What Changed

- `server/src/services/native-runtime/native-cleanup-admission-hold.ts`:
`nativeCleanupHoldsAdmission` reads the task's cleanup gate. For an
operator-required gate it first tries
`retireStoppedSessionCleanupGates`, which retires the gate when the old
owner's runner and provider processes are proven stopped. This is the
same proof that Retry uses.
- `server/src/services/heartbeat/queue.ts`: `claimQueuedRun` keeps an
unstarted `paperclip_runner` run queued while its task has a settling or
unproven close. This matches the existing
`predecessor_settling_in_process` hold, which covers only the same
agent. A reassignment changes the agent.
- `server/src/services/native-runtime/native-cleanup-release-wake.ts`: a
released close now also wakes queued-run dispatch. A close that
escalates to operator-required also wakes it, so admission tries the
stopped-process proof at once. The queued-run liveness redrive covers
any later proof.
-
`server/src/__tests__/heartbeat-native-cleanup-admission-hold.test.ts`:
an assembled heartbeat test. It is red on master (the run is claimed
into a pending close and into an operator-required close). It is green
here (the run stays queued, then is admitted after the close settles). A
control case shows that a run for another task is not held.

## Verification

- `cd server && npx vitest run
src/__tests__/heartbeat-native-cleanup-admission-hold.test.ts` (3
passed; 2 red on master)
- `cd server && npx vitest run
src/services/quarantined-task-retry.test.ts
src/services/heartbeat/queue.test.ts
src/__tests__/heartbeat-workspace-busy.test.ts
src/services/queued-run-dispatch-liveness.test.ts` (180 passed)
- `cd server && npx tsc --noEmit` passes.
- Paid E2E run on a combined branch:
https://github.com/paperclipai/paperclip/actions/runs/43161593494
(results in a comment below).

## Risks

- A run whose task has an operator-required gate that is never proven
stopped now stays queued instead of failing. Before, that run failed,
and Retry was blocked by the same proof. The liveness redrive re-checks
it with back-off.
- The hold applies only to unstarted `paperclip_runner` runs of the
gated task. Other tasks and agents are not affected.
- Previous-release saved state: no persisted shape changes. The gate
lives in process memory, and runs saved by the previous release are
claimed by the same rule.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 23:53:54 -06:00
DottaandClaude Opus 5.5 ebaffddb01 fix(issues): release the outgoing assignee's never-started run on reassignment (#21030)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A lead agent creates tasks and assigns them to other agents
> - A task created without an assignee goes to its creator, with a
queued run that has not started yet
> - When the lead then reassigns the task, the route stopped only a
running run. The queued run kept the task's execution lock
> - The new assignee's wake waited behind that lock, so nothing ran
until the queue reached and withheld the old run
> - This pull request releases the outgoing assignee's never-started run
on reassignment and wakes the new assignee at once
> - The benefit is that reassignment starts the new owner's work
immediately, without a stale lock or a duplicate task

## Linked Issues or Issue Description

**What happened?**
In the paid chat eval `tasks.attachment-brief`, the CEO created a task
with no assignee, so it went to the CEO with a queued run. The CEO then
reassigned it to Morgan. The CEO's queued run kept the execution lock.
Morgan's assignment wake was deferred behind it. After 86 seconds the
queue cancelled the stale run as `issue_assignee_changed`. Before that,
the CEO got a 403 when it tried to cancel the run, and it created a
second copy of the task.

**Steps to reproduce**
1. Assign a task to an agent that is busy, so the task's assignment run
stays queued.
2. Reassign the task to another agent.
3. The queued run keeps the execution lock, and the new assignee's wake
is deferred.

**Expected behavior**
Reassignment cancels the outgoing assignee's never-started run for that
task and wakes the new assignee at once.

## What Changed

- `server/src/routes/issues.ts`: on reassignment, when no running run
holds the task, also stop the outgoing assignee's queued run that holds
the task's execution lock. The stop uses the existing `issue_reassigned`
code and the existing stop settlement.
- Reassignment stops pass `suppressImmediateRecovery: true`. Without it,
releasing the lock queued an `issue_assignment_recovery` run for the
outgoing assignee, which took the lock back before the reassignment was
saved. The new assignee's wake is the successor.
- `server/src/__tests__/issue-reassignment-queued-run.test.ts`: a
database-backed route test. It is red on master (the queued run stays
queued and holds the lock, and the new assignee's wake is
`deferred_issue_execution`). It is green here (the run is cancelled as
`issue_reassigned`, and the new assignee has an admitted run for the
task). A control case shows that a queued lock holder that is not the
outgoing assignee's run is not touched.

## Verification

- `cd server && npx vitest run
src/__tests__/issue-reassignment-queued-run.test.ts` (2 passed; red on
master)
- `cd server && npx vitest run
src/__tests__/issue-agent-mutation-ownership-routes.test.ts
src/__tests__/issue-queued-comments-routes.test.ts
src/__tests__/human-directed-work.test.ts` (378 passed)
- `cd server && npx tsc --noEmit` passes.
- Paid E2E run on a combined branch:
https://github.com/paperclipai/paperclip/actions/runs/43161593494
(results in a comment below).

## Risks

- Behavior change: a reassignment now cancels the outgoing assignee's
queued run that holds the task. Before, that run was cancelled later at
claim time with `issue_assignee_changed`. The eval and the run log
already recognize both codes.
- Reassignment stops no longer queue an immediate recovery run for the
outgoing assignee. If the reassignment write then fails, the periodic
recovery sweep still recovers the task.
- No schema or saved-state change.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 23:53:49 -06:00
DottaandClaude Opus 5.5 a372271a40 fix(wake-queue): keep a task done for its delegator's comment posted during the closing run (#21032)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A lead agent delegates a task, and the assignee works on it in a run
> - When someone comments while that run is active, the comment waits as
a deferred wake for the next run
> - If the run marks the task done, an agent comment with resume intent
reopened the task. This started another assignee run and more lead wakes
> - When the comment came from the lead that delegated the task, during
the run that closed it, the lead gets the completed result anyway
> - This pull request keeps the task done in that case. Other agents'
feedback and any person's message still reopen it
> - The benefit is no repeated work after a task is finished, and no
lost person messages

## Linked Issues or Issue Description

**What happened?**
In the paid chat eval `secrets.least-scope-delivery`, the CEO created
Morgan's task. While Morgan's run was active, the CEO posted a redundant
instruction comment on it. Morgan's run completed the task. The deferred
agent comment then reopened the done task. This caused a second Morgan
run and extra CEO wakes, and the eval's run bound was exceeded. #20996
covers a person's owed answers. This change covers agent comments with
resume intent.

**Steps to reproduce**
1. Let agent A create a task assigned to agent B.
2. While B's run is active, let A comment on the task with resume
intent.
3. Let B's run mark the task done.
4. The deferred comment reopens the task, and B runs again.

**Expected behavior**
The task stays done. A receives the completed result and can follow up.
A person's message still reopens the task.

## What Changed

- `server/src/modules/wake-queue/application/use-cases.ts`: the "agent
feedback after completion" reopen rule now also requires that the
comments are not watcher comments posted during the finishing run.
Person messages, `issue_reopened_via_comment`, and other agents'
feedback do not change.
- `server/src/modules/wake-queue/application/ports.ts` and
`adapters/postgres.ts`: `isWatcherCommentDuringFinishingRun` is true
only when every comment exists, has an agent author and no person
author, comes from the task's creator agent or its parent's assignee
(not the finishing agent), and was created after the finishing run
started.
- When this is true, the deferred wake is cancelled with the existing
reason "Deferred execution wake no longer applies to a terminal task".
The comment stays on the task.
- `server/src/__tests__/heartbeat-comment-wake-batching.test.ts`: a new
heartbeat case. It is red on master (the task reopens and the assignee
runs again) and green here (the task stays done, one run). The existing
case, where a non-delegating agent's feedback reopens the task, still
passes.
- `use-cases.test.ts`: a unit scenario for the new rule.

## Verification

- `cd server && npx vitest run
src/__tests__/heartbeat-comment-wake-batching.test.ts
src/modules/wake-queue` (369 passed; the new case is red on master)
- `cd server && npx tsc --noEmit` passes.
- Paid E2E run on a combined branch:
https://github.com/paperclipai/paperclip/actions/runs/43161593494
(results in a comment below).

## Risks

- Behavior change: if a delegating agent asks for new work in a comment
during the closing run, the assignee does not get it automatically. The
delegator receives the completed task and can comment again or create
follow-up work. A person's message is never dropped by this rule.
- Comments with no author type (older rows) count as agent comments only
when they have an agent author and no person author.
- No schema change.

## Model Used

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

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 23:53:43 -06:00
Nicky Leach d68088465c Reuse retained Runner descriptions during startup (#21004) 2026-11-11 21:20:08 -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 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>
2026-11-11 22:04:58 -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 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 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 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>
2026-11-11 20:33:48 -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 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 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 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>
2026-11-11 17:30:32 -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 9c56e5465c fix(native): keep the agent's own unanswered question open after a turn ends without a report (#20934)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - An agent can ask the person a question and wait for the answer.
Native task status comes from the server's status arbiter, not from
model prose
> - When a native turn ends without `paperclip_finish`, the Runner sends
a default report: `done`, objective satisfied, with the agent's final
message as the only evidence
> - A later comment from the person moves an open question into history
(#15392), so the question no longer gates completion
> - Together these close a task as `done` while the agent is still
waiting for its own answer and has produced nothing. No Retry appears
and nothing flags it
> - This pull request keeps such a task `in_review` on the agent's own
unanswered question when the only completion evidence is the agent's
final message
> - The benefit is that a "waiting for your answer" turn can no longer
become a silent false completion

## Linked Issues or Issue Description

Refs #15392

**What happened?**
Restart-chaos wave on master 7e8901aa60, OpenCode task t06 (W2Q), no
fault injected. Run fec9530f asks a question (interaction c2d3b3f2); the
task goes `in_review`. A board comment says "include NOTE-238ef87a in
the brief; do not write the brief yet; I will answer shortly". Run
f6cbefa8 replies "I will not write the brief until the board's answer
arrives. Ending turn to await the codename answer." and ends its turn
without `paperclip_finish`. The Runner's default report
(`provider:opencode:agent-message` as the only evidence,
`reportedWorkDisposition: done`) is accepted, and the low-risk claim
policy sets the task `done`. The question is still unanswered and the
`brief` document does not exist.

**What did you expect to happen?**
The task keeps waiting for the answer. The answer continues the task,
and the agent writes the brief.

**Steps to reproduce**
1. An agent asks a question on its task.
2. The person posts a comment before answering.
3. The agent ends its turn without a completion report, saying it waits
for the answer.
4. The task is `done`; the question stays pending.

## What Changed

- New `server/src/services/native-runtime/own-question-wait.ts`:
- `reportsOnlyProviderMessage(result)`: true when every evidence
reference in the report is `provider:<provider>:agent-message` and the
report has no artifact and no verification. This is the shape of the
Runner's default report for a turn that ended without
`paperclip_finish`.
- `pendingOwnQuestionId(db, …)`: the agent's own pending, undismissed
ordinary question (`ask_user_questions`, not a governed tool, secret or
connection request) on the task.
- `finalizeNativeRun` (`native-run-finalizer.ts`): when the proposed
decision is `done`, the task is not an Agent Chat conversation, the
report cites only the agent's final message, and the agent's own
question is still pending, the decision becomes `in_review` with reason
`own_question_unanswered` and releases the checkout. The question stays
pending, so the person's answer continues the task through the existing
answer path.
- Dismissal (`POST /issues/:id/interactions/:interactionId/dismiss`,
`dismissInteraction`): new `enqueueOwnQuestionDismissalWake` runs inside
the dismissal transaction, only when the card moves from pending to
dismissed. When the task is `in_review` on an applied
`own_question_unanswered` decision of the assignee's run, the dismissed
card is the assignee's own question, and no other pending, undismissed
question of that agent remains, it saves a durable wake intent
(`issue_question_dismissed`, idempotency key per question) in the
existing native status wake outbox (`enqueueWake`, now exported). The
outbox dispatcher delivers and retries it, so the agent continues
without the answer. Dismissal of any other request keeps the no-wake
rule.
- `assignmentDenial` (`agent-interaction-delivery.ts`): an
`issue_question_dismissed` wake counts as a person's wake when its
`dismissedInteractionId` names the recipient's own question on that task
with a `dismissedByUserId`, so a recipient that refuses agent
assignments still receives it.
- `doc/execution-semantics.md` describes the exception to the
historical-question rule.

## Verification

- New case in
`server/src/services/native-runtime/historical-task-questions.test.ts`
("keeps the agent's own unanswered question open after an unreported
turn"), run through the real `PaperclipControlPlanePort.completeRun` and
`finalizeNativeRun` with a low-risk `agent_claim_policy` contract, as on
staging:
- Red on master (finalizer hunk removed, test kept): `unreported` fails,
`expected 'done' to be 'in_review'`.
- Green: `unreported` rests `in_review` with `own_question_unanswered`
and the question still pending. `unreported-dismissed`,
`unreported-answered` and `verified` (work-product evidence) still rest
`done`, so the #15392 behavior is unchanged.
- Dismissal after finalization (same test): the release check is null
while a second own question is open and returns the agent once both are
dismissed. A repeated dismissal saves no second intent; the saved intent
is a queued `issue_question_dismissed` outbox row. Delivery test
`delivers a dismissal wake only for a person's dismissal of the
recipient's question` (30 passed). Route test `saves the dismissal wake
intent in the dismissal transaction only when the card is dismissed`
(147 passed).
- `cd server && pnpm exec vitest run
src/services/native-runtime/historical-task-questions.test.ts
src/services/native-runtime/native-run-finalizer`: all passed.
- `cd server && pnpm exec vitest run
src/services/native-runtime/paperclip-control-plane-port.test.ts
src/services/native-runtime/status-arbiter.test.ts`: 119 passed.
- `cd server && pnpm exec tsc --noEmit -p tsconfig.json`: passes.
- Chaos, Daytona OpenCode, seed 6201, W2Q K0, 4 tasks (wave
w-20261111T211421-s6201, run from master 7e8901aa60 plus this change
because the pinned runner image must match runner sources): no false
completion. t02 and t04 PASS (in_review -> in_progress -> done after the
answer). t01 failed `R3:retry_needed` through the blocked path fixed
separately in #20931 (not in this build). t03 timed out because its
rewake OpenCode turn stayed active for 20 minutes without ending
(provider turn never finished; this change acts only at finalization,
which never ran). The unreported-done path did not occur in this sample,
so the red/green proof for this change is the integration test. Before:
wave w-20261111T200718-s6201 t06 closed done with the question
unanswered.

## Risks

- Low. Only a `done` decision whose report cites nothing but the agent's
final message changes, and only while the same agent's own ordinary
question is pending and undismissed. Explicit completion reports with
their own evidence keep the current rule. A person can still dismiss the
question to release it.
- An agent that really finished without `paperclip_finish` while its own
old question is open now rests `in_review` instead of `done`. The open
question is visible on the task; answering or dismissing it settles the
task.
- No schema change. A result saved by an older release is finalized by
the same rule because it reads only the saved report and interaction
rows.
- Related observation (not changed here): in 5 Claude/OpenCode W2/W2C
cells the agent wrote "the task remains in_progress as instructed" yet
the task became `done` (Claude t06, wave s6101). The run's
`paperclip_finish` input itself sent `reportedWorkDisposition: "done"`,
`objectiveSatisfied: true`, with the deferred phase listed as
`blocksCompletion: false`. The server did not map a yielded disposition
to done. That is model behavior on a contradictory report, a different
root from this 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 16:50:28 -06:00
DottaandClaude Opus 5.5 65ebd96bcf fix(chat): keep the person's brief as a document on a new case's primary task (#20954)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - With Cases enabled, a person can ask chat to open a case from a
brief and hand it to an agent
> - The case scenario expects the brief and a workflow document on the
primary task before work starts
> - In one run the CEO copied the brief's facts into case fields but
saved only the workflow document
> - The Cases chat guidance says nothing about where the person's brief
belongs
> - This pull request adds one guidance sentence: save the brief with
`initialDocuments` in the same case-handoff request
> - The benefit is that the assignee always has the person's source,
saved before its run begins

## Linked Issues or Issue Description

**What happened**
In the chat eval `cases.create-case` (run 43107042937), the person
attached a customer bug brief and asked the CEO to open a case and have
Morgan investigate. The CEO created the case, its fields, the workflow
document and the kickoff. It did not save the brief on the primary task.
In an earlier run (43101125481) the CEO did save it, so the behavior
depends on the model's choice.

**Expected behavior**
The new primary task has the person's brief as a document, saved in the
request that creates it, before the assignee starts.

**Steps to reproduce**
1. Enable Cases. Attach a brief in chat with the CEO.
2. Ask: "Open a case for this customer bug, record the facts from my
brief, and have Morgan investigate first."
3. Read the primary task's documents: in some runs only `workflow`
exists.

## What Changed

- `CASES_CHAT_DIRECTIVE` (rendered only while Cases is enabled): one
sentence. When the person opens a case from a brief, save the brief as a
document on the primary task with `initialDocuments` on `POST
/api/companies/{companyId}/case-handoffs`. Case fields do not replace
the brief.
- `case-history-api-discovery.test.ts`: a regression test for the new
guidance.

## Verification

- Red: the new test failed before the change. Green: `npx vitest run
src/__tests__/case-history-api-discovery.test.ts
src/__tests__/agent-conversations.test.ts` (67 passed).
- The route already accepts `initialDocuments` for a newly created
primary task (`packages/shared/src/case-handoffs.ts`).
- End-to-end: chat eval run linked in a comment below.

## Risks

- Low risk. The sentence renders only when Cases is enabled. No API or
permission 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 16:47:12 -06:00
DottaandClaude Opus 5.5 6cfb1f3d87 fix(environments): resume a warm sandbox that its finished predecessor is still releasing (#20953)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A warm Paperclip Runner session keeps its Daytona sandbox between
runs. The next run of the same task resumes that sandbox through a
`reuse_by_environment` lease.
> - Acquisition only resumes a lease that is `released`, `retained`, or
held by the same run. A finished run releases its lease only after it
saves its instruction and workspace files, which takes several seconds.
> - A board wake that arrives in that window does not see the warm
lease. It creates a fresh sandbox. The session's durable backup cannot
be restored there, because its source lease is still held. The run fails
with `native_session_recovery_failed: runner_harness_state_mismatch:
backup_without_reusable_lease` and the task needs Retry, which starts a
new provider session.
> - This pull request makes acquisition wait, for a short bounded time,
for a lease that a recently finished predecessor is still releasing, and
then resume it.
> - The benefit is that the follow-up continues the same warm session in
the same sandbox, with no Retry, no restore and no lost provider
context.

## Linked Issues or Issue Description

**What happened?**
Restart-chaos wave `w-20261111T205259-s6301` (Codex, Daytona warm,
volume-free server, `--ephemeral-server` from #20930), task t01. The
board re-wake run `e7129ec7` started at 20:57:44. Its predecessor
`5cf8c306` reported `run.terminal` at 20:57:38 but saved its instruction
copy until 20:57:51, so its warm lease was still `active`. At 20:57:53
the new run recorded `startup reuse: environment (fresh)`. It then
failed with `runner_harness_state_mismatch:
backup_without_reusable_lease` (error code
`native_session_interrupted`). Recording the `workspace_finalize`
operation then failed with a Postgres deadlock, and the run settled
`native_finalization_missing`. Retry run `79aa18ec` worked with a fresh
provider session. The K1 restart in that wave was at 20:55:32 and is not
the cause: six warm runs after it reused the sandbox normally.

**Root cause**
`reusableLeaseCanBeResumed` in
`server/src/services/environment-runtime.ts` excludes an `active` lease
held by another run. That rule is correct for a running predecessor. For
a predecessor that has already ended, the `active` state is a short
release tail. The new run creates a sandbox, and
`retiredSourceAuthorizesNativeBackupRestore`
(`native-retired-backup-source.ts`) correctly refuses to restore the
backup from a source lease that is not yet `released` or `retained`
(`idleWarmSourceRetiresBackup`). The executor's last branch
(`native-session-executor.ts`, host preparation, created allocation)
then throws `backup_without_reusable_lease`.

**How often**
Seen in 2 of 361 retained wave result directories on this machine
(`s6301` today, `s5201` earlier). It needs a board wake within a few
seconds of the predecessor's terminal event, so it is timing dependent.

## What Changed

- `server/src/services/environment-runtime.ts`: add
`waitForSettlingReusableLease`. Before the plugin and built-in sandbox
paths select a reusable lease, acquisition polls (250 ms) for up to 20 s
while a matching `reuse_by_environment` lease is `active` under a
different run that is `succeeded`, `failed`, `cancelled` or `timed_out`
and finished in the last two minutes. It then continues with the normal
selection, which resumes the released lease.
- A running predecessor causes no wait (unchanged: the new run gets a
new sandbox). A predecessor that ended more than two minutes ago and
never released causes no wait.
- `server/src/__tests__/environment-runtime.test.ts`: two red/green
cases (succeeded and failed predecessor release the lease 400 ms after
the successor starts acquiring) and one case for an old unreleased lease
(no wait, new sandbox, old lease untouched).
- `doc/execution-semantics.md`: describe the wait next to the existing
warm-reuse rules.

## Verification

- Red on master: `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/environment-runtime.test.ts -t predecessor` fails both
cases with `expected 'warm-2' to be 'warm-1'`: the successor created a
second sandbox instead of resuming the first.
- Green: the same command passes (3 tests).
`environment-runtime.test.ts`, `plugin-warm-tasks.integration.test.ts`
and `environment-run-orchestrator.test.ts` pass (288 tests). Server `tsc
--noEmit` passes.
- Not verified in a chaos wave. The window is a few seconds wide and
appeared in 2 of 361 waves, so one wave cannot show the change.

## Risks

- A successor can start up to 20 s later when its predecessor's release
is slow. Creating a sandbox and restoring a backup takes longer, and
failing needs a Retry. The two-minute bound keeps a stuck,
never-released lease from delaying every later run.
- If a caller held a lock that the predecessor's release needs, the wait
ends at 20 s and acquisition behaves as before.
- Compared with #20863 (fresh-sandbox fallback) and #20811 (unsaved
provider state marker): this change does not alter either. It reduces
how often a warm continuation reaches the fresh-sandbox branch. The
backup restore proof is unchanged, so an unproven backup still fails
closed with a working Retry.
- The `workspace_finalize` deadlock in the same run is a separate defect
and is not fixed here.

## 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 16:40:58 -06:00
DottaandClaude Opus 5.5 2719145ae2 fix(chat): save a requested start order as a first-class blocker (#20947)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - People use chat to tell the CEO agent how work must be ordered
> - A person said "Do not start the website until the design is
approved"
> - The CEO only promised to hold the work. It did not save a blocker,
so nothing stopped the Website task
> - Chat guidance names blockers only when chat creates new tasks. It
says nothing about an order between existing tasks
> - This pull request adds one guidance line: save the order as a
blocker on the waiting task
> - The benefit is that Paperclip enforces the order and wakes the
assignee when the prerequisite is done

## Linked Issues or Issue Description

**What happened**
In the chat eval `task-coordination.block-on-design` (run 43088288923),
the person asked the CEO to keep Website from starting until Design is
approved. The CEO read both tasks and replied "I'm holding both until
the design is actually approved". It saved no blocker. Website stayed
startable.

**Expected behavior**
The CEO adds Design to Website's `blockedByIssueIds`. Website cannot
start while Design is open. Its assignee wakes when Design is done.

**Steps to reproduce**
1. Create Design and Website tasks in backlog. Assign Website to an
agent.
2. In chat with the CEO, say "Do not start the website until the design
is approved."
3. Read Website: `blockedBy` is empty in some runs, because the guidance
does not tell the agent to save the order.

## What Changed

- `AGENT_CHAT_DIRECTIVE`: one paragraph. When the person says an
existing task must not start until another is done or approved, add the
prerequisite to the waiting task's `blockedByIssueIds` with `PATCH
/api/issues/{id}`, and keep existing blockers. A comment or a promise
does not stop the work. Link both tasks.
- `agent-conversations.test.ts`: a regression test for the new guidance,
in the directive and in the rendered chat prompt.

## Verification

- Red: the new test failed before the guidance change.
- Green: `npx vitest run src/__tests__/agent-conversations.test.ts
src/__tests__/case-history-api-discovery.test.ts` (66 passed).
- Local scripted-human check (dev server, Playwright): with the blocker
saved, the Website task shows the Design row under "Blocked by", a Todo
click returns 200 without a Morgan run, Design done wakes Morgan with
`issue_blockers_resolved`, and the follow-up chat message sends.
- End-to-end: chat eval run linked in a comment below.

## Risks

- Low risk. The change adds about 470 characters to the chat prompt. It
changes no API or permission. The route and field already exist and
agents already use them.

## 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 16:12:19 -06:00
DottaandClaude Opus 5.5 612cc7e3e6 fix(heartbeat): renew every legacy controller lease in one shared statement (#20924)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A legacy run holds a durable controller lease while it prepares. The
server renews the lease every 10 seconds, and it stops the run if no
renewal succeeds within the 60 second lease.
> - Each run renewed in its own transaction on a private pool of two
connections. Each renewal used six round trips (begin, statement
timeout, transaction id, notify, update, commit) and woke live-run
recovery for that run.
> - When many runs prepare at the same time on a loaded database, the
renewals queue behind each other. The queue grows with the number of
runs, and every run loses its lease at about the same time.
> - A healthy server must not fail its own runs because it has many of
them.
> - This pull request renews all leases of one server process through
one shared queue. Requests that arrive while a renewal is in flight
share the next statement.
> - The benefit is that the renewal cost no longer grows with the number
of live runs, so a burst cannot starve the lease.

## Linked Issues or Issue Description

**What happened?**
On a staging server that did not restart, 22 runs failed with
`controller_lease_lost` in about 13 minutes. Every run was a legacy run
in the `preparing` stage with `providerWorkStarted: false`. Every loss
had cause `renewal_deadline`. The renewal had been pending 28 to 48
seconds (`renewalPendingMs`). The deadline callback ran on time
(`deadlineLateMs` 3 to 861 ms), so the event loop was not blocked. The
successful renewals before the loss had each taken 20 to 30 seconds,
because the deadline fired only 25 to 48 seconds after the last renewal
(`sinceRenewalMs`). About 30 runs were live in the process. The first
wave started when a user retried about 14 blocked tasks at once. The
automatic retries of the first wave then failed the same way, which made
the second wave. 12 of 16 affected tasks became blocked.

**Steps to reproduce**
1. Start about 30 legacy runs in one server process.
2. Make each renewal transaction slow (for example, a loaded database).
3. Wait one lease period.

**Expected behavior**
The leases stay owned. The renewal cost does not grow with the number of
runs.

**Actual behavior**
All runs fail with `controller_lease_lost` (`renewal_deadline`).

## What Changed

- `server/src/services/legacy-controller-lease.ts`: All controllers of
one process renew through one queue per database. One flush is in flight
at a time. Requests that arrive during a flush share the next flush, so
a renewal waits behind at most one other statement. A flush is one
transaction on the private lease pool: `statement_timeout` and one
statement that renews all requested rows.
- The statement locks the requested rows with `for update skip locked`.
A row that another transaction holds is reported busy, and only that run
retries after 250 ms. It does not stall the other runs in the batch. A
row that is not this boot's live legacy lease is reported rejected, as
before.
- Renewal no longer emits the `active-execution-recovery` notification.
A renewal only moves the deadline later. Recovery already has a deadline
at the earlier expiry and rereads the moved deadline then. This removes
one recovery pass, `pg_current_xact_id` and `pg_notify` per renewal.
- The lease contract does not change: 60 second lease, 10 second
renewal, boot-id fence, expired leases cannot renew, the local deadline
is measured from the attempt start, and the loss causes and timings are
the same.
- `doc/run-log-events.md`: Documents the shared renewal queue.

Upgrade story: no persisted shape changes. The row columns, lease length
and boot-id fence are the same. A lease held by the previous release has
a different boot id, so the new batch rejects it and leaves it
untouched. A test covers a previous-release lease in the same batch.

## Verification

- Red on `origin/master`, green with this change:
- `keeps thirty preparing controllers owned when every renewal takes
four seconds on a two-connection pool`: before, all 30 controllers lost
the lease with `renewal_deadline` (the staging shape). After, none lose
it.
- `renews fifteen concurrent controllers in one statement` (real
Postgres): before, 15 update statements. After, 1.
- `renews through one private statement without waking live-run
recovery`: before, the renewal woke `active-execution-recovery`. After,
it does not.
- `live-recovery-work.test.ts` `renews without waking recovery and
re-reads the moved controller deadline at the old one`: before, 2
recovery passes per renewal. After, none, and recovery still moves to
the new deadline at the old one.
- Safety cases: a row lock on one run does not delay another run; a
previous-release lease in the same batch stays untouched; a terminated
lease connection is retried; existing deadline, retry, rejection and
revocation tests pass.
- `npx vitest run src/services/legacy-controller-lease.test.ts
src/__tests__/live-recovery-work.test.ts`: 88 passed.
- `pnpm verify`: see the CI checks on this head.

## Risks

- Medium-low. All renewals of a process now share one statement. A
failed flush fails every request in it; each watcher retries
independently, as it did before.
- A busy row is retried every 250 ms until the lease deadline. Before,
the renewal waited on the row lock for up to the statement timeout. The
loss behavior is the same.
- Recovery wakes at the old lease expiry instead of after each renewal.
It finds the live lease and moves to the new expiry.
- Not in scope: the downstream workspace recovery that blocked tasks
after a lost lease (`legacy_execution_requires_reconciliation`,
`source_unavailable`).

> 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, 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 15:57:20 -06:00
DottaandClaude Opus 5.5 228b0169be fix(dispatch): let the answer to the agent's own question continue the task it blocked on (#20931)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - An agent can ask the person a question (`request_human_input`) and
wait for the answer
> - A later comment from the person moves an open question into history
(#15392). The agent still needs the answer, so it reports a blocker and
the task becomes Blocked
> - The person then answers. Dispatch cancels the answer's continuation
with `issue_not_in_progress`, because only a dependency-blocked task may
take an answer while Blocked
> - The task rests Blocked with a failed run and a Retry. The answer is
lost until a person clicks Retry
> - This pull request lets the answer continue the task when the same
agent blocked it to wait for that answer, proven from durable records
> - The benefit is that the person's answer always reaches the agent
that asked, without a Retry

## Linked Issues or Issue Description

Refs #15392, Refs #20872

**What happened?**
Restart-chaos waves with no fault injected fail every W2Q task on every
provider (Claude, OpenCode, Codex) with `R3:retry_needed`. For Claude
task t03 on master 7e8901aa60: run 5aeb9244 asks a question and the task
goes `in_review`. A board comment ("I will answer shortly; do not write
the brief yet") wakes run d728886f. That comment makes the question
historical, so `paperclip_finish` with a `response_wake` wait is
rejected ("without a pending wait condition"). The agent then calls
`paperclip_block`; the status decision `current_track_blocker_waiting`
sets the task Blocked. The board answers. Continuation run adbda5de is
cancelled: "resolved-interaction continuation issue is no longer
in_progress (current status: blocked)". The task rests blocked/failed
with Retry.

**What did you expect to happen?**
The answer continues the task. The agent writes the brief with the
answer and finishes.

**Steps to reproduce**
1. An agent asks a question on its task.
2. The person posts a comment before answering.
3. The agent reports a blocker while it waits for the answer.
4. The person answers the question.
5. The answer's continuation run is cancelled with
`issue_not_in_progress`.

## What Changed

- Status committer (`status-decision-committer.ts`): when it applies an
agent-reported, board-owned blocker (`current_track_blocker_waiting` or
`task_wide_blocker_bound`), it records the agent's own pending,
undismissed ordinary questions on the task as `awaitingInteractionIds`
in the decision JSON, in the same transaction under the issue lock
(`questionsAwaitedByAgentBlock`).
- New `agentBlockingQuestionState`
(`server/src/services/blocked-question-answer.ts`). It reads only
server-owned rows. It returns the verified `lastStatusDecisionId` and
`statusVersion` when: the answered interaction was created by this agent
on this task and resolved by a person; the task is Blocked and still
assigned to the agent; the task's `lastStatusDecisionId` is an applied
Blocked decision with one of the two reason codes, made by a run of the
same agent, whose `awaitingInteractionIds` contains this interaction;
and the task's `statusVersion` still equals that decision's
`projectedStatusVersion` (nothing changed the status since).
- `decideQueuedRunStaleness`
(`server/src/modules/run-dispatch/domain/policy.ts`): the Blocked
exemption for an answer continuation also accepts this fact
(`isAgentBlockingQuestionAnswer`). The postgres adapter loads it under
the issue lock.
- `heartbeat.ts` resolved-interaction checkout: for that answer,
`blocked` is an accepted expected status, and the verified state is
passed to `issues.checkout` as `blockedState`.
- `issues.checkout`: with `blockedState`, the guarded update accepts a
Blocked row only when `lastStatusDecisionId` and `statusVersion` still
match, so a person who re-blocks the task in between keeps the new
block. When the expected statuses include `blocked`, a checkout from
Blocked also clears `unblockDescriptor`, `blockedTransitionAt` and
`blockedOwnerNotifiedAt`, as an ordinary status change from Blocked
does.
- `doc/execution-semantics.md` describes the rule.

## Verification

- New integration test
`server/src/__tests__/heartbeat-blocked-question-answer.test.ts`
(embedded Postgres, real `commitNativeStatusDecision`): the agent's
blocker decision makes the task Blocked, the person answers, the answer
is delivered through `heartbeat.wakeup` with the same context shape as
staging.
- Red on master (product hunks removed, test kept): `delivers the answer
when the agent blocked the task to wait for it` fails with `status:
cancelled, errorCode: issue_not_in_progress`, the same code as the chaos
run.
- Green: 5/5 pass. The answer run succeeds, the adapter runs, and the
blocker fields are cleared. Negative cases still cancel with
`issue_not_in_progress`: a person re-blocked the task after the agent,
the question did not exist when the agent blocked, and the task is done.
A checkout with a stale verified state conflicts and keeps the person's
new blocker.
- `policy.test.ts`: new case for the fact (continue when set, cancel
when unset, reassignment, backlog/done/cancelled).
- `cd server && pnpm exec vitest run src/modules/run-dispatch
src/__tests__/heartbeat-dependency-scheduling.test.ts`: 208 passed.
- `cd server && pnpm exec vitest run
src/__tests__/issues-service.test.ts
src/__tests__/heartbeat-stale-queue-invalidation.test.ts
src/services/explicit-native-continuation.test.ts`: 656 passed.
- `cd server && pnpm exec tsc --noEmit -p tsconfig.json`: passes.
- Chaos, local, seed 5101, `--workloads W2Q --faults K0 --adapters
codex,claude --lifecycle warm`, on 722303e4a2 (first head): wave
w-20261111T204405-s5101, 6/6 PASS (3 Codex, 3 Claude). Every task went
in_review -> blocked -> in_progress -> done without Retry. Before: the
same local seed on master (w-20261111T190352-s5101) failed 3/3 Codex W2Q
cells with `R3:retry_needed`, and Daytona Claude wave
w-20261111T200553-s6101 failed 2/3 W2Q cells the same way.

## Risks

- Low to medium. Only a resolved interaction continuation on a Blocked
task changes behavior, and only when the durable linkage holds. If a
board-owned blocker about something else and the agent's own question
are open together, the answer resumes the agent; it then reports its
blocker again. The blocker report carries no interaction reference, so
the server cannot tell them apart; cancelling the answer instead loses
it. A person's Blocked status, a block from before the question,
dependency blocks, reassignment, done and cancelled keep the old cancel.
- State saved by an older release: no schema change and no migration. A
Blocked decision saved before this change has no
`awaitingInteractionIds`, so its answer keeps the old behavior (cancel,
Retry works).
- The checkout change only applies when a caller passes `blocked` as an
expected status.

## 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 15:54:33 -06:00
DottaandClaude Opus 5.5 edd00526e7 fix(native): park a response_wake with no admitted response instead of self-waking (#20935)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A native agent run ends with a structured result. The status arbiter
turns that result into an issue status and next actions.
> - A result can report `yielded` with `continuation.kind:
"response_wake"`. This means "wake me when somebody responds".
> - Only a direct Board comment gave this kind a passive wait. Every
other `response_wake` became an immediate `issue_status_changed` wake to
the same agent.
> - The agent woke, saw nothing new, reported `response_wake` again, and
woke again. Nothing bounded this loop. It spent provider budget and its
back-to-back runs caused other failures on warm sandboxes.
> - This pull request parks such a result as a visible wait, and puts
declared continuation wakes under the rewake throttle as a backstop.
> - The benefit is that an agent that waits for input stops and costs
nothing until a real response arrives.

## Linked Issues or Issue Description

**What happened?**

Restart-chaos waves on master `683fd1e9a5` (no faults injected) showed
one task run 30 to 66 times, about 12 s apart. Example: wave
`w-20261111T190352-s5101`, task t02 (W2C), 65 runs in 13 minutes. Each
run reported `yielded` with `continuation.kind: "response_wake"` and the
summary "No changes made. This status-change wake does not trigger phase
2". Each run was woken by `issue_status_changed`, and each finalization
committed `live_continuation_registered` with an `enqueue_continuation`
effect of kind `response_wake`.

Root cause: `arbitrateNativeStatus()`
(`server/src/services/native-runtime/status-arbiter.ts`) only treats
`response_wake` as passive when `boardResponseWaitAuthorized` (from
`readNativeBoardResponseWaitSource`), external chat, or plan wait is
true. Otherwise it falls to the generic yielded branch, and
`status-decision-committer.ts` (`enqueue_continuation`) enqueues an
`issue_status_changed` wake to the same agent. `issue_status_changed` is
not in `THROTTLED_ISSUE_REWAKE_REASONS`, and the native committer writes
an `issue.updated` activity row with the run's id for every decision, so
the rewake throttle saw every native run as progress.

**Expected behavior**

A `response_wake` with no admitted response source parks the task. A
comment, an interaction answer or a status change by somebody else wakes
the agent. No path can loop without a bound.

**Steps to reproduce**

Run `tests/restart-chaos/main.ts --env local --adapters codex
--workloads W2,W2Q,W2C --concurrency 10 --tasks 10 --seed 5101
--lifecycle warm --triggers time`. W2/W2C tasks loop after phase 1.

## What Changed

- `status-arbiter.ts`: a `yielded` / `response_wake` result with no
admitted response source and no blocking remaining work now commits
`response_wake_waiting_for_input`, keeps the task `in_progress`, and has
no effects. Policy version is `phase6-v14`. Board, external chat, plan,
governance, dependency, pause and unfinished-work branches are unchanged
and still come first.
- `native-runtime/native-response-wake-wait.ts` (new) and
`recovery/service.ts`: recovery treats that applied decision as a
durable wait while it is the issue's latest decision, the issue is
`in_progress` with the same agent assignee, and no user comment arrived
after the run started. This applies to the stranded-work sweep and to
the paused-agent passive-wait check.
- `execution-projection.ts`: a committed run with this reason shows the
phase `waiting_for_answer`, label "Waiting for a reply", and a next
action that tells the user to comment or change the status. This shows
only while that decision is still the issue's latest decision and the
issue is `in_progress`; an older run shows "Completed".
- Backstop in `issue-rewake-throttle.ts` and `heartbeat/queue.ts`: an
`issue_status_changed` wake that carries a declared `continuationKind`
(the `enqueue_continuation` effect) is now a throttle candidate. The
native status committer's own `issue.updated` / status-decision activity
no longer counts as run progress or as new input. After two consecutive
successful runs with no issue-visible progress, such a wake waits for
the existing escalating cooldown (2 min, doubling, max 30 min). The
native status dispatcher (`dispatchNativeStatusWakeBatch`) does not drop
a throttled intent: it keeps the intent `queued`, reports the cooldown
deadline to the deferred-receipt worker, and dispatches it again when
the cooldown ends. Status wakes without a declared continuation (blocker
owner, notify owner, retry) are unchanged.
- Restart chaos: new enforced universal check U5 (`universal.ts`). A
task with more than 10 runs fails as `U5:run_budget_exceeded`
(`RESTART_CHAOS_U5_MAX_RUNS`). The README records the calibration.
- `doc/architecture/native-status-arbitration.md`: the decision table
and the wait rules.

## Verification

- RED on master (same reason as the waves):
`heartbeat-process-recovery.test.ts` "parks a native response_wake that
no response admitted…" fails with decision `reasonCode:
"live_continuation_registered"` and effect `{ kind:
"enqueue_continuation", continuationKind: "response_wake" }`.
`heartbeat-issue-rewake-throttle.test.ts` "throttles a
same_agent/response_wake/retry continuation…" fails 3/3 because the wake
is admitted.
- GREEN: the integration test finalizes the run, dispatches native
status wakes and runs the stranded-work sweep three times. It asserts
one run total, one wake request, no recovery action, the task
`in_progress`, and the projection "Waiting for a reply".
- `heartbeat-process-recovery.test.ts` "delays a no-progress native
continuation until the rewake cooldown…" finalizes a real `same_agent`
continuation after a no-progress run, dispatches twice (one throttle
receipt, intent stays `queued`, deadline equals `nextAllowedAt`), then
dispatches after the cooldown and gets the run. It fails when the
dispatcher drops the throttled intent.
- Focused suites pass: `heartbeat-process-recovery`,
`heartbeat-issue-rewake-throttle`, `status-arbiter`,
`native-status-arbiter-corpus`, `runtime-mode`, `execution-projection`,
`chat-run-milestone-work`, `heartbeat-dependency-scheduling`,
`heartbeat-run-summary` (904 tests), and all restart-chaos unit tests
(584 tests). `pnpm --filter @paperclipai/server typecheck` passes.
- Universal audit over the 17 saved waves of 2026-11-11: U5 flags 12
tasks, all looping tasks (12 to 66 runs), and 0 tasks on all-pass waves.
- Chaos rerun of local warm seed 5101 (W2, W2Q, W2C): result is added in
a comment on this PR.

## Risks

- Behavioral shift: a model that reports `response_wake` to mean
"continue now" no longer gets an immediate run. The task waits visibly
for a reply. The runner prompt already describes `response_wake` as a
real response wait, and `same_agent` stays available for server-owned
continuations.
- The throttle now applies to native runs. A chain of declared
continuations with no issue-visible output (no comment, document, work
product or other activity) waits for the cooldown after the second such
run. `same_agent` continuations are server-owned (repository
preparation), so this affects only repeated no-output chains.
- Old saved state: decisions committed under `phase6-v13` keep their
recorded effects. A run that finalizes after deploy uses the new policy.
No migration.
- Budget hard-stop behavior is unchanged.

## 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>
2026-11-11 15:40:44 -06:00
DottaandClaude Fable 5.2 b2779054d4 fix(issues): let a native run hand its own task to a person without cancelling itself (#20884)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A task has one assignee. An agent often finishes its work by handing
its own task to a person for review.
> - The task update route stops the outgoing run before it saves a
reassignment.
> - Since #16082, a legacy run that hands its own task to a person is
exempt, so it finishes its turn. Native runs were not exempt.
> - On staging, a native run (ACPX Claude, 11.5 minutes of work) sent
`PATCH /api/issues/{id}` to hand its task to a person. The route
cancelled that run with `issue_reassigned`. The agent saw
`api_transport_failure`. The task moved to the person, but it kept a
stale execution blocker ("Saved work recovered. Send a message to
continue.").
> - The native runtime assumes a running run still owns its task. So
widening the exemption alone is not enough: the run then fails after its
result is accepted, and its finalizer can act on the person's task.
> - This pull request applies the exemption to native runs and makes the
native terminal fence, restart recovery and finalizer handle a run that
gave its task away.
> - The benefit is that an agent can hand its task to a person and
finish normally, with no blocker on the person's task.

## Linked Issues or Issue Description

Refs #16082 (legacy exemption). Related: #20541 (agent-to-agent self
handoff) and #18293 (agent key handoff). Those PRs cover other actors
and targets. This PR covers native runs that hand their task to a
person.

**What happened?**

A native agent run handed its own task to a person (`assigneeAgentId:
null`, `assigneeUserId`, status `in_review`) through its signed run
token. The route cancelled the run that sent the request
(`issue_reassigned`, "Cancelled before issue reassignment"). The
cancelled native run then went through workspace recovery. The person's
task kept an execution blocker with `restore_failed`.

**Expected behavior**

The run that hands its own task to a person keeps running and finishes
its turn. The task rests `in_review` with the person. No execution
blocker or recovery action stays on the task. The outgoing run cannot
change the task after the hand-off.

**Steps to reproduce**

1. Run a native agent on its assigned task.
2. From that run, send `PATCH /api/issues/{id}` with `assigneeAgentId:
null`, a person's `assigneeUserId`, and status `in_review`.
3. Before this fix, the route cancels the run with `issue_reassigned`.

**Why the exemption was legacy-only**

#16082 limited the exemption to the run shape of its incident and kept a
verified stop for every other case. The new H2 chaos workload showed why
native needed more than the route change. With only the route widened:

1. The run's terminal fence `assertCurrentWakeCommentsRead` requires the
run to still own the task. It threw
`native_current_wake_comments_binding_changed` after the result was
accepted. The run then failed with `native_session_lease_lost`, and its
finalizer recorded `workspace_finalization_failed` as an agent-owned
recovery on the person's task.
2. A restart after the hand-off left the run `running`, because restart
recovery refused it with `issue_execution_lock_changed`.
3. The native finalizer only superseded a `done` decision. A
`needs_review` report moved the person's task back to `in_progress` and
queued a continuation for the outgoing agent.
4. The REST guard against the outgoing run's later mutations
(`issue_handed_off`) applied to legacy runs only.

## What Changed

- `server/src/routes/issues.ts`: the signed current-run human
self-handoff exemption now applies to native runs. All other checks
stay: exact run, agent, company and task, `agent_jwt` source, human
recipient. For a native run the route first records the hand-off on the
run (`resultJson.humanSelfHandoff`, new `native-human-self-handoff.ts`).
Board reassignment, agent keys, and handoffs to an agent or to nobody
still stop the outgoing run.
- `current-wake-comments.ts`: the terminal fence accepts a run whose
recorded hand-off matches the person who now owns the task. Without the
record (for example a board reassignment), or after a later
reassignment, it still refuses.
- `native-restart-recovery.ts`: after a restart, a handed-off run is not
resumed. It passes the execution-lock check and is then settled by the
existing ownership gate (`task_ownership_or_status_changed`) without
changing the person's task, instead of staying `running`.
- `status-arbiter.ts` and `native-run-finalizer.ts`:
`supersedeHandedOffDecision` keeps the person's task status and strips
retries, continuations, owner wakes, recoveries, blockers and reviewer
bindings, also from decisions that already preserve the status. Two
effects stay: a governed request's interaction, and the
workspace-finalization repair, so saved work is not lost. Review runs
are not affected.
- `server/src/run-task-ownership.ts` (renamed from
`legacy-run-task-ownership.ts`): `assertHandedOffRunTaskOwnership`
rejects later task mutations from the outgoing legacy or native run, in
the route and under the update transaction's locks.
- `tests/restart-chaos/`: new opt-in workload `H2`. The agent saves a
brief, then hands the task to its creator in review as its last step.
The oracle requires the task to rest `in_review` with the creator, every
run to settle without `issue_reassigned`, readable non-empty run
history, and no execution blocker or active recovery action.
- Docs: `doc/execution-semantics.md`, task feature map and agent access
map, chaos README.

## Verification

- Red on `origin/master` (same tests, master source): the route cancels
the native run (500, and 409
`runner_goal_reassignment_stop_unconfirmed`); the finalizer moves the
person's task to `in_progress` with a continuation for the outgoing
agent; a late `done` from the outgoing native run closes the person's
task.
- Red chaos wave on master's server (`--server-from` a master checkout,
`--adapters claude --workloads H2 --faults K1 --matrix`): 0/2 pass. Both
runs ended `cancelled` / `issue_reassigned`
(`T:handoff_did_not_cancel_run`).
- Green focused tests on this branch:
- `native-human-self-handoff.test.ts` (embedded Postgres): hand-off
through the real issue service; the run stays `running`; `done`,
`needs_review` and failed-turn reports leave the task `in_review` with
the person, with `getExecutionBlocker` null and no recovery action; the
terminal fence accepts only the recorded hand-off; a late change from
the outgoing run fails with `issue_handed_off`; pure policy cases.
- `issue-agent-mutation-ownership-routes.test.ts`: native and legacy
self-handoffs persist without `cancelRun`, and native records the
hand-off before the update. Board, agent key, other run, task, agent and
company still stop the run.
- Native runtime suite (restart recovery, wake comments, arbiter,
stale-owner completion), `issues-service`, `low-trust-red-team-routes`:
pass. Server and restart-chaos typecheck pass. `node
scripts/check-maps.mjs` passes.
- Green chaos waves on this branch (local, `--adapters claude
--workloads H2`):
- `--matrix --faults K1 --seed 4459`: 2/2 pass (K0 baseline and a K1
crossing).
- `--tasks 3 --faults K1 --fault-after-seconds 60` and
`--fault-after-seconds 100`: 3/3 pass each. In the first, all three
restarts landed after the hand-off while the runs were live; the runs
settled and the tasks stayed with the person.
- The final waves ran on the pre-rebase head `8a2a3b6a74`. The rebase
onto master only merged the chaos workload lists (`types.ts`,
`config.ts`, `journeys.ts`). No Daytona wave was run.

## Risks

- The outgoing native turn keeps running after the person owns the task.
Its native task tools already require the run to own the task, so they
refuse (`paperclip_runner_tool_binding_not_authorized`). Its REST
mutations are rejected with `issue_handed_off`.
- A restart after the hand-off settles the run as failed
(`native_restart_recovery_blocked`) instead of resuming it. The person's
task is not changed and needs no Retry.
- The finalizer rule covers any native run whose task belongs to a
person at finalization, not only a self-handoff. Before this change such
a run could still move the person's task and queue its own continuation.
- Saved state: `resultJson.humanSelfHandoff` is a new optional run key.
An older server ignores it, so a run handed off on the new server and
finished on an old one fails its fence as today. No schema change.
- No API shape 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 (Anthropic), model ID `claude-opus-5-5`, through Claude Code
with tool use and code execution. The session does not expose the
context window or reasoning mode.

## Checklist

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

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

---------

Co-authored-by: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 15:30:59 -06:00
DottaandClaude Opus 5.5 7cd5558e41 fix(authz): send agents denied a cost-recording route to the Costs page (#20922)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - When an agent is denied a board-only action, the denial names the
board page where a person can do it, and agents relay that link
> - Cost recording retry ("Retry cost recording") is board-only and
lives on the run's row on the Costs page
> - The denial for every `/companies/:id/costs` route named the Budgets
page instead
> - So an agent asked to fix a run's missing cost sent the person to
Budgets, where the control does not exist
> - This pull request maps cost routes to the Costs page and names that
page in the retry route's catalog entry
> - The benefit is that the person lands on the control the agent told
them to use

## Linked Issues or Issue Description

**What happened**
In paid chat eval run 43086418488
(`budgets-costs.repair-cost-recording`), the CEO tried `POST
/api/companies/{companyId}/costs/retry-accounting`, got `403
board_access_required` whose remediation said `[Open
budgets](/<prefix>/activity/budgets)`, and told the person to use the
retry on Budgets. The retry control is on the Costs page.

**Expected behavior**
Board denials for cost routes name `[Open
costs](/<prefix>/activity/costs)`. Budget and budget-incident routes
still name Budgets.

**Steps to reproduce**
1. As an agent, call `POST
/api/companies/{companyId}/costs/retry-accounting`.
2. Read the 403 `remediation`.
3. It links `/<prefix>/activity/budgets`, a page with no cost retry
control.

## What Changed

- `server/src/routes/authz.ts`: `COMPANY_COSTS_PAGE`
(`/<prefix>/activity/costs`); `/companies/:id/costs/*` denials use it.
Budgets and budget incidents keep `COMPANY_BUDGETS_PAGE`.
- `server/src/routes/openapi.ts`: the retry route's description says a
board user retries with Retry cost recording on the run's row on the
Costs page.
- `board-access-denial.test.ts`: regression row for the retry route (red
before the change, green after).

## Verification

- `pnpm exec vitest run src/__tests__/board-access-denial.test.ts
src/__tests__/openapi-routes.test.ts
src/__tests__/agent-refusal-finality.test.ts
src/__tests__/cost-data-quality-routes.test.ts
src/services/native-runtime/runner-api.test.ts` (server): all pass. The
new row fails without the `authz.ts` change.
- End-to-end: the chat eval case `budgets-costs.repair-cost-recording`
grades that the agent's guidance links the Costs page; it will be rerun
after this merges.

## Risks

- Low. Only the link text and target in cost-route denials and one
catalog description change. Agents that previously relayed Budgets now
relay Costs.

## 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 14:54:36 -06:00
DottaandClaude Opus 5.5 34f71f7d57 fix(server): defer a run that meets a busy agent directory instead of failing it (#20829)
## Thinking Path

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

## Linked Issues or Issue Description

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

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

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

## What Changed

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

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

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

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

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

## Verification

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

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

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

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

## Risks

- A deferred run waits 5 to 15 seconds per deferral, up to 30 times. If
the holder is still busy after that, the run fails as before with a
plain message.
- Before admission, deferral cancels the run and creates a retry run,
like the existing workspace-busy wait.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 13:56:45 -06:00
DottaandClaude Opus 5.5 fb695f7ae6 fix(secrets): let an approved binding fill a blank agent config value (#20908)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents can propose binding a company secret to another agent; a
person approves it with "Create binding" on a chat card
> - Approval refuses when the target config key already holds a
different value, so it never overwrites a working credential
> - An empty string or empty plain value also counted as "already
exists"
> - So an agent whose key was cleared could not be repaired through the
governed binding card: approval returned 409
> - This pull request treats a blank value like an unset key; non-empty
values still conflict
> - The benefit is that the supported binding approval repairs a member
with a blank credential

## Linked Issues or Issue Description

Refs #20879

**What happened**
In the `teams.configure-team` chat eval, the CEO proposed binding the
company OpenRouter secret to the QA member as `env.OPENROUTER_API_KEY`.
QA's saved value was an empty string. Approving that proposal fails with
409 "Agent config path already exists: env.OPENROUTER_API_KEY".

**Expected behavior**
Approval fills a blank value (empty string or empty plain value) with
the secret reference. A non-empty value still blocks the approval.

**Steps to reproduce**
1. Save an agent with `adapterConfig.env.TOKEN = ""`.
2. Have an agent propose a binding of a company secret to that agent as
`env.TOKEN`.
3. Approve the proposal as the board user.
4. The approval returns 409 and the binding is not created.

## What Changed

- `secret-proposals.ts`: `applyBindingApproval` treats `undefined`, `""`
and `{ type: "plain", value: "" }` as an unset key for both `env.<KEY>`
and other config paths.
- `secret-proposals-routes.test.ts`: regression tests for an empty
string and an empty plain value. The existing test that a non-empty
value conflicts still passes.

## Verification

- `pnpm exec vitest run src/__tests__/secret-proposals-routes.test.ts`
(in `server/`): 49 passed.
- Red check: without the service change, the two new tests fail.

## Risks

- Low risk. Only a blank value changes behavior. A blank value carries
no credential, so nothing working is replaced.

## Model Used

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

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 13:55:22 -06:00
DottaandClaude Opus 5.5 eb5ad4c920 fix(cases): say a Cases-disabled denial is final and name the Experimental setting (#20892)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Cases is an experimental feature; an instance admin turns it on in
Experimental settings
> - When Cases is off, every case and workflow route answers 403 "Cases
are disabled" with no other information
> - A chat agent asked to "open a customer case" could not tell whether
the denial was final or what to offer
> - In one eval run it created a project and a task in place of the
case; in another it spent most of its turn searching routes before it
replied
> - This pull request gives the denial a code and a final, actionable
remediation, like the board-only denial
> - The benefit is a fast, truthful reply that names the setting and
lets the person choose

## Linked Issues or Issue Description

**What happened**
In the chat eval cases.cases-disabled ("Open a customer case for this
bug." with Cases off), the agent got 403 "Cases are disabled". In one
run it then created a project and a bug task without asking. In a later
run it searched many routes and instance settings for about 100 seconds
before it replied.

**Expected behavior**
The denial says that Cases is off, that the denial is final, where an
instance admin turns it on, and that the agent must not create a
substitute record before the person chooses. The agent replies at once
and offers the setting or an ordinary task.

**Steps to reproduce**
1. Turn off Cases in Experimental settings.
2. As an agent, call GET /api/companies/{companyId}/cases.
3. See a bare 403 "Cases are disabled" with no code or remediation.

## What Changed

- Rebased on #20894, which already gave the case routes a
`cases_disabled` code, a final remediation and the hidden-settings
check. This PR moves that remediation into one helper used by every case
and workflow path, and adds: no substitute task or project before the
person chooses, and the offer of an ordinary task.
- New `casesDisabledError()` in `server/src/services/cases-disabled.ts`:
same 403 message, plus `code: "cases_disabled"` and a remediation. The
remediation says the denial is final, forbids a substitute task or
project before the person chooses, links `[Experimental
settings](/<prefix>/company/settings/instance/experimental)`, and offers
an ordinary task.
- Native tool results carry only the error message, so the native case
tools, workflow tools and runner tool authority inline the remediation
in the message (`casesDisabledError({ inlineRemediation: true })`).
- Case routes, workflow service, case handoffs, native case tools,
native workflow tools and the runner tool authority use the helper
instead of a bare `forbidden("Cases are disabled")`.
- Tests: `cases-routes.test.ts` checks the code and remediation;
`native-case-tools.integration.test.ts` checks the native message
carries the remediation (red on master, green here).

## Verification

- `npx vitest run src/__tests__/cases-routes.test.ts
src/services/case-handoffs.test.ts
src/services/native-runtime/native-case-tools.integration.test.ts
src/__tests__/heartbeat-case-handoff.test.ts` (166 passed).
- The cases.cases-disabled chat eval is rerun on a temporary branch;
results are recorded in the PR conversation.

## Risks

- Low risk. The status and message do not change. The error body adds
`code` and `remediation`.

## Model Used

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

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 13:36:08 -06:00
DottaandClaude Opus 5.5 6dc36700b1 fix(costs): mark unpriced usage in the agent, model and project cost reports (#20889)
## Thinking Path

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

## Linked Issues or Issue Description

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

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

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 13:36:03 -06:00
DottaandClaude Opus 5.5 6154b9485e fix(authz): name the Experimental settings page when an agent is denied an instance experiment (#20894)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - People ask chat agents to turn on instance experiments, such as
Cases
> - Agents cannot read or change experimental instance settings, which
is correct
> - The denial they get names no page, so the agent cannot tell the
person where to act
> - The case API gate ("Cases are disabled") also names no page
> - This pull request makes both denials, and the API catalog entry,
name the Experimental settings page
> - The benefit is that the agent replies with a usable link instead of
a vague "ask an admin"

## Linked Issues or Issue Description

**What happened**
An agent that asks for `GET` or `PATCH
/api/instance/settings/experimental` gets the generic board-only denial.
That denial says "Link a page only if you already know its route; do not
search for one." The case API, while Cases is off, returns only "Cases
are disabled". The agent has no route to give the person.

**Expected behavior**
The denial is final and names the page where an instance admin turns the
feature on: `[Open Experimental
settings](/<prefix>/company/settings/instance/experimental)`. If the
operator hides that page, the denial does not link it.

**Steps to reproduce**
1. Use an agent API key.
2. Send `PATCH /api/instance/settings/experimental` with
`{"enableCases": true}`.
3. See a 403 with a remediation that names no page.
4. With Cases off, send `GET /api/companies/<id>/cases` with the agent
key.
5. See a 403 "Cases are disabled" with no remediation.

Refs the chat eval `instance-operations.instance-settings` (retargeted
from the removed Agent Chat toggle to Cases).

## What Changed

- `server/src/routes/authz.ts`: the board-only denial map links
Experimental settings for `/instance/settings/experimental`. It does not
link it when `instance.experimental` is hidden.
- `server/src/routes/cases.ts`: the Cases gate keeps the "Cases are
disabled" message. It adds `code: cases_disabled` and a final
remediation that names the same page (same hidden-page guard).
- `server/src/routes/openapi.ts`: the GET and PATCH catalog entries for
experimental settings name the page. They say that agent credentials
cannot read or change these settings. They give Cases (`enableCases`) as
an example.
- Regression tests in `board-access-denial.test.ts`,
`cases-routes.test.ts` and `openapi-routes.test.ts`.

## Verification

- `cd server && pnpm exec vitest run
src/__tests__/board-access-denial.test.ts
src/__tests__/openapi-routes.test.ts
src/__tests__/cases-routes.test.ts`: 107 passed.
- Red check: with the three source files reverted, 4 new assertions fail
(GET and PATCH denial links, the catalog entry, the Cases gate).
- `src/services/native-runtime/runner-api.test.ts` and
`src/__tests__/instance-settings-routes.test.ts`: pass.

## Risks

- Low risk. Only error bodies and catalog text change. Status codes and
the "Cases are disabled" message stay the same.
- The catalog text is static, so it names the page even when an operator
hides it. The denials check the hidden setting.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 13:14:42 -06:00
DottaandClaude Opus 5.5 a589b74fbf fix(chat): name goal links, missing-name questions and component reference pages in chat guidance (#20888)
## 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
> - Chat evals showed three gaps in that guidance
> - The agent explained a task's purpose from titles only, because the
guidance gives no goal link and no instruction to read goal descriptions
> - The agent built a full landing page with an invented product name,
because the chat guidance has no rule to ask for a missing name first
> - The agent posted a real question card or drew a text mock when the
person asked to see a card, because the guidance names the design guide
and ux-lab pages only in general
> - This pull request adds three short rules to the chat guidance
> - The benefit is grounded purpose answers with links, one necessary
question instead of placeholder work, and links to the real reference
pages

## Linked Issues or Issue Description

**What happened**
In chat evals (OpenCode runner, DeepSeek flash): (1) "Why are we doing
this website task?" got an answer from titles only, with no goal links
and without the parent goal's stated reason. (2) "Build a landing page
for my new product." produced a full site named with a placeholder; the
agent never asked for the product name. (3) "Show me the standard
question card" made the agent post a real question card. "Show me how
chat looks when a tool request is denied" made the agent draw a text
mock and probe real tool requests.

**Expected behavior**
(1) The agent reads the task, project, goal and parent goals, gives the
reason that the goal descriptions state, and links each record. (2) The
agent asks one free-text question for the missing name and uses the
answer in the brief. (3) The agent links the design guide question card
or the denial ux-lab page and says they are synthetic presentation
fixtures.

**Steps to reproduce**
1. Run the chat evals goals.explain-purpose,
questions-and-approvals.necessary-question,
developer-labs.component-reference and
developer-labs.interaction-fixture on master.
2. Read the reply and tool events of each run.

## What Changed

- Chat guidance: goal link format `/<prefix>/goals/<goalId>`, and a rule
to explain a task's purpose from its saved goal chain (GET
/api/goals/{id}, follow parentId), link every record, and say so when
the task has no goal.
- Chat guidance: ask for a missing name (for example the product name on
a landing page) with `request_human_input` (`questions`, one required
free-text question) before building or delegating; never use a
placeholder name.
- Chat guidance: when the person asks to see a component or chat state,
link the design guide (question card states) or the named `/ux-lab/...`
pages instead of creating a real card or a mock, and say they are
synthetic fixtures.
- Tests: new red/green tests in `agent-conversations.test.ts`; they also
check that the named UI routes exist in `ui/src/App.tsx`.

## Verification

- `npx vitest run src/__tests__/agent-conversations.test.ts` (62
passed); the new tests fail on master because the text is absent.
- `npx vitest run` for heartbeat-context-summary,
heartbeat-chat-task-link, board-access-denial, task-markdown,
current-wake-comments and heartbeat-list tests (119 passed).
- The affected chat evals are rerun on a temporary branch; results are
recorded in the PR conversation.

## Risks

- Low risk. Guidance text only. The chat directive grows by about 1.6
KB.
- The missing-name rule is narrow (a name the work must carry). It must
not cause extra questions for clear requests.

## Model Used

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

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 13:12:52 -06:00
DottaandClaude Opus 5.5 c02de922af fix(chat): deliver the person's time zone to chat agents (#20898)
## Thinking Path

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

## Linked Issues or Issue Description

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

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

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

## What Changed

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

## Verification

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

## Risks

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

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

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 13:11:57 -06:00
DottaandClaude Opus 5.5 683fd1e9a5 test(restart-chaos): cells for staging's no-restart failures (re-wake, Nth recovery step, long turn, K15 without volume) (#20893)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart-chaos harness (`tests/restart-chaos/`) tests that agent
work survives control-plane faults
> - In one 8-hour staging window, 20 of 25 failed runs happened with no
restart at all, so the harness did not see them
> - The harness had blind spots: one short task per agent, no wake right
after a successful run, no fault in a later recovery step, short turns,
and a redeploy that always keeps its instance home
> - This pull request adds cells for those shapes, so the harness
reproduces these failure classes on master
> - The benefit is that fixes for these classes can be proven red and
green with the harness, without staging

## Linked Issues or Issue Description

**What happened?**
On staging, an agent ran 10–13 tasks at once on reused sandboxes. 30–60
s after a long run succeeded, an ordinary wake (a comment or an issue
monitor) started the next run of the same task. That run or its recovery
failed before any provider work, with errors such as
`runner_remote_process_identity_unavailable`, `Controller journal drain
timed out` and `journal_tool_input_unanswered`. Other failures happened
in a later recovery step (a held agent-directory lock, a missing runtime
asset after a deploy without a volume).

**Expected behavior**
The chaos harness can reproduce each of these shapes on demand, and
grades them: no failed attempt, every task `done`, and no task left
blocked without a working Retry.

**Steps to reproduce**
See the commands in the README sections "Re-wake after success", "Fail
the Nth recovery step", "Long turn" and "Redeploy without a volume, with
overlap".

Related: #20844 (session fault SR1). W2C is the configuration change
after a successful run; SR1 is the same change while a question is
parked. This PR does not add or change `session-faults.ts`.

## What Changed

- New workloads W2, W2Q, W2C (`rewake.ts`): phase 1 ends with the task
left open; the harness, as the board, wakes the same task the seeded
0–60 s after its run settled. W2 uses a comment or a board-scheduled
issue monitor (by nonce). W2Q parks on an unanswered
`request_human_input` question and is woken by a comment before the
answer. W2C changes the agent's `adapterConfig.idleTimeoutMs` before the
wake. Oracle: phase 1 succeeded, the wake was served by a run that
succeeded, `no_failed_attempt`, `done`, markers.
- `--faults K0`: cells with no fault, for workloads whose shape is the
fault.
- `--fail-recovery-step <step>[:<nth>]` (`recovery-step-faults.ts`):
when the wave's first restart fault starts its replacement, the harness
arms one crash point in a new fail mode; the nth hit throws the error of
the real step, once. Steps: `runnerd-start` (`spawn paperclip-runnerd
EAGAIN`), `teardown` (provider destroy did not finish), `directory-lock`
(`Agent directory operation is already active`). Crossed tasks get
`recovery_step_failed_once` and `no_failed_attempt`.
- Crash points (`packages/shared/src/crash-points.ts`): fail mode in the
control file (`<token> <name>:<nth> fail [pid=<n>]`), with an optional
call-site error; `pid=` binds the line to one server process. The
harness arms the step for the replacement only and disarms it once its
receipt lands. New point
`agent_directory.operation_lock.before_acquire`. The teardown point
moved into its `try`, so fail mode behaves like a failed provider
destroy; kill mode is unchanged. All points stay disarmed under
`NODE_ENV=production`. The runner package's own crash-point copy is not
changed (runner image content ID unchanged).
- `--long-turn-minutes <n>` (`long-turn.ts`, 15–60, opt-in): every
task's first turn runs rounds of real tool work for n minutes before its
workload. Checks `long_turn_rounds` and `long_turn_run_observed`.
- K15 `--k15-fresh-home` (Daytona) and `--k15-overlap-seconds
<min>-<max>` (`redeploy.ts`, `supervisor.ts`, `edge-proxy.ts`): each
deploy generation gets a new, empty instance home and no volume
variables; the next generation boots beside the old one behind an edge
proxy on the wave's port, takes traffic once ready, and the old one gets
SIGTERM after the overlap.
- `tests/runner-e2e/source-sdk-public-inputs.json`: refreshed pins for
the changed shared crash-point files.
- README: one section per new cell, with what it covers.

## Verification

- `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts`
(48 files, 581 tests after rebasing on master, includes the new
`shape-cells.test.ts`).
- `pnpm exec vitest run packages/shared/src/crash-points.test.ts
server/src/__tests__/crash-point-call-sites.test.ts` (fail mode, call
sites).
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`.
- Chaos waves on master `1d67e309e0` server code (Daytona image
`ghcr.io/paperclipai/paperclip-daytona-runner@sha256:99fc2671fc320816407f63cffd3acbd27471621febcd26494fe25bb5d7c5dd77`,
content ID `1836c515…`), all RED unless noted:

| Cell | Wave | Result on master | Staging class |
| --- | --- | --- | --- |
| W2/W2Q/W2C, 10 tasks on one Codex agent, `--daytona-warm`, K0 |
`w-20261111T163206-s5201` | 9/10 fail. 3 tasks (W2 t02, W2C t03, t06):
first fault `native_runner_process_exited`, same-run reattach,
`native.remote_runner_recovery_failed` with `stage:
process_verification`, `reason: process_not_running`, `runnerLifecycle:
ready`, `runnerSettled: false`, terminal
`runner_remote_process_identity_unavailable`, "Workspace repair
required". 3 tasks (t04, t08, t10): `native_restart_recovery_blocked`
(`native_retry_source_superseded`) while its successor ran (R4
`concurrent_runs`). 3 W2Q tasks: the answer's continuation was cancelled
`issue_not_in_progress` (status `blocked`), and the task needed Retry. 2
W2C tasks then looped: 60+ succeeded runs in 20 minutes on an
`in_progress` task. | D′/T terminal step reproduced with no restart, no
governed wait and no PRP timeout (same error code and refusal fields).
C+ retry outcome (ownership ambiguous) reproduced as
`native_restart_recovery_blocked`. Not reproduced: `Controller journal
drain timed out`, `did not durably suspend`,
`journal_tool_input_unanswered`. |
| Same, local `--lifecycle warm` | `w-20261111T160103-s5101` | 4/10
fail. 3/3 W2Q: continuation cancelled `issue_not_in_progress`, Retry
needed. 1 W2C: the same `in_progress` run loop (39+ runs). | The
remote-runner classes need a remote runner; locally they do not show. |
| `--fail-recovery-step runnerd-start:1`, K1 at 10 s, local A1 |
`w-20261111T162432-s5303` | Fail. After K1 the first runnerd start of
recovery failed (`spawn paperclip-runnerd EAGAIN`); the run ended
`native_provider_terminal_failed` ("previous turn is no longer active",
"Automatic recovery stopped") and needed Retry. Control, same seed and
K1 without the step fault (`w-20261111T171114-s5303`): pass. | A later
recovery step that fails once ends the run instead of trying again (the
A-chain shape). |
| `runnerd-start:2`, local | `w-20261111T170909-s5304` | Step never
reached: recovery started the runner once and succeeded. Grade fails
`recovery_step_failed_once` (tested nothing). | — |
| `teardown:1`, K1, Daytona A1 | `w-20261111T172048-s5312` | Pass. The
teardown failed once and the sweep settled it. | Not reproduced. |
| `directory-lock:1`, K2, Daytona A1 | `w-20261111T171247-s5311` | Step
never reached by A1 after K2. | Not reproduced; A1 recovery does not
collect remote agent files. |
| K15 `--k15-fresh-home --k15-overlap-seconds 30-150`, `--storage s3`,
Codex and OpenCode, T1/C1 | `w-20261111T173618-s5322` | 1/4 fail
(OpenCode T1): `native_continuation_requires_reconciliation` with "Agent
directory belongs to a different execution environment"; the Retry runs
failed `adapter_failed` "Configured instruction entry is missing"; the
task rests `blocked` on a board recovery action, and two sandboxes were
left (R6). One Codex T1 task finished before its fault
(`fault_crossed_tool`). | A-deploy: a controller-local file missing
after a deploy without a volume. Here the missing file is the agent's
instruction bundle, not a runtime-context asset; whether staging keeps
instruction bundles outside the instance home is not verified. |
| `--long-turn-minutes 15`, K1 at 600 s, local A1 |
`w-20261111T175505-s5401` | Task pass (10/10 rounds, 992 s span, run
1019 s, K1 crossed mid-turn). The wave exited 2 only because I switched
branches in the worktree during the wave. | Shape check only. |


## Risks

- Product code changes are test instrumentation only: three `crashPoint`
call sites take a fail-mode error, and one new call site was added.
Disarmed (always in production) each call is one comparison. Moving the
teardown point into its `try` does not change kill mode: the process
dies at the same instant.
- The new cells are opt-in. Default waves plan the same tasks as before
for the same seed.
- The W2 cells depend on model behaviour (phase 1 leaves the task open).
The oracle records the task status at the wake.
- `--k15-fresh-home` drops everything under the instance home. Whether
staging keeps agent instruction bundles outside the instance home is not
verified (see the K15 result).

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, agentic coding with
tool use (shell, file edits, test and chaos runs) 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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 12:58:41 -06:00
DottaandClaude Opus 5.5 b96967a97b fix(authz): name the environment settings page when an agent is denied an environment route (#20880)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Execution environments are the computers that agents run on. Only a
board user can list, inspect, test or change them.
> - An agent in chat can be asked "Which computer can run Morgan with
Claude Code?". Its first environment read returns 403.
> - The 403 says the denial is final, but it names no page. The agent
cannot link the person to the control that answers the question.
> - Other board-only areas (secrets, plugins, budgets) already name
their page in the denial.
> - This pull request adds the environment settings page to the agent
denial for every environment route, and says that testing and changing
computers are board-only too.
> - The benefit is that the agent can send the person straight to the
correct control, and it does not search for a probe route that does not
exist.

## Linked Issues or Issue Description

**What happened**

An agent asked which computer can run a coding harness called the
environment list or capabilities route. The route returned 403 "Board
access required" with a remediation that named no page ("Link a page
only if you already know its route; do not search for one"). The agent
had no route to give the person.

**Expected behavior**

The denial names the environment settings page as a Markdown link with
the `<prefix>` placeholder, like the denials for secrets and plugins. It
also says that the other environment steps (capabilities, probes,
changes) are board-only, so the agent replies instead of trying them.
When an operator hides environment settings
(`PAPERCLIP_HIDDEN_SETTINGS=instance.environments`), the denial does not
link the page.

**Steps to reproduce**

1. Use an agent API key.
2. Call `GET /api/companies/<companyId>/environments/capabilities`.
3. Read the 403 body. Before this change, `remediation` contains no
link.

## What Changed

- `server/src/routes/authz.ts`: board-only denials on
`/companies/:id/environments...`, `/environments/...` and
`/environment-leases/...` now link `[Open environment
settings](/<prefix>/company/settings/instance/environments)` and add one
sentence that says execution environments are board-only throughout. The
link is left out when the operator hides `instance.environments`.
`/companies/:id/project-environments` (agent-readable) is not matched.
- `server/src/__tests__/board-access-denial.test.ts`: regression tests
for the list, capabilities, probe and lease routes, the scope sentence,
the project-environments exclusion, and the hidden-settings case.

## Verification

- `cd server && pnpm exec vitest run
src/__tests__/board-access-denial.test.ts`: 33 passed. With the old
`authz.ts`, 5 of the new tests fail (red), and they pass with the change
(green).
- `cd server && pnpm exec vitest run
src/__tests__/environment-routes.test.ts
src/__tests__/agent-permissions-routes.test.ts`: 99 and 126 passed.

## Risks

- Low risk. Only the agent-facing 403 remediation text changes. Status
codes, the error code and board behavior do not change.
- The scope sentence is environment-specific, so other denials are
unchanged.

## 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 12:49:19 -06:00
Nicky LeachandPaperclip 5dfbb04b85 Poll asynchronous workspace provider preparation (#20865)
## Thinking Path

> - Paperclip starts agent runs through a workspace provider when a
project uses remote storage.
> - The provider must prepare the workspace before the agent starts.
> - One long worker call ties the checkout to its transport.
> - Some checkouts take longer than the transport allows.
> - This change lets an opted-in provider accept work and report its
deadline.
> - Core waits for completion through short status calls and stops at
the provider deadline.

## Linked Issues or Issue Description

**What happened?**

A workspace provider can lose its reply when preparation takes longer
than a single transport request. This stops run bootstrap before agent
execution.

**Expected behavior**

Core should wait for a provider-confirmed workspace through short
requests. The provider's deadline must bound the wait.

**Steps to reproduce**

Use a workspace provider that takes longer to prepare a checkout than
its transport can remain open. Start a run that requires a new checkout.

## What Changed

- Add an optional workspace preparation method to the plugin SDK.
- Validate pending receipts and keep the original binding checks on
completed replies.
- Poll only during active preparation. Retry a lost status reply within
the advertised deadline.
- Preserve the original operation for providers that do not offer
asynchronous preparation.
- Reject deadline offers that exceed the existing lifecycle lock budget.
Honor accepted offers without shortening them.
- Checkout verification uses the existing synchronous RPC with a
10-second deadline and a 12-second transport timeout. [Core verification
PR #20876](https://github.com/paperclipai/paperclip/pull/20876)

## Verification

- All 63 workspace SDK and Core integration tests passed with temporary
local PostgreSQL. The tests cover lost status replies, deadline expiry,
oversized offers, and synchronous verification with an async-capable
worker.
- All 58 runner SDK input, evidence, and public type tests passed with
the regenerated source pins. The local server typecheck passed.

## Risks

- This change activates only when the provider advertises the method.
Existing providers keep their current behavior.
- Polling uses one-second intervals during active preparation. It stops
at completion, failure, or the provider deadline.
- No database migration is required.

## Model Used

- OpenAI GPT-6 Codex with local code execution and tests.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-11 10:33:02 -08:00