Commit Graph
23 Commits
Author SHA1 Message Date
DottaandClaude Opus 5.5 8a4eaab2c0 fix(heartbeat): deliver a person's answer that waited behind a turn that closed the task
A person can answer an agent's question while the agent's turn is still
running. The answer then waits on its own deferred wake. If that turn
completed the task, release cancelled every deferred assignee wake on the
done task, including the answer, even though the person's own queued
messages reopen the task. The answer was dropped without any run receiving it.

Treat an undelivered person's answer that was saved before the task was
completed as owed input: reopen the done task straight to in_progress (an
answer continues the assignee's own execution) and promote the answer wake.
Answers saved after completion, tool-action outcomes, already-delivered
answers, agent-requested wakes and cancelled tasks keep the old behavior.

Eval steering.approval-delivery (oracle v7): also accept Acorn work that the
worker's settling turn saved after the answer was saved, while still
requiring exactly one later turn bound to the answer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 19:42:20 -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 b93df4ca38 fix(chat): consume a saved chat wake whose message a completed run answered (#20689)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - External chat (for example GitHub review requests) wakes the
assigned agent with a saved chat message
> - When the agent is already running, the chat wake is saved behind
that run and released after it finishes
> - The running agent often reads the new message as part of its current
turn, so the message is already answered when the saved wake is released
> - The release kept that chat wake anyway, removed its now-delivered
message ids, and started a reviewed chat run with no message
> - A reviewed chat run with no message cannot be attested, so it failed
with `reviewed_chat_execution_binding_not_authorized` and the task
became blocked
> - This pull request treats a chat message wake like a comment wake:
when a completed run already received all its messages, the saved wake
is consumed
> - The benefit is that a message that arrives during an active run no
longer blocks the task after it was answered

## Linked Issues or Issue Description

**What happened?**
A GitHub chat message arrived for a task while the assigned agent's run
was active. The wake was saved behind that run. The run read the message
and answered it. When the run finished, the saved chat wake was still
promoted. Its message ids were removed because a completed run already
received them. The new run had source `chat:<provider>` and no message,
so reviewed chat attestation denied it with `context_mismatch`. The run
failed with `reviewed_chat_execution_binding_not_authorized`.
Review-path recovery inherited the same context and failed too, and the
task was blocked.

**Steps to reproduce**
1. Assign a task in review to an agent that is running.
2. Deliver an external chat message for the task. The wake is saved
behind the active run.
3. Let the active run receive the message as wake input and succeed.
4. Release the task's execution lock.
5. See that the saved chat wake is promoted with no message, instead of
being consumed.

**Expected behavior**
The saved chat wake is consumed with "Queued messages were already
delivered to a completed run". No new run starts. The chat does not get
a "not started" notice for a message that was answered.

**Paperclip version or commit**
`origin/master` at 6e524d8a69.

**Additional context**
This was seen on a staging deployment on five runs across three tasks in
one day. It is a message timing race, not a deploy or authorization
problem.

## What Changed

- `server/src/modules/wake-queue/adapters/postgres.ts`: a saved wake
whose reason is `External chat message received` and whose payload
mutation is `chat_message_received` is treated as message-only. If a
completed run already received all of its messages, the release consumes
it, the same as a comment wake. If any message is still unread, it is
promoted as before.
- `server/src/services/chat-inbound-wakeup-publications.ts`: a chat
receipt consumed for that reason does not publish "This follow-up was
not started". Other declines keep that notice.
- `server/src/services/queued-comment-delivery.ts`: constants for the
chat message wake reason and the delivered-message reason.
- New test
`server/src/modules/wake-queue/adapters/chat-wake-delivered.test.ts`.

## Verification

- `cd server && npx vitest run
src/modules/wake-queue/adapters/chat-wake-delivered.test.ts`
- RED on `origin/master`: the release promotes the consumed chat wake
("Unexpected promotion of a consumed chat wake"), and the receipt
resolves to a `not_started` notice.
- GREEN with this change: the wake is cancelled with the delivered
reason, no run starts, no "not started" notice, and a chat wake with an
unread message is still promoted.
- Related suites: `src/modules/wake-queue`,
`heartbeat-reviewed-chat-binding.integration.test.ts`,
`durable-chat-wakeup.test.ts`,
`heartbeat-native-status-context.test.ts`,
`external-chat-wait.integration.test.ts`,
`heartbeat-held-comment-wake.test.ts`.
- `npx tsc --noEmit -p server` passes.

## Risks

- A chat wake that merged with another kind of wake takes the merged
wake's reason and mutation. Only a wake whose current reason and
mutation are both the chat message values is treated as message-only, so
other merged intents keep promotion.
- A visible "Your follow-up is queued." notice is not followed by a "not
started" notice when the message was already answered. That matches the
existing behavior when the wake is promoted.
- Not changed: a separate case where messages from two different chat
conversations are merged into one saved wake. That case fails with
`binding_denied` and needs its own change.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, through Claude Code
with tool use (shell, file edit, read-only API access). Extended
reasoning was used.

## Checklist

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-11-10 20:15:13 -06:00
DottaandClaude Opus 5.5 e620ce3678 fix(heartbeat): deliver each accepted follow-up exactly once through restarts and admission holds (#19741)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A user can comment, click Retry, or rely on automatic recovery while
an agent works on a task
> - Restart-chaos waves showed accepted follow-ups being dropped, or
delivered twice, around restarts and temporary company admission holds
(accounting still settling after a restart)
> - A dropped comment means the user's request is never acted on. A
duplicate reopens work that is already done
> - This pull request makes accepted work wait through a temporary hold
and resume when it clears. It also stops a comment from being promoted
again after a completed run already received it
> - The benefit is that each accepted follow-up is delivered exactly
once, eventually, unless the task is cancelled or a reached hard stop
applies

## Linked Issues or Issue Description

Refs #19702 (never-started tasks during a temporary hold), #19700
(bounded accounting hold), #14910 (stale reopen from a deferred wake; no
file overlap).

**What happened?**
Restart-chaos waves found these shapes:
1. **Comment wake cancelled.** A comment was saved behind a run and
promoted when the run ended. A temporary company hold refused the
unstarted queued run, and claim cancelled it terminally. The recovery
wake that followed was cancelled the same way, and the task rested
`blocked` without the comment.
2. **Dead Retry.** A Retry click during the hold stayed
`deferred_issue_execution` after the hold cleared. Nothing woke it, and
the issue held no execution lock.
3. **Refused assignment.** A new task's first wake was refused during
the hold. Nothing woke stranded-work recovery when the hold cleared, so
the task stayed `todo` with no run.
4. **Silent comment after recovery.** A comment was saved behind a run
that restart recovery finalized. A company-scoped hint waited out the 30
s deferred-receipt rescan, so the task looked done with the message
unread.
5. **Comment delivered twice.** A run whose provider process was lost
had a saved comment. Its retry started after the comment, received it in
its prompt (task history), did the work, and finished the task. The
saved wake was then promoted anyway, which reopened the done task.

**Expected behavior**
An accepted follow-up (comment, Retry, recovery) is delivered exactly
once, eventually, unless the task is cancelled. A temporary refusal
never ends in a terminal cancel, skip, or block. Reaching a hard stop
remains the board's decision.

## What Changed

- `heartbeat/queue.ts`: on a budget block at claim, an unstarted queued
run stays queued when the block is temporary. A reached hard stop or
blocked unpriced usage still cancels it.
- `recovery/service.ts`: stranded-work recovery waits out a temporary
hold for any task. It does not escalate to `blocked`. This extends
#19702 beyond never-started tasks.
- `budget-block-ownership.ts`: a shared `isBoardOwnedBudgetBlock`, moved
out of the recovery service. `budgets.ts` is unchanged.
- `run-cost-accounting.ts`: settling a run's accounting wakes queued-run
dispatch, the deferred-receipt worker, and stranded-work recovery. Held
work waits passively; this is what resumes it.
- `deferred-execution-notifications.ts`: new
`notifyDeferredWakesForIssue`. `native-run-projection.ts` (a run
reaching a terminal status) and `environments.ts` (lease release) now
hint each saved wake of the issue by id, so delivery does not wait out
the company rescan interval.
- `wake-queue/adapters/postgres.ts`: release treats a saved comment as
delivered only when a succeeded run of the wake's agent on that issue
started after the comment's latest revision, and either received the
comment as its own wake input (`wakeCommentIds`) or was a retry of the
interrupted turn whose task history included it. In that case it cancels
the saved wake with "Queued messages were already delivered to a
completed run" and leaves the task as it is. Task history alone never
consumes a message saved for its own turn. One query checks the whole
batch.
- `run-cost-accounting.ts`: settling late receipt debt also sends the
hold-release notifications.
- `doc/execution-semantics.md`: documents the rule.

## Verification

Red on `master` and green with this change, on embedded Postgres:
- `server/src/__tests__/heartbeat-held-comment-wake.test.ts`:
- A promoted comment refused by the hold stays pending, with no
cancelled run and no `blocked` task. It is delivered once after the hold
clears through real `accountRunCost`.
- A Retry clicked during the hold starts exactly once after accounting
settles, which wakes its owner.
- A new task whose assignment was refused during the hold starts once
accounting settles, which wakes stranded-work recovery.
- A comment that a later succeeded retry already received as history, or
that a later run received as wake input, is consumed without reopening
the task.
- Guard tests (green on both):
- A comment is still delivered in each of these cases: the later run
never received it, it was edited after that run started, it was posted
while the run was already running, or a non-retry run only saw it as
history.
  - A task whose continuation was refused waits out the hold.
  - A reached hard stop is still refused.
- `server/src/__tests__/native-run-projection-work.test.ts`: projecting
a recovered run terminal hints the saved comment's own wake (red on
`master`).
- Related suites pass locally (14 files, 482 tests): wake-queue, queued
comments routes, refused assignment wake, native run projection.
- Live replays (local, unmasked, kept instances):
- `--replay w-20261107T031005-s7501/t04` (Claude C1, K1): PASS. The
comment's run was created at 03:56:24 during the hold, stayed queued,
started at 04:06:24 when the hold cleared, and succeeded. It is the only
run carrying the comment.
- `--replay w-20261107T002911-s7401/t01` (before the per-wake hint): it
reproduced shape 4. Run 592d772f was killed 250 ms after
`run.result.accepted`, released and finalized at restart, and the task
rested `done` with the comment still saved. Calling the existing sweep
on the kept database promoted it, which confirmed that the missing piece
was the trigger.

## Risks

- A queued run can now sit queued for the length of a temporary hold,
which #19700 bounds. It does not hold the issue's execution lock, so
later comments still coalesce into it.
- The delivered-comment rule depends on the run's recorded prompt
context. A succeeded run that received the comment in its history counts
as delivery, even if the agent did not act on it. This matches
exactly-once delivery semantics.
- Settling accounting now emits three extra notifications per run. They
are coalesced per company.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, run as an agent in
Claude Code with tool use (shell, file editing, local test execution,
live restart-chaos replays).

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 00:24:09 -06:00
DottaandClaude Opus 5.5 002455ea06 fix(issues): finish a reassignment whose native stop settles after the request (#19354)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A user can stop or interrupt a running native task and then send a
message to give new direction
> - When the stopped run left a workspace hold, the dispatch gate
withholds the follow-up run until recovery saves the work
> - The gate must save the user's message on its receipt so that the
deferred-receipt worker continues it after recovery
> - Two conditions made the gate skip the message instead of saving it,
so the task stayed in progress with "Saved work recovered. Send a
message to continue." although the user already sent one
> - This pull request saves the message in both conditions
> - The benefit is that the message the user already sent continues
once, automatically, after recovery

## Linked Issues or Issue Description

**What happened?**

After a user stopped or interrupted a native task with an isolated
sandbox workspace and sent a message, the follow-up run was cancelled
with `execution_reconciliation_required`. Its error said "Saved work
recovered. Send a message to continue." Nothing re-woke the task. In the
interrupt case, a periodic sweep started a run about four minutes later.
In the case where the follow-up first waited for the stopped run's
workspace save (`workspace_busy`), each later retry parked on the same
resolved recovery action, and the task did not continue.

**Expected behavior**
When the recovery that the follow-up waited on resolves, the message
that the user already sent continues once, automatically. The product
does not ask for a second message.

**Steps to reproduce**

1. Run a native task in a managed sandbox with an isolated workspace.
2. Stop the run (or send a comment with interrupt), then send a message
while the stop and workspace save are still settling.
3. Wait for the workspace recovery to resolve.
4. See that the follow-up run is cancelled with
`execution_reconciliation_required` and that no run continues the
message.

**Paperclip version or commit**
`master` before this change.

**Root cause**
`deferUnstartedUserComment` (run dispatch) refused to save the withheld
message in two cases:
1. It compared the message time with the source run's `finishedAt`. A
native run finishes only after its provider stop and workspace save
settle, which takes seconds. A message written after the Stop request
but before that settlement was treated as input written before the Stop.
The receipt was marked `skipped`.
2. It required the run's own receipt to be user-owned. A workspace-busy
retry or an admission retry carries the user's message under a
system-owned receipt, so the message was dropped with the retry.

## What Changed

- Add `userStopBoundaryAt(run)` in `run-cancellation.ts`. For a run
cancelled by a user Stop or interrupt, it returns the recorded Stop
request time (`cancellation.recordedAt`) when that is earlier than
`finishedAt`. For all other runs it returns `finishedAt`.
- Use this boundary in the dispatch gate's message deferral, in native
explicit-continuation admission, and in the remote-Stop continuation
check, so the three places agree on "message written after the Stop".
- When the withheld run is a system-owned retry, walk a bounded chain
(max 8) of retries that did not reach the provider (`workspace_busy`
with `providerWorkStarted: false`, or a verified pre-dispatch
`execution_reconciliation_required`) to the originating user receipt.
Save the message on that receipt with the user's original wake context.
Mark the retry's system receipt `skipped`.
- Update the "input written before the Stop" test so that the Stop
request, and not only the settlement, comes after the message.

## Verification

- New regressions in `server/src/services/workspace-recovery.test.ts`
(`continues a message ... without a second message, after recovery
already resolved`): a direct follow-up and a workspace-busy retry. Each
runs recovery to completion first, then lets the gate withhold the
follow-up, then runs only the deferred-receipt worker. Each asserts that
exactly one successor run is queued with the user's message and the
user's authority, and that no provider work starts.
- Red before the fix: both tests fail because the receipt is `skipped`
instead of `deferred_issue_execution`. Green after the fix.
- `npx vitest run src/services/workspace-recovery.test.ts` (384 passed).
- `npx vitest run src/services/explicit-native-continuation.test.ts
src/modules/run-dispatch src/services/execution-continuation.test.ts
src/services/run-cancellation.test.ts` (581 passed).
- 19 more heartbeat, retry, queued-comment, and native recovery suites
(1272 passed). One failure, `heartbeat-process-recovery > redacts opaque
environment-bound credentials from Sentry diagnostics: result`, also
fails on unchanged `master` and is not related.

## Risks

- A message written after the Stop request but before the stopped run
settled now continues. The stopping run cannot consume new input after
its Stop request, and admission still removes comments that a run
already delivered (`undeliveredLegacyUserCommentIds`). All admission
gates (holds, stop proofs, approvals, budget) still apply.
- Only user Stops and interrupts move the boundary. Other cancellations
keep `finishedAt`.
- The retry walk is bounded, stays in one company, agent, and issue, and
accepts only retries without provider work. It revives a user receipt
only from `cancelled` or `skipped`, so a second retry cannot save the
same receipt twice.
- Not in scope: a run that fails during native preparation with no user
message (for example `runner_state_identity_mismatch:
prior_owner_terminal_state_indeterminate;
retirement_refused=unexpected_root_entry` on a system wake). After its
recovery resolves, the task still waits for a user message. That is a
separate root cause in the native session executor's prior-owner
retirement proof.

## Model Used

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

## Checklist

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

> - Paperclip manages agents and their work.
> - The wake queue records task comments before an agent starts.
> - A queued run can accept another comment through coalescing.
> - Claim rebuilds that run's input from its primary wake.
> - Coalescing updated the run and its receipt but left the primary
batch unchanged.
> - This change saves the accepted batch atomically in the owning
writer.
> - The benefit is complete input at claim without repeating old work.

## Linked Issues or Issue Description

Related: #18887 preserves offline queued turns. #13948 addresses
equivalent automatic wake deduplication. This change repairs the
accepted comment batch at the existing claim boundary.

**What happened?**

A queued run started with comment A. Admission accepted comment B, saved
`[A, B]` in the run context, and finished B's coalesced receipt. The
primary wake still held `[A]`. Actual claim rebuilt input from that wake
and dropped B.

**Expected behavior**

Claim must retain every accepted live comment in order. Stop, changed
source authority, deleted comments, and company boundaries must still
apply. Already claimed wakes must retain their existing recovery source.

**Steps to reproduce**

1. Queue a task run whose primary wake has an authoritative comment
batch `[A]`.
2. Admit comment B through `createHeartbeatQueue.enqueueWakeup` and the
real Postgres admission writer.
3. Confirm that admission accepts `[A, B]` and records B's coalesced
receipt.
4. Claim the original queued run. Before this fix, the claimed input
contains only A.

**Paperclip version or commit**

The untouched producer regression fails on
`d7283e875772ca8c684dc86e471e6b48d543b7b1` with both process and native
Runner/Codex adapter selection. Provider execution is disabled in the
fixture.

## What Changed

- Save the accepted comment batch in the unclaimed primary wake, run
context, and coalesced receipt in one transaction.
- Lock the primary wake before the run. Recheck the expected run status
and same-company, same-agent and run bindings; apply the existing actor
equality fence only to durable receipts before mutation.
- Preserve incoming input as normal deferred work when the locked source
changed. Preserve receipt identity and actor fields.
- Preserve the original primary actor and trigger comment while updating
its accepted batch. Each incoming receipt retains its actual actor. Keep
claimed source payloads and legacy envelopes on their existing recovery
paths.
- Safely defer a same-named different agent without rewriting the
original agent's run, wake or receipt. Missing and foreign-company
targets remain rejected. Do not rebuild input from historical receipts.
- Add actual process/native producer, real database rollback, Stop,
deletion, foreign-source, and fallback controls. Update three direct
writer fixtures for the required expected-status contract.

## Verification

- The untouched process and native producer red on
`d7283e875772ca8c684dc86e471e6b48d543b7b1` loses accepted comment B at
actual claim. The initial incomplete mock is retained separately and
excluded from behavioral proof.
- Current process/native matrix: 22 passed, including mixed authors,
system primary, same-named different agents, changed status, claimed
sources, Stop, deletion and foreign-source boundaries. Queue, Postgres
admission and use-case suites passed 234 cases before the
source-preserving serializer follow-up; its 22 actual producer cases
passed again afterward.
- CI on the previous head found a real ordinary mixed-author
workspace-copyback regression. The earlier 13-case selection omitted
this test; that coverage gap is preserved. The first correction exposed
a second real source-provenance/copyback failure. After preserving the
primary trigger and actor while updating only its batch, the complete
workspace recovery file passed all 188 cases, including ordinary
mixed-author input and native copyback.
- Real HTTP queued-comment consumption, discard/reorder and Stop
boundaries: 101 passed before these review corrections. Existing chat
and Slack consumers each passed their selected real writer case.
- Previous-head CI also timed out in the CLI child-server startup
fixture and a ZIP transfer case. The full untouched transfer file passed
17 cases locally. Startup remains unclassified; its intentionally
reserved port is part of the fixture and is not diagnosed as the cause.
No CI retry was issued.
- An earlier queue run passed its 231 assertions but timed out during
teardown; its preserved replay passed all 60 queue cases. This is
distinct from the behavioral reds.
- Exact `ab59d944895774d89f950248e356e93af00f4832` server type layers
and production server/Runner build passed. The unchanged-default
serializer file passed all 11 cases. Current-head CI and both
substantive reviews remain required; previous grades and checks do not
qualify this head.
- No hosted task was retried or changed. Local tests do not establish
any hosted deferred event's cause, provider execution, native
acknowledgement, physical cleanup, or serving revision.

## Risks

- Admission now performs bounded row reads and locks for each coalesced
wake. It adds no idle scan or remote request. The existing task
transaction owns the operation; lock order follows queued claim.
- Claimed-wake recovery uses source identity. Its payload remains
unchanged, and the existing actual native copyback tests cover that
boundary.
- A source that changes before the lock can defer the new input. This
preserves work instead of consuming it into a stale run. No migration or
historical backfill is required.

## Model Used

OpenAI `gpt-6.1-sol`, with reasoning, repository inspection, shell
execution, and test tools. The runtime does not report the
context-window size. A separate OpenAI coordinator reviewed the source.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-03 22:09:58 -06:00
DottaandPaperclip 9ca9af544a Drive remaining live run and resource recovery from durable work (#18419)
Reviewed and verified at 6a4c193906.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-31 16:00:35 -05:00
DottaandPaperclip f5e5b05a33 Preserve native workspace recovery across Stop and sandbox reuse (#18332)
Land the thirteen-file native recovery fix independently from #18245.

Retain verified Stop evidence and queued user comments. Retire stale
preparation ownership atomically and require paired sandbox lease
handoff receipts before workspace recovery.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-30 10:57:53 -05:00
f3af9887ca fix(chat): keep each chat conversation in its own wake batch (#17054)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Chat channels let a linked user talk to an agent from Slack, GitHub
and other platforms. Each external thread is a chat conversation that is
bound to one issue.
> - One issue can have many conversations. A GitHub pull request review
makes one conversation for each inline finding thread.
> - When replies arrive behind an active run, the wake queue merges them
into one deferred wake. The merge did not look at the conversation.
> - A reviewed chat run can attest only one conversation. A merged batch
from two threads always failed attestation with `setup_failed` /
`reviewed_chat_execution_binding_not_authorized`, so no reply got an
answer.
> - This pull request keeps each chat conversation in its own wake
batch. The single-conversation attestation check stays the same.
> - The benefit is that replies in different review threads each get
their own attested run, one after the other.

## Linked Issues or Issue Description

Refs #16793 (related: chat origin during coalescing). No public issue
exists, so the bug report follows.

**What happened?**
A linked user replied in three different inline review-finding threads
of one pull request while the agent's review run was still active. The
three wakes merged into one deferred wake. The promoted run failed in
about 2 seconds with `setup_failed` /
`reviewed_chat_execution_binding_not_authorized` and
`providerWorkStarted=false`. None of the three replies got an answer.
The failed-chat retry cannot recover this batch, because a retry source
holds one conversation.

**Steps to reproduce**
1. Bind two chat conversations (two GitHub finding threads) to one
`in_review` issue.
2. Start a run for the assigned agent on that issue.
3. As the linked user, reply once in each thread.
4. Finish the active run.
5. See one promoted run that fails with
`reviewed_chat_execution_binding_not_authorized`.

**Expected behavior**
Each thread gets its own run. Each run attests its one conversation and
answers its reply.

**Cause**
`findExistingDeferredWake` matched on company, agent, issue and actor
only. The active-run coalesce path had the same gap.
`authorizeChatConversationForBoundRun` correctly denies a batch with
more than one conversation.

## What Changed

- `run-dispatch/domain/wake-context.ts`: add the `chatConversationId`
wake-context key, a reader, and `wakeChatConversationsConflict`. A
conflict means that both wakes have a conversation and the two
conversations are different.
- `issue-assignment-wakeup.ts` and `chat-channels/inbound/wakeups.ts`:
stamp the conversation ID on each inbound chat wake.
- `chat-channels/failed-run-retry.ts`: stamp the conversation ID on
failed-chat retry wakes, so later replies do not merge into a retry from
another conversation.
- `wake-queue` admission: do not coalesce into an active run of another
conversation. The deferred-wake lookup now matches only wakes of the
same conversation or wakes without a conversation.
- `heartbeat/queue.ts`: the same-scope queued or running coalesce target
is skipped when the conversations conflict.
- `doc/execution-semantics.md`: describe per-conversation chat wake
batching.
- Tests: an integration regression in the GitHub review block of
`chat-channels.integration.test.ts`, and unit cases for the active-run
branch in `wake-queue/application/use-cases.test.ts`.

## Verification

- Red before the fix: `pnpm --dir server exec vitest run
src/__tests__/chat-channels.integration.test.ts -t "replies in review
finding threads"`. The main case fails with one run: `{ status:
"failed", errorCode: "setup_failed", error:
"reviewed_chat_execution_binding_not_authorized" }`.
- Green after the fix, same command, 3 of 3 pass:
- Replies in two finding threads give two successive runs. Each run
succeeds, has `paperclipExternalChatExecutionBound: true`, and holds
only its own comment and conversation.
- Two replies in the same thread still give one run with both comments.
- After the linked identity is revoked, each separated run is still
denied (`setup_failed`) and the adapter does not execute.
- `vitest run src/modules/wake-queue src/modules/run-dispatch`: all
pass, with the new cases (different conversation, same conversation, run
without a conversation, non-chat wake).
- Related suites pass: `heartbeat-comment-wake-batching`,
`durable-chat-wakeup`, `chat-failed-run-retry-boundary`,
`issue-queued-comments-routes`, `heartbeat-stale-queue-invalidation`.
- Full `chat-channels.integration.test.ts`: 823 passed, 1 timed out. The
timed-out test (`GitHub terminal receipt composes pinned HTTP and
durable source authority (held_token)`) passes alone with and without
this change. The sandbox has a 4 GB memory cap.
- Not run locally: `pnpm -r typecheck` and `pnpm build`. The server
`tsc` process was killed at the 4 GB sandbox memory cap. CI runs both.
- Runtime dependency note: the deferred-wake lookup adds one JSONB
predicate to the existing deferred-wake query. It reads the same rows.
The query count does not change. The integration regression runs it
against embedded Postgres.

## Risks

- Low risk. No schema change and no migration.
- Behavior change: chat replies from different conversations on one
issue now run one after the other, not in one batch. Before this change,
such a batch always failed, so no working flow becomes slower.
- Wakes without a conversation (non-chat wakes, and wakes queued before
this deploy) keep the old coalescing. A deferred wake from before the
deploy has no conversation key, so one later reply can still merge into
it. That window closes when the queue drains.
- The check keeps the single-conversation attestation in
`authorizeChatConversationForBoundRun`. Revoked identities are still
denied.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), in Claude Code through
the Paperclip agent harness. It used tool use (shell, file editing, test
runs) and extended reasoning.

## Checklist

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

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

---------

Co-authored-by: devinfoley <139239+devinfoley@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 22:47:24 -07:00
Devin FoleyandPaperclip d30648dee8 Preserve accepted messages for paused recipients (#16679)
Keep accepted deferred requests pending while their recipient is paused and deliver through normal gates after resume. Preserve company, task hold, and cleanup checks.

Verification: 247 focused checks, recursive typecheck, the full CI test and build matrix, and Apex 5/5. The local Git fixture initial-branch limitation is documented in the PR.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-17 18:38:17 -07:00
Devin FoleyandPaperclip 391dd15ff2 fix: recover deferred messages and completed unblock waits (#16673)
Recover accepted deferred messages through the latest finished issue run and
recognize exact completed agent unblock notifications as passive idle waits.

Preserve assignment handoffs, pause and Stop, failed outcomes, and cleanup
ownership. Honor local and SSH release contracts and atomically settle cleanup
claims. Add PostgreSQL recovery, concurrency, and interruption regressions.

Validation: recursive typecheck and build pass; 678 focused tests pass; full CI
passes; Apex review is 5/5 with no unresolved findings.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-17 16:53:29 -07:00
8d9d2f9ddf fix(recovery): preserve deliberate stops and dependency waits
Preserve deliberate assignee blocks and operator stops during dependency and run-release recovery. Attribute native waits to their bound owner. Ignore unrelated edits and shared-workspace exports from another issue. Continue after genuine dependency changes while preserving independently authorized user input.

Keep the original contributor's implementation and review history. Add native ownership, unrelated-edit, later readiness, and shared-workspace regressions. The shared-workspace loop was reproduced before the fix.

Co-Authored-By: Devin Foley <devin@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-15 00:18:23 -07:00
DottaandPaperclip 83076d7e7c feat: return completed handoffs to Agent Chat (#14408)
Return completed Agent Chat handoffs through a durable outbox and scope each generated update to its supplied tasks. Add recovery, browser delivery, result access, and calibrated quality coverage.

Validated with two consecutive ten-case Claude/Codex campaigns, all CI checks, and a 5/5 review.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-09-29 10:25:52 -05:00
DottaandPaperclip d0b67bfe71 feat: queue approvals and answers during active runs (#13539)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Users guide running agents through messages, questions, and approval
cards.
> - Messages already wait in a queue when an agent is running.
> - Card responses did not appear in that queue. Some question answers
also steered a later run without a user click.
> - A fast approval could invalidate the agent's review handoff and
cause it to stop its own run.
> - This pull request gives card responses the same queue controls and
preserves the exact response during delivery.
> - Users can wait for completion or explicitly send the response with
Interrupt or Steer.

## Linked Issues or Issue Description

Refs #13517, which is merged. This PR targets master and adds queued
interaction responses on top of the onboarding changes. Related
continuation work: #10519 and #12866.

**What happened?**

Accepting a proposal while its source run was active left a saved
response outside the message queue. The agent could then lose its review
path, reassign the task, and cancel itself. Answers to older questions
could also steer another active turn without a click.

**Expected behavior**

Save the response immediately. Queue its continuation behind the active
run. Deliver it after completion, or when the user explicitly chooses
Interrupt or Steer. Preserve approval revisions and answer choices.

**Steps to reproduce**

1. Let an agent publish a confirmation card while its run is still
active.
2. Accept the card before the agent finishes its review handoff.
3. Inspect the message queue and the task's next run.

**Paperclip version or commit**

Reproduced on da8a3876c with the onboarding changes from #13517.

**Deployment mode**

Local development from source. The fix covers legacy adapters and native
Runner turns.

## What Changed

- Project resolved cards into the existing queue as immutable responses.
Keep answers and exact approval revisions.
- Require an explicit click to steer a response into a compatible native
turn. Use Interrupt when a fresh session is required.
- Preserve typed response context through interruption, cleanup waits,
and normal queue promotion. Keep the direct answer channel for a
provider blocked on its original question request.
- Accept the source run's review handoff after its card resolves. Reject
stale agent reassignment that would orphan a queued response.
- Add deterministic regression tests and an `accept-while-running` case
to the first-task suite. Require recorded timestamp overlap before that
case can pass.
- Keep the first-task skill name out of user-facing messages.

## Verification

- Red-green: the original route failed the queue regression; the changed
route passes it.
- Focused server/UI tests: 139 passed, including 64 queue-route tests.
- Runner harness unit tests: 314 passed.
- Server, UI, and Runner E2E typechecks passed. UI token gates passed.
- Full repository typecheck and build passed. Server typecheck passed
again after review fixes.
- Review regressions: 165 queue/reopen route tests, 53 wake admission
tests, and 18 run identity tests passed. Approval acknowledgement
recovery and both message/approval arrival orders are covered.
- Full local test run: 12,401 passed; three new admission regressions
ran against a cached pre-fix module. A fresh run of that entire suite
passed (53 tests). The complete CI suite passed on the final commit.
- Previous-head CI at `c28e2ef12`: 32 checks passed and 2 optional
Storybook checks skipped. Every server/workspace/browser shard, Runner
verification, build, typecheck/release registry, canary, policy, and
security check passed. Greptile: 5/5, no unresolved threads. Earlier
interrupted CI workers were replaced by this fresh complete run.
- After integrating the updated parent: 314 harness tests, 119
queue/admission tests, 44 onboarding/question-delivery tests, and 13
native recovery tests passed locally. Full repository typecheck and
build passed.
- Clarified the skill wording preference: routine replies describe the
action without announcing the internal skill; direct questions and
permission/security/execution disclosures remain truthful.
- The paid `accept-while-running` scenario is registered for all four
local first-task profiles. It has not been run against a model in this
change.

- Rebased onto the merged parent at `11921075a`; the resulting tree
exactly matches the locally verified integration tree. Final-head CI on
`b53054807` passed: 54 successful checks, 2 optional Storybook checks
skipped, no failed checks. Every new server/browser shard, aggregate
verify/e2e gate, Runner, typecheck, build, canary, and security check
passed on the first attempt. Greptile reviewed this exact head at 5/5
with no unresolved threads.

## Risks

- Responses now wait instead of implicitly steering another active turn.
A provider blocked on the original question still receives its answer
directly.
- Approval receipts cannot be edited, discarded, or reordered as
comments. This preserves the recorded decision.
- Interruption must still prove that the prior execution stopped. The
tests cover cleanup waits and duplicate delivery.
- The new paid overlap case can be unexercised if the model finishes
before the click lands. It cannot pass without evidence of overlap.
- No database migration is required. This repairs the existing approvals
and execution controls; it does not implement the roadmap's work-stream
queues.

## Model Used

OpenAI GPT-6 through Codex. The exact deployed model ID and
context-window size were not exposed in this session. Capabilities used:
agentic reasoning, repository inspection, code editing, terminal
commands, and test execution.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-09-16 14:44:36 -05:00
DottaandPaperclip f1d57863d2 fix: make connection checks and task handoffs reliable (#13404)
Preserve connection-probe outcomes through cleanup, reduce unrelated startup work, and report selected Claude authentication accurately. Make artifact download actions match their labels.

Route delegated feedback through its active child, retain accepted messages across completion, and avoid redundant worker runs for proven closing notes. Preserve explicit follow-ups, human input, company boundaries, source provenance, and mixed issue references.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-09-14 10:06:06 -05:00
DottaandPaperclip f2c5e54dca fix(runner): preserve handoff work and publish requested files (#13355)
## Thinking Path

> - Paperclip lets people manage AI agents and their tasks.
> - A task keeps its instructions, progress, and files when its assigned
agent changes.
> - The replacement runner lost the interrupted run's context and could
overwrite an existing draft.
> - A saved message also stayed attached to the former agent and could
reopen the task after the replacement finished.
> - File tasks could report Done with only a local path that the user
could not open.
> - This pull request transfers handoff context and saved messages, and
makes requested files accessible through the existing attachment
contract.
> - Users can change agents and collect completed work without repeating
instructions or confirming bookkeeping.

## Linked Issues or Issue Description

Refs #13338. Builds on merged #13354 for queue admission and #13353 for
remote workspace retry. #10123 concerns restricted recovery-model
escalation; this change instead covers ordinary native handoff and file
completion.

**What happened?**

Codex wrote a draft before a user assigned the task to Claude. The
replacement lacked continuation context and replaced the draft. A queued
user message could later restart the former agent and reopen the
completed task. Separately, a runner could finish a requested file but
return only a machine-local path. Remote native runs had no bound file
publication tool.

**Expected behavior**

The replacement reads and preserves existing work, receives saved
messages once, and keeps each message's author. The former agent stays
stopped. A requested file has a working attachment or accessible work
product before Done. Text-only tasks do not require attachments.

**Steps to reproduce**

1. Ask Codex to save three newsletter names and then wait.
2. Queue an instruction to keep those names and expand the draft.
3. Use Interrupt and assign to select Claude.
4. Verify the original names survive, the result has a working download,
and only the source and replacement runs exist.
5. Ask either provider for a Markdown checklist and open the file from
its completed response.

## What Changed

- Carry the exact same-task interrupted run's summary, semantic
receipts, and history into handoff context. Tell the replacement to
inspect existing files before editing.
- Adopt saved ordinary task comments into the successor's receipt under
the task lock. Preserve authors and separate mention, chat, and
interaction contracts.
- Prevent a former-assignee comment wake from reopening a completed task
or starting a stale execution.
- Reject workspace-only, fabricated, and cross-task file completion
references with actionable runner feedback. New file output also needs a
matching current-run publication receipt and asset filename/size/hash,
or an accessible work product registered by the current run. Prior
output can remain context alongside a current file, or be verified and
re-registered internally. Authorized chat attachment reuse retains its
verified current-run clone receipt; older receipt shapes require an
intact matching source.
- Bind remote file reads to the active environment runner and reuse the
existing attachment and work-product publication path.
- Enforce workspace confinement, regular single-link files, stable
identity, a 10 MiB limit, and exact size and SHA-256 checks. Rotate the
native session fingerprint for the updated tool contract.
- Contain rejected remote signals and protocol-failure cleanup,
including logging failures. Preserve the original cleanup rejection for
its owner; a rejected operation never supplies stop acknowledgement or
cleanup proof.
- Allow exactly one maximum-size base64 file through the native SSH
command adapter, preserving a finite output cap.
- Document handoff and accessible file completion rules.

## Verification

- Each observed bug has a failing regression before its fix. Final
post-rebase integration passed 732 tests across 13 files before the
final receipt and signal guards; final affected results are below.
- Publication provenance and compatibility: 8 provenance regressions and
2 compatibility regressions failed before their fixes; the final four
affected suites pass 53 tests, including mixed old/new references and
real authorized chat reuse. Controls cover old attachments and work
products, filename/size/hash/origin mismatch, missing/wrong receipts,
current-run publication, same-run durable proof, internally
re-registering preserved bytes, and no-new-file follow-ups.
- Remote signal rejection: the real Node subprocess previously exited 1
when the production launcher signalled a deleted sandbox. It now stays
alive for both a failed signal and failed logging; all 349 executor
tests pass. The failed signal still provides no termination proof.
- Remote file reader and SSH command boundary: 33 tests passed,
including real Linux descriptor reads and the actual SSH adapter
subprocess output cap (network executable replaced by a deterministic
fixture). Exact 10 MiB bytes pass, one byte beyond the encoded cap
fails.
- Live local Claude and Codex Stop journeys preserve the saved file,
deliver queued instructions once, and reach Done with two total runs.
The handoff journey preserves the original names and download with
exactly two runs. Both local providers deliver exact checklist files
without a completion confirmation.
- Live combined Daytona verification passed: the original failed task's
Retry reused its sandbox; a selected Git subfolder produced an exact
downloadable file; warm and deliberately resumed Claude runs took about
33 seconds. Codex produced a 240-byte download in 33.1 seconds after 134
seconds of contention/backoff. Both cloud downloads retained exact bytes
after the two owned sandboxes were deleted.
- The sandbox-deletion retest identified a separate ignored promise in
protocol-failure cleanup. Two real Node subprocess regressions failed
under fatal unhandled-rejection policy before the fix; all 36 protocol,
lifecycle, and integrity tests now pass. The original close promise
still rejects to its owning runtime. The final live retest passed: a
normal Claude Daytona task completed in 132.352 seconds, then its
sandbox was deleted. Thirteen samples over 361 seconds confirmed the
same controller stayed healthy, the task stayed Done with unchanged run
IDs, and its attachment retained exact bytes. The post-deletion browser
download passed with zero page errors; all five owned sandboxes are
confirmed absent.
- Full repository typecheck and build passed on final commit
`de64d16f1`. Final-head Greptile is 5/5 with no unresolved threads.
[Final-head
CI](https://github.com/paperclipai/paperclip/actions/runs/34736623758)
passed: 32 successful checks and two conditional skips. The earlier
mixed-source full local test invocation was deliberately stopped before
rebase, so no pristine green full local aggregate is claimed. Its known
failures passed in later affected suites.

## Risks

- Handoff may adopt only ordinary comments from its validated former
owner. Other delivery contracts must remain independent.
- File verification fails closed if a remote file changes during
reading. The runner must retry publication or explain a blocker.
- The updated session fingerprint starts a fresh provider process where
needed to install the new tool contract.
- No schema migration or historical status reconciliation is included.

## Model Used

OpenAI `gpt-6-astra` through Codex, with reasoning, code execution,
browser testing, and tool use. The context-window size is not exposed in
this task.

## 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-09-13 08:41:20 -05:00
DottaandPaperclip df984cbc2c fix: dispatch queued legacy messages with operator identity and task permissions (#13315)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Task conversations save messages that arrive during an active turn.
> - Legacy adapters deliver these messages in a later turn.
> - A run can stop before the saved queue is delivered.
> - The Interrupt button previously required an active run, so it could
not release this queue.
> - This pull request lets a board operator send the saved queue after
the run stops and retries queues missed during finalization.
> - Manual dispatch must use the clicking operator and must not require
permission to create agents.
> - The task can continue without a duplicate message or a second
execution owner.

## Linked Issues or Issue Description

**What happened?**

A legacy task retained a queued message after its run stopped. Interrupt
was disabled because the queue had no active target. Finalization and
deferred message admission can also leave a queue without a successor.

**Expected behavior**

Interrupt sends the saved messages when no runner is active. Messages
that arrive during normal completion are delivered automatically. An
uncertain previous execution still requires proof that its process or
sandbox stopped.

**Steps to reproduce**

1. Queue a user message during a legacy conversation turn.
2. Let the turn stop or simulate a server restart before queue
promotion.
3. Open the task with a deferred queue and no active run.
4. Try Interrupt. Before this change, the button is disabled.

**Paperclip version or commit**

Reproduced against `8f40b4ad4`.

**Deployment mode**

Legacy conversation adapter. The same persisted queue state is covered
with an isolated PostgreSQL fixture.

Related public work: #13275 adds active legacy interruption. #13291
addresses automatic sandbox conversation recovery. This change handles
explicit saved-queue delivery and late queue promotion.

## What Changed

- Accept a null Interrupt target while retaining queue identity,
revision, company, and assignee checks.
- Save the operator's request on the existing queue. Reuse normal
admission after verified stop, including older messages, different
authors, and queues whose original wake came from the system.
- Strip interruption authority from caller-supplied wake payloads. Only
the board queue route can persist that authority.
- Retry durable interruption requests after restart and deferred queues
after legacy cleanup.
- Let an explicit Interrupt retry cleanup for its stopped run, including
old ephemeral leases that recorded success without a provider stop
receipt. Preserve retained resources, other lease owners, and the
automatic retry limit.
- Preserve the server's waiting explanation when normalizing and
combining queue entries.
- Revalidate the consumed board queue receipt at dispatch so a different
message author does not cause setup failure.
- Use the Interrupt user's execution identity for the new run. Preserve
original message authors. Validate the receipt independently at startup
and inherit the resulting identity on retry.
- Persist authenticated board authority for ordinary manual wakes too.
Adopting someone else's queued messages cannot switch a manual run to
that author's permissions. Strip caller-supplied authority markers and
retain private conversation ownership checks.
- Keep the clicking user when a manual wake is merged into an older
deferred receipt. Update its requester and payload in the same
transaction.
- Use the same current-queue/revision API on task details and pipeline
conversations; show Interrupt after a legacy target stops.
- Keep manual wakes out of active runs, including unscoped agent wakes.
They receive their own execution identity; a matching receipt requester
is not sufficient because an exact retry can retain a different
originating identity.
- Authorize both existing-agent wake endpoints with `agent:wake`,
available to active non-viewer company members. Keep `agents:create` for
hiring. Validate the stored task and current assignee before an exact
task retry.
- Reject viewer Interrupt requests before saving intent or stopping
execution. Keep external chat retry authorization and per-action
agent/user permission checks.
- Preserve edits and discards until dispatch. Prevent another queue
promotion when the same agent already has a successor. Keep independent
reviewer recovery available.
- Suppress cancelled/failed run toasts for intentional operator
interruption. Keep ordinary runtime error notices.
- Add UI, route, admission, restart, successor ownership, and toast
regression tests. Document the behavior.
- Reuse the existing socket reservation helper for both
credential-quorum test cases after CI exposed an ambient-port collision.
This changes test preparation only; production credential staging is
still called exactly once.

## Verification

- Failing regression tests reproduced the message-author identity bug
and an operator's `agents:create` rejection before the fixes.
- All 316 focused tests pass across eight route, queue, identity,
authorization, continuation, and responsible-user suites, including the
44-test rerun of queue admission and actual startup after the final
manual-wake restriction. Regressions reproduce cross-user merging both
with and without a task, and same-requester receipt ambiguity. The
cross-company existence guard also passes both tests.
- Startup integration tests reach adapter execution under the clicking
operator and retain that identity through follow-up. Coverage includes
mixed authors, adopted queues, system-origin queues, restarts, forged or
stale receipts, viewers, suspended memberships, changed assignees,
private conversations, and caller-supplied authority markers.
- The earlier queue/cleanup/UI regression suite passed 402 tests. The
final review corrections pass another 180 tests across queue
admission/persistence, real heartbeat startup, UI API, conversation
rendering, and pipeline suites. Regression tests reproduced both review
findings before correction. The final head has a 5/5 review with no
unresolved threads. Full CI passes on `c2002979c`, including every
general and serialized server shard, all browser shards, Paperclip
Runner verification, typecheck, build, canary dry run, and the aggregate
gates.
- Full `pnpm -r typecheck`, `pnpm build`, and UI token gates pass after
the final application changes. CI identified an outdated task-page API
mock after the shared helper extraction; the fixture now exercises the
real helper, and all 131 task-page/API tests pass. The final application
build passes with the additional manual-wake restriction.
- CI exposed a pre-existing port collision in the Codex
credential-quorum fixture. It reproduced locally; both listener cases
now use the existing bounded reservation helper. All 41 credential tests
pass on rerun. One intervening local run hit a separate ambient bind
collision in the two-occupied-port case.
- The full local `pnpm test:run` attempt was stopped after host
contention caused focused-suite timeouts. The affected focused tests
passed on rerun. An expiring trace fixture and a missing
private-conversation state were corrected. The successful full CI run is
the complete-suite verification.
- Hosted Interrupt previously cleared the original queue and produced
exactly one successor with neutral interruption feedback. It exposed the
dispatch authorization defect. Retry on that earlier build was rejected
for missing `agents:create` before creating another run.
- Deployed the final application build (`38257f391`) to the scoped
hosted instance and verified readiness. The latest PR commit changes
only the credential test fixture; application code matches that
deployment. A live Retry by the same operator without `agents:create`
created one successor attributed to that operator, passing the former
dispatch permission gate. Startup then stopped at
`configuration_incomplete` because that operator has not configured
their required personal Claude Code OAuth secret; the post-deployment
run page confirms the operator identity and no provider work started,
and the My secrets UI still shows the token as not set. Provider
execution remains unverified pending that credential. No permission
grants or credentials were changed.

## Risks

Queue admission and finalization can race. The task lock, durable queue
receipt, current comment IDs, and successor guard prevent duplicate
dispatch. Process and lease stop checks, task pauses, approvals,
ownership, and budgets remain in force. The API change only allows null
on legacy Interrupt; native steering still requires an active run. No
schema migration is required. Active non-viewer board members can now
invoke existing agents without agent-creation permission. Agent
self-invocation rules, raw provider-trace admin access, task retry
scope, external chat authorization, and action-specific user/agent
permissions remain enforced.

## Model Used

OpenAI GPT-6 through Codex. The session does not expose an exact backend
model ID or context-window size. Used reasoning, repository search, code
execution, tests, and browser tools.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` 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-09-12 17:11:30 -05:00
DottaandPaperclip ab15aff390 feat: add experimental persistent agent chat (#13284)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Conversations must use the same tasks, controls, and execution
history.
> - Users need an ongoing chat with an agent without managing task
properties.
> - Agents should clarify and plan work, then hand execution to assigned
project tasks.
> - This pull request combines the reviewed Agent Chat stack for one
squash merge.
> - The benefit is persistent conversation with normal task governance
and shared UI.

## Linked Issues or Issue Description

**Subsystem affected**

Task lifecycle, agent runtime tools, shared task UI, and browser/paid
runner tests.

**Problem or motivation**

Users need one persistent conversation with each agent. A separate chat
store or renderer would duplicate task behavior and bypass existing
controls.

**Proposed solution**

Use a task-backed chat per company, user, and agent. Reuse the task
composer and transcript. Clarify and plan in chat, then create assigned
project tasks with the relevant plan. Keep Agent Chat behind its own
disabled-by-default experimental setting.

**Roadmap alignment**

This implements the task-backed direction in [CEO
Chat](https://github.com/paperclipai/paperclip/blob/master/ROADMAP.md#-ceo-chat).
Related proposals: #2504 and #9693. Related request: #7981. The
maintainer requested one squash merge of the complete stack.

Consolidates the reviewed runtime
[#13281](https://github.com/paperclipai/paperclip/pull/13281), backend
[#13282](https://github.com/paperclipai/paperclip/pull/13282), and UI
[#13283](https://github.com/paperclipai/paperclip/pull/13283) layers
with this PR's E2E coverage. All four layers passed CI and received
Greptile 5/5 before consolidation. This PR targets master and includes
the complete feature.

## What Changed

- Add personal canonical chat tasks with ordinary company visibility,
immutable identity, idempotent first sends, and an idle waiting state.
- Process `/new` in queue order. Preserve history, release a chat pause,
and fence old provider context and delayed writes.
- Keep chat lifecycle rules across recovery, finalization, assignment,
task lists, and rollups.
- Support research and plan revision in chat. Hand plans to ordinary
assigned project tasks before execution starts. Reject new chat
subtasks.
- Add repository-aware project creation and discovery tools, including
multiple repository IDs and GitHub URLs, authorization, idempotency, and
durable project-created cards.
- Reuse task UI components for chat, with starred/recent agent
navigation and a separate `enableAgentChat` experimental flag.
- Add deterministic browser tests and 24 paid chat cells across four
Codex/Claude profiles, with validated reports and screenshots.
- Integrate current master recovery, controller lease, queued-message,
and task UI changes. Gate chat interruption and deferred promotion on
ownership/feature policy. Guarantee lease renewal and active controls
are stopped even if teardown fails.
- Preserve master's migration 0273 and generate chat migration 0274 with
idempotent replay for development databases.

## Verification

- Prior exact heads of all four PRs passed Linux CI, including build,
typecheck, general/serialized tests, and browser E2E. Each had Greptile
5/5 and no unresolved findings.
- Integrated local verification passed: full repository typecheck and
production build, Storybook build, token gates, 340 focused UI tests,
all 20 deterministic chat browser tests, two migration replay tests, 88
focused chat/queue/native/controller tests, and provider/session
regressions including real lease expiry. These include the three
lifecycle regressions for the final admission/teardown fixes; server
typecheck also passes. Current head
`1268eda16cc2af892055917e7292f068820be135` has Greptile 5/5 with no
unresolved findings and passing security scans. All final-head CI gates
passed: build, full Runner verification, typecheck/release registry,
canary, all general/serialized test shards, and all browser E2E shards
([CI
run](https://github.com/paperclipai/paperclip/actions/runs/34696739927)).
Local PostgreSQL startup contention required serialized retries; skipped
fixtures do not count as passing coverage.
- The earlier paid campaign passed all 24 chat cells and retained 32
screenshots:
[report](https://d1p6rlowie26tp.cloudfront.net/runner-e2e/campaigns/gha-34648511170-1/index.html?report=agent-chat#suite-agent-chat).
It tested `abacbdfd2f660709ec37312cdb758284c8399d04`; it is prior
evidence, not a paid run of this integrated head.
- Manual check: enable Agent Chat in Experimental settings, open an
agent, clarify and revise a plan, then hand off to an assigned project
task. Stop a reply, send `/new`, and verify fresh context with retained
history. Disable the setting and verify agent shortcuts/new chat turns
are blocked.

## Risks

- Queue/session integration can affect retries and delayed writes. Tests
cover ownership, cancellation, reset boundaries, idle recovery, and
ordinary task behavior.
- Migration 0274 adds conversation fields and constraints. Replay is
idempotent and preserves existing development chat history.
- This combines the previously reviewed stack at the maintainer's
request. Agent Chat remains off by default and is separate from
Conference Room.

## Model Used

OpenAI Codex, GPT-6 Astra (`gpt-6-astra`), with reasoning, code
execution, browser tools, and parallel review. The exact context-window
size is not exposed in this session. Codex and Claude also ran as test
subjects in the linked paid campaign.

## 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-09-12 08:56:04 -05:00
DottaandPaperclip b1efd65edc fix: continue interrupted task conversations with bounded retries (#13237)
## Thinking Path

> - Paperclip manages AI agents and their tasks.
> - A task can outlive a provider process or a server restart.
> - Legacy recovery treated unknown tool outcomes as a permanent
execution hold.
> - That hold could also reject a later user message.
> - A conversation turn can use prior history without replaying prior
tool calls.
> - This pull request lets supported conversation adapters continue
within the existing retry budget.
> - Users can send a new message after automatic attempts stop.

## Linked Issues or Issue Description

**What happened?**

A server restart could interrupt a local ACP run and leave its task
behind a permanent recovery hold. A later user message could be
cancelled before the provider answered. The immediate recovery path
could also create a successor outside the durable failure counter.

**Expected behavior**

Continue with a bounded new conversation turn. Preserve a compatible
provider session or use full task context when it is unavailable. Do not
replay recorded tools. When automatic attempts stop, allow a new user
request through the normal execution gates.

**Steps to reproduce**

1. Start a task with a local conversation adapter.
2. Restart the server while the provider is working.
3. Let the previous run become interrupted.
4. Send a follow-up message and observe the recovery hold on the old
behavior.

Related work: Refs #13075 for durable task recovery. Refs #12946 for
retry-limit and checkout-lock handling. This change routes conversation
recovery through the existing bounded scheduler.

## What Changed

- Mark supported local conversation failures for continuation. Keep
native-runner and non-conversation recovery rules.
- Carry an interruption notice into the next turn. Retain stopped ACP
session history even when a write outcome is unknown.
- Clear unavailable ACP sessions so the next bounded attempt can use
full task context.
- Route immediate failure recovery through the same durable scheduler as
process-loss recovery. Release only the predecessor checkout when its
retry takes ownership.
- Retire obsolete conversation holds using immutable run evidence, in
bounded batches with an activity record. Preserve outcome evidence and
do not wake historical tasks.
- Block actual admission and Resume while a predecessor process or
environment lease is still active. Keep the original interruption notice
after a rejected wake. Preserve the upstream blocked-wake waiting
contract: bounded retry planning can happen during cleanup, while
deferred messages and execution remain gated.
- Add subprocess and database regression tests. Update the execution
contract.
- Add the current thread-status field to the native recovery provider
fixture so its damaged-journal test reaches the intended boundary.
Tolerate an already-exited fixture process during test cleanup while
still asserting both processes terminate.

## Verification

- Workspace typecheck passed: `pnpm -r typecheck`.
- Build passed: `pnpm build`.
- Module boundaries passed: `pnpm check:module-boundaries`.
- Focused tests passed: 293 recovery/session/dispatch tests, 66 retry
and response-gate tests, and 37 native-session tests. Some suites
overlap.
- Tests cover interrupted writes, missing sessions, concurrent retries,
restart persistence, pending questions and approvals, execution gates,
and historical holds.
- Built the Rust test executables with `pnpm --filter
@paperclipai/paperclip-runner build:rust` for native-runner
verification.
- Full Vitest coverage verified locally using the repository’s general
and serialized shards, with focused reruns for failures and files not
reached after a shard stopped. The ownership-gate regression is fixed
and the complete affected server shard passes (1,390 tests). Local
parallel runs also hit temporary-directory, resource, and timing
failures; those suites pass with canonical temporary paths and
sequential reruns. No test timeouts were increased.
- Final merged-branch regression run: 577 tests pass across process
recovery, retry scheduling, liveness, durable chat, wake-queue
application/adapter, dispatch, continuation, native sessions, and task
chat. Earlier focused verification also passed 19 native control tests.
Token gates and whitespace validation pass.
- Browser verification passed all three ACP Stop/continue/pause
scenarios, including a rerun after merging the upstream waiting
behavior: `PAPERCLIP_E2E_PORT=3397 pnpm test:e2e
tests/e2e/acp-stop-continuation.spec.ts`. The interrupted-write case
verifies that follow-up completes without a repeated write.

- Final-head [CI run
34625037394](https://github.com/paperclipai/paperclip/actions/runs/34625037394)
passed on `06ac4bd9d150f8b209a96e5fd609c696958794a0`: all 31 reported
checks are green, including server/workspace suites, all browser shards,
native runner verification, build, typecheck, release dry run, and
aggregate gates. The two conditional Storybook checks were skipped.
Greptile reviewed this exact commit at 5/5; all review threads are
resolved.

## Risks

- A new model turn can choose to repeat an action. Paperclip does not
replay recorded tool calls and does not certify unknown action outcomes.
- Conversation adapters now stop after their retry budget instead of
requiring action reconciliation. Explicit Stop, pause, dependency,
approval, budget, and ownership gates remain in force.
- No schema migration or dependency changes. Historical holds are folded
without changing task status or waking work.

## Model Used

OpenAI GPT-6 through Codex, with reasoning, repository tools, code
execution, and test execution. The session does not expose a more
specific model build ID or context-window size.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-09-11 12:16:04 -05:00
DottaandPaperclip 889947c238 feat: add experimental native chat connectors (#13038)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - People also ask agents for work in their existing chat tools.
> - Each external conversation needs one task and a current authorized
source.
> - Retries, Stop, and provider failures must not duplicate work or
expose private data.
> - The first chat PR establishes the opt-in provider and data
contracts.
> - This PR adds experimental channel integration and its durable
control plane.
> - Users can request work from connected channels and inspect delivery
in Paperclip.

## Linked Issues or Issue Description

Refs #13100 and #13092. This is the second of exactly two chat PRs.
Foundation #13100 is merged and changed 143 files. Runner prerequisite
#13092 is also merged. This PR changes 400 files against master, below
the 500-file review limit. It contains no wireframe images or HTML
galleries.

## What Changed

- Add native Slack, GitHub, Microsoft Teams, Telegram, and Discord chat
connections. Keep chat disabled unless the operator enables experimental
chat connectors. Preserve the production GitHub tool connection and its
normal setup path.
- Bind each provider bot identity to one immutable Paperclip agent. Bind
each admitted external conversation to one task. Paperclip owns tasks,
runs, permissions, and audit records.
- Add durable admission, per-conversation queues, questions, task
controls, progress, final replies, images, files, and delivery receipts.
Board comments remain internal unless explicitly sent to the channel.
- Check current identity, provider reach, resource access, credentials,
runtime generation, and exact source before provider effects. Keep
private responses private. Never send raw reasoning, private logs,
credentials, or tool arguments.
- Hold uncertain sends for explicit audited resolution. Make Board
Send-to-channel atomic and idempotent. Keep reconnect and setup
credentials in Paperclip secret storage.
- Preserve current native-runner authority across retries, lost
acknowledgements, and recovery. Keep immutable input and completion
contracts separate from newer user input. Receipt reconciliation cannot
launch a provider.
- Reconcile chat close/new ordering and provider-effect lock order.
Audit resource access changes in the same transaction. Submit only the
selected resource from each UI toggle so stale pages cannot undo
unrelated access changes.
- Drain Codex stdout before certifying process exit. Bound the drain
with the existing shutdown grace. Preserve observed terminal authority
without treating an undrained process as successful or reusable.
- Incorporate master `018ca5da` with its ACP Stop, mobile task layout,
runner packaging, and official lock changes. Preserve dedicated
chat-answer continuations in both directions when ordinary queued
comments are adopted after Stop.
- Fence late adapter readiness behind an earlier Stop for the same run.
Preserve verified cleanup for registered adapters. Handle single Stop,
agent pause, duplicate Stops, and failure release without creating a
false cancellation receipt.
- Incorporate master's `6dd48cad4` wake-queue extraction. Preserve exact
failed-chat retry authorization and lineage, retired question-source
suppression, and the block on generic recovery that would discard the
admitted source. Fresh deferred input retains its separate promotion
path.
- Incorporate master `2a05b5ed3` and its queue-admission extraction,
simplified transaction ports, and separate runner CI job. Preserve exact
durable receipts, actor separation, and dedicated-answer isolation
through the new module. A failed receipt insert rolls back the
accompanying deferred-wake merge.

## Verification

Current head: `afe19299d06253cb628eb398e91d1200ea9f412a`, incorporating
master `2a05b5ed3457ea33efd6895520447d1d97fe98d8`. The conflicts are
resolved. This successor fixes two test-harness boundaries exposed by
CI: per-case route-module preparation and actual durable-save completion
before intentional runner termination. Production code and all existing
test/turn deadlines are unchanged. [Exact-head Greptile
review](https://github.com/paperclipai/paperclip/pull/13038#issuecomment-5587250594)
is **5/5**, completed September 10 at 13:20:55 UTC, with no actionable
findings or open review threads. [Fresh exact-head
CI](https://github.com/paperclipai/paperclip/actions/runs/34481724341)
passes **all 24 jobs**, including Build and both required aggregates.
Normal exact-head guarded merge was attempted and rejected by the
remaining branch approval policy: CODEOWNER review is required and no
human approval is present. Normal **squash auto-merge is enabled** as of
September 10 at 13:36:26 UTC. Requested CODEOWNERS have been notified;
no approval bypass or self-approval was used. Earlier-head results below
remain historical evidence, not qualification of this successor.

- Final exact-head Linux evidence: 995/995 chat integration cases; 36/36
agent-skills routes; 35/35 runner live-session cases, including real
process kill/resume; 1948 runner Vitest cases with three existing
benchmark/platform guards; 870/870 API-authority cases; and 104 browser
cases with four existing optional skips. Rust, conformance/replay, full
repository build, typecheck, canary, all server/workspace shards, and
both required aggregates pass with normal CI concurrency. Earlier failed
attempts remain recorded below.

- Latest test-only qualification: 141/141
route/permissions/authentication cases pass in separate cold forks, with
plain server types and independent review clear. The real-runner suite
passes 35/35, with plain runner types and independent review clear. A
controlled premature-save acknowledgement fails as expected; matching
ownership/effect/process evidence, rejected saves, real turn outcome,
test abort, and pre-kill liveness are covered. No local reproduction of
the original CI scheduling failure is claimed. The preceding [CI
run](https://github.com/paperclipai/paperclip/actions/runs/34479680858)
passes 21/24 jobs, including all 995 Linux chat cases and browser
aggregate (104 passed, four existing optional skips); only Build, the
skills serialized shard, and the required verification aggregate fail.
Its exact-head Greptile review was 5/5. Both failed job logs are
retained.

- Final fixture qualification: all eight focused Discord cases and all
995 chat integration cases pass. The exact modal statement/PID is
observed before taking the real connection lock; the test then proves
its actual blocking relationship before mutation. Original SQL
execution, provider behavior, negative assertions, and 1s/15s timeouts
remain unchanged. Independent review is clear and test/production hashes
remain frozen. The preceding [CI
attempt](https://github.com/paperclipai/paperclip/actions/runs/34477184777)
passed 22 jobs, including Build/runner, typecheck, canary, all other
test shards, and browser aggregate (104 passed, four existing optional
skips); the two fixture failures and failed verification aggregate
remain recorded, not relabeled as a pass.

- Current queue-module composition: 308/308 recovery/batching/queue/Stop
tests; 995/995 full chat integration; 89/89 module tests, including real
PostgreSQL receipt-insert rollback; 24/24 workflow/module-boundary
tests; plain server and UI types. All four actual local process/ACP
browser paths pass in 1.4 minutes. Fresh databases, no skips or retries,
stable reviewed source hashes. The initial boundary failure is retained;
its no-op service wrapper was removed without changing recovery context
or weakening the check. An exploratory standalone test-directory
typecheck fails because its new upstream transformation config is not a
standalone typechecking project; standard CI/build does not invoke it,
and no configuration was weakened to suppress those diagnostics.

- The preceding head `e02a63d462ce5d47433b0aeb632bb6fd20aab1ba` passed
[all 24 CI
jobs](https://github.com/paperclipai/paperclip/actions/runs/34436462958)
and exact-head Greptile review at 5/5. Required CODEOWNER review
prevented its normal merge before master advanced again.

- Final extracted-module composition: 307/307 recovery, batching, queue
and Stop-control tests; 995/995 full chat integration; 49/49 module
tests including eight PostgreSQL adapter cases; and 19/19 issue-update
tests. Plain server types pass. All four actual local process/ACP
browser paths pass in 1.3 minutes. Fresh databases, no skips or retries
in these cohorts, frozen source hashes, and independent review clear.

- The preceding head `3e4e1c1c` passes [all PR CI
jobs](https://github.com/paperclipai/paperclip/actions/runs/34415826820),
including Build and required `ci / verify` and `ci / e2e`. Both the
original Rust failure and the previously load-sensitive lineage fixture
pass with unchanged Linux concurrency. Master advanced afterward and
required this reconciliation.
- Final master composition: 448/448 focused UI tests, 186/186 adapter
tests, 24/24 queue/control tests, and 11/11 packaging tests. Plain UI,
server, shared, and adapter types pass. Token gates and diff checks
pass. Independent server and UI reviews are clear.
- Stop-registration regression: both real-service cases fail against
exact `a95` source and pass with the fix. The full corrected
recovery/control suite passes 265/265. Duplicate-owner and failed-Stop
controls also pass. Plain server types pass. The readiness barrier
prevents provider startup without adding an acknowledgment to an already
terminal run.
- Final qualification strengthens terminal-field equality and repeats
both affected cases successfully on a fresh database. All four actual
local process/ACP browser paths pass again in 1.3 minutes, without skips
or retries. The final screenshot shows Cancelled, a paused subtree,
retained input, and no error toast.
- Two new actual-service regressions fail before the merge fix. They
prove that queued-comment adoption could consume a dedicated chat answer
or add unrelated input to that answer. The fixed four-case cohort
passes, including ordinary upstream continuation and adapter Stop
controls. Full recovery passes 257/257. All four actual local
process/ACP Stop browser flows pass in 1.4 minutes, without skips or
retries, on a fresh database.
- The unchanged runner artifact was qualified with 171/171 transport
tests, 870/870 API-authority tests, conformance 1/1, and replay 11/11.
Six controlled reader tests prove the exit/drain repair. Its local
serial Rust workspace passed 546 top-level cases plus two invoked
helpers; the later passing Linux CI supplies default-concurrency
evidence.
- Prior exact-source full chat integration passes 995/995. Settings
regressions cover concurrent stale pages, 501 destinations, pending
state, rejected updates, and explicit retry. These deterministic tests
do not prove live provider behavior.
- Retained failed attempts and their causes are in the [qualification
log](https://github.com/paperclipai/paperclip/blob/afe19299d06253cb628eb398e91d1200ea9f412a/doc/plans/chat-adapters/2026-09-08-chat-queue-and-webhook-repair.md).
The first merge adapter run timed out while macOS slept for 290 seconds.
Its unchanged repeat passed with a temporary sleep guard. No assertion,
deadline, or CI gate was weakened.

Review commands include `pnpm --filter @paperclipai/server exec vitest
run src/__tests__/heartbeat-process-recovery.test.ts
src/__tests__/issue-queued-comments-routes.test.ts` and `pnpm exec
playwright test --config tests/e2e/playwright.config.ts
tests/e2e/acp-stop-continuation.spec.ts`. Database suites require fresh
disposable databases. See the [browser
runbook](https://github.com/paperclipai/paperclip/blob/afe19299d06253cb628eb398e91d1200ea9f412a/doc/plans/chat-adapters/2026-09-04-chat-adapters-browser-e2e-runbook.md)
for provider setup and separate live acceptance steps.

## Risks

- This remains experimental. Deterministic tests and bounded live
evidence do not establish every provider feature, tenant, permission
layout, or media shape. Teams work-tenant qualification is still open.
- Failed and uncertain provider effects remain visible and can require
operator action. A transport receipt does not prove recipient
visibility.
- Native controller and runner artifacts must remain compatible.
Preserve lease ownership, terminal authority, source binding, and
quarantine during future changes.
- Access and audit rows commit together, but activity notifications
remain best-effort. This is not a new durable event outbox.
- The PR operation does not deploy a live server, replace its runner, or
change provider permissions. Remaining live qualification is documented
in the [temporary
handoff](https://github.com/paperclipai/paperclip/blob/afe19299d06253cb628eb398e91d1200ea9f412a/doc/plans/chat-adapters/2026-09-08-open-qualification-followups.md).

## Model Used

OpenAI Codex assisted with implementation, tool execution, testing, and
review. The work records `gpt-6-astra` assistance. The environment does
not report a context-window size. No private reasoning traces are
included.

## 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-09-10 10:06:45 -05:00
Nicky LeachandPaperclip ae0c1fbbd5 refactor(server): move the admission half of the deferred wake state machine into the wake-queue module (#13136)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The heartbeat service admits wake requests while an issue has an
active execution run.
> - That admission branch mixes wake policy, database reads, and
database writes in one service.
> - This structure makes the wake-queue boundary hard to test and
extend.
> - This pull request moves the admission policy and its database
adapter into the wake-queue module.
> - The result keeps heartbeat orchestration small and makes the
admission behavior testable in isolation.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

The heartbeat service now delegates deferred wake admission to the
wake-queue module. The module keeps the existing merge, defer, and
ordinary-wake outcomes.

**Subsystem affected**

server/ — REST API and orchestration services.

**Current behavior**

The heartbeat service contains a 146-line branch that reads wake state,
chooses an outcome, and writes the result.

**Proposed behavior**

The wake-queue module owns the pure admission decision and the adapter
reads and writes. The heartbeat service calls one module method.

**Reason and benefit**

This boundary reduces service coupling and lets module tests cover the
admission policy. The change keeps the existing reason strings and
outcomes.

**Breaking changes**

None. The change preserves the current behavior and public API.

**Additional context**

This pull request follows [PR
#13132](https://github.com/paperclipai/paperclip/pull/13132), which
merged the first slice of this refactor. I searched GitHub for duplicate
and related pull requests before opening this pull request.

## What Changed

- Move deferred wake admission policy into
`server/src/modules/wake-queue`.
- Add module ports and a PostgreSQL adapter for the admission reads and
writes.
- Keep the existing wake outcomes and stored reason strings.
- Extend the module boundary check to reject service imports from the
application layer.
- Add unit and adapter tests for the moved behavior.

## Verification

- `node --test scripts/check-module-boundaries.test.mjs` passes.
- The `server/src/modules/wake-queue` suite passes 58 tests.
- The eight pinned heartbeat and queued-comment tests remain unchanged
and require CI verification.
- Every continuous-integration check must reach a terminal green state
before merge.

## Risks

The refactor changes the location of wake admission logic. A missed
adapter condition could change deferred wake behavior. The tests cover
the policy outcomes and the adapter writes. The residual tenant-scope
risk remains documented in the review record.

## Model Used

OpenAI Codex, GPT-5, tool use and code execution. The implementation
author ran the tests and prepared the commit set.

## 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-09-10 00:55:48 -07:00
Nicky LeachandPaperclip 0bff1d5cb6 refactor(server): simplify the wake-queue module (#13139)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The server uses a deferred wake queue to release work at the correct
time
> - The wake queue had repeated decisions, helpers, queries, and
recovery data
> - This repetition made the module harder to read and left a drain
invariant implicit
> - This pull request moves pure decisions into the policy layer and
simplifies the queue flow
> - The benefit is a smaller, clearer module with the same behavior and
direct test coverage

## Linked Issues or Issue Description

Refs #13136

**Problem:** The deferred wake queue carried repeated logic across the
application and database layers.

**Expected behavior:** The queue keeps the same wake, release, recovery,
and escalation behavior after the refactor.

**Solution:** Move pure decisions into the policy layer, share repeated
data and predicates, and state the drain invariant in the application
layer.

## What Changed

- Remove the unused database port method and carry the blocked release
notice kind as a typed field.
- Move four pre-drain decisions into pure policy functions with
table-driven tests.
- Resolve the responsible user once in the application layer.
- Use the canonical helper for agent invokability checks.
- Share string helpers and run predicates across the module.
- Split the deferred-wake decision flow and share recovery facts with a
discriminator.
- Bound the drain loop and throw when it processes a wake identifier
twice.
- Rename queue ports to describe their behavior.
- Share row-loading code between stranded-issue escalation adapters.
- Restore the interaction-continuation integration test case.

## Verification

- Run the three wake-queue module test files. They pass 61 cases
locally.
- Run `pnpm check:module-boundaries`. It passes locally.
- Run the `server/` type-check and confirm that no error names the
changed wake-queue module or `heartbeat.ts`.
- Run continuous integration and confirm that `promotes an interaction
continuation after removing a coalesced self-authored comment` passes.

## Risks

The change refactors queue control flow and database adapter boundaries.
The main risk is a behavior change in deferred wake release or recovery.
The new policy tests and the restored integration case cover these
paths. The local environment cannot run the integration test because the
same dependency failure occurs on the base branch.

## Model Used

OpenAI Codex, GPT-5, context window not exposed by the runtime, with
reasoning, 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
- [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-09-10 00:03:22 -07:00
Nicky LeachandPaperclip 6dd48cad43 refactor(server): move the release half of the deferred wake state machine into a wake-queue module (#13132)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The heartbeat service releases deferred issue wakes and promotes the
next run
> - The release logic sat in a large service function, which made its
decisions and database effects hard to test
> - A promotion race could reopen an issue without creating the promoted
run
> - Company checks did not protect every read and write, and some
readers used a second transaction connection
> - This pull request moves the release logic into a layered wake-queue
module and closes these race and company-scope defects
> - The benefit is clearer decisions, safer writes, and focused tests
while existing callers keep the same entry point

## Linked Issues or Issue Description

Refs: #10195

## What Changed

- Move the release half of deferred issue execution from `heartbeat.ts`
into `server/src/modules/wake-queue/`.
- Add pure policy decisions with table-driven tests.
- Claim a wake before the reopen write and advance to the next wake when
the claim fails.
- Add company predicates to guarded reads and writes.
- Pass the transaction-scoped issue snapshot to reader ports.
- Keep `releaseIssueExecutionAndPromote` as the public wrapper.

## Verification

- Run `pnpm check:module-boundaries`.
- Run `pnpm exec tsc --noEmit` inside `server/` and compare the result
with the known baseline.
- Run the wake-queue unit and adapter tests in continuous integration.
- Check the promotion-claim ordering, guarded writes, transaction-scoped
reads, and cross-company outcomes.
- Search the pull request diff for internal issue identifiers.

## Risks

- The refactor changes the transaction path for deferred wake release.
- A stale or lost wake claim now skips that wake and continues with the
next queued wake.
- The public wrapper keeps its name and signature, which limits caller
risk.
- Continuous integration must confirm the full server test suite and
build.

## Model Used

OpenAI Codex, GPT-5, exact runtime model version not exposed, context
window not exposed, with shell, Git, and GitHub tool use.

## Checklist

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