Commit Graph
1889 Commits
Author SHA1 Message Date
DottaandClaude Opus 5.5 d87f6fa4c2 test(runner-e2e): add staging-soak reliability cases to everyday workflows (#19455)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The paid runner E2E suite (`tests/runner-e2e`) drives the real
browser, server, native runner, provider and Daytona
> - A manual staging soak found about nine reliability bugs (S47–S55,
H2, U9). Each fix got unit or integration tests
> - The end-to-end journeys that exposed the bugs existed only as ad-hoc
browser scripts, so a regression could return without a signal
> - These journeys are expensive, so they must not run per PR or in
`--all`. They must run on demand before a release
> - This pull request adds the journeys to the existing
`everyday-workflows` suite as an explicit `reliability` group
> - The benefit is one command that runs the whole reliability set
before a release or after a risky runner change

## Linked Issues or Issue Description

**What is the problem?**
The staging-soak scenarios that found S47–S55, H2 and U9 are not
repeatable. They were ad-hoc browser scripts on staging.

**Why does it matter?**
Without a repeatable end-to-end case, a reliability regression (stalled
follow-ups, duplicate execution, dead Retry, hangs after silent
commands) can return without a signal.

**What is the proposed solution?**
Add the scenarios to the paid `everyday-workflows` suite as
explicit-only cases with independent graders and a selectable
`reliability` group.

Refs #19370, #19355, #19354, #19407, #19419, #19394, #19421, #19431 (the
related fixes).

## What Changed

- `everyday-reliability-cases.ts`: seven new cases
(`interrupt-redirect`, `reassign-midrun`, `three-turn-revise`,
`long-silent-command`, `tree-pause-release`, `queue-steer`,
`queued-across-restart`), their soak signatures, prompts, bounds and the
explicit matrix.
- `everyday-reliability-flow.ts`: browser and public-API drivers for
each journey. A shared watcher records `executionBlocker`,
`interruption` and execution projections while it waits. It uses an
offered Retry at most once (never while an automatic retry is
scheduled).
- `everyday-reliability.ts`: pure graders. They encode the product rule:
interrupted work resumes automatically or offers a Retry that works on
first use. A send-a-message blocker, a dead or repeated Retry, a silent
stall, or duplicate execution fails.
- `types.ts` / `selectors.ts`: new optional `explicitGroup` on a task.
Such a task is selected only by its group, `--case`, or an exact `--id`.
`--all`, the plain suite and generic selectors skip it. `--list` still
shows it.
- `catalog.ts`: the suite gets the reliability tasks, the released
Grok/Kimi/Pi facades (only for `long-silent-command`), a per-turn
Daytona override for `three-turn-revise`, the `reliability` and `slow`
groups, definition version 11, and a computed `expectedMatrixSize` (52 →
62).
- `everyday-flow.ts`: a narrow hook calls the scenario driver after task
creation. Riley is created for the reassign and tree cases. The
reliability checks are added after settlement.
- `everyday-observations.ts`: an allowed interrupted run may end
`operator_interrupted` or `issue_reassigned`. Lifecycle checks accept a
declared reassigned parent owner.
- `live-fixtures.ts`: reliability cells get the existing 1,000-cent
company and lead-agent hard stop.
- Tests: new `everyday-reliability.test.ts` (selection exclusion,
matrix, credentials, and calibrated pass/fail cases for every grader).
Updated counts in `catalog.test.ts`, `everyday.test.ts` and
`installed-cli.test.ts`.
- Docs: new "Staging-soak reliability group" section in
`EVERYDAY-WORKFLOWS.md` (cases, signatures, run commands, cadence,
coverage boundary) and a link from `doc/evals.md`.

`everyday-flow.ts` was already over 1,000 lines. This PR adds only about
20 lines of wiring there. The scenario logic is in the new modules.

Matrix (10 cells, all Daytona, single attempt): one `runner-codex`
(gpt-5.6-sol) cell per case, plus `runner-acpx-grok`, `runner-acpx-kimi`
and `runner-acpx-pi` for `long-silent-command`.

Run command:

```sh
pnpm test:e2e:runner -- --suite everyday-workflows --group reliability --max-parallel 2
```

## Verification

- `pnpm test:e2e:runner:typecheck` passes.
- `node node_modules/vitest/vitest.mjs run --config
tests/runner-e2e/vitest.config.ts everyday catalog installed-cli
selectors support.test`: 15 files, 362 tests pass.
- The full runner-e2e Vitest run has the same 14 failures with and
without this change (`controller-outage`, `operating-receipts`,
`pi-controls-evidence`, `pi-file-evidence`, `workflow-security`). These
fail on the base commit in this local environment too.
- `pnpm test:e2e:runner -- --list --suite everyday-workflows --group
reliability` lists exactly the ten cells.
- `--all`, `--suite everyday-workflows` (still 52 cells),
profile/environment selectors and the daily/weekly scheduled selections
contain no reliability cell. A test asserts this.
- Not run: the paid cells. They need provider and Daytona credentials.
The first paid run of the group is the acceptance step.

## Risks

- The drivers have not run against a live stack. A first paid run can
expose harness timing or selector issues (for example, how fast the
queued Interrupt control appears). The failure then names the stalled
step and keeps `reliability-observations.json`.
- `long-silent-command` does not inject a runner connection loss. The
named-cause contract is checked only if a loss happens naturally.
- The Grok/Kimi/Pi facades run as ordinary released profiles. If one
returns to pending qualification, it drops out of the group and the
matrix size follows.
- No product code changes. Existing everyday cells keep their selection
and checks. The suite definition hash changes because the suite
definition changed.

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

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with
tool use (shell, file edit, code execution). Standard context window.

## Checklist

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

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Remote workspace imports preserve existing files before the server
changes workspace authority.
> - Archived Git imports reconstruct saved files and Git state in a
private repository.
> - A failed upload can leave a pending snapshot journal with no object
in storage.
> - A later reconstruction can have different Git administrative bytes
for the same saved logical source.
> - This pull request lets that import record a new unpublished capture
after it proves that prior objects are absent.
> - The benefit is safe retry through the original import journal while
source checks and atomic publication remain in place.

## Linked Issues or Issue Description

Refs #19396. Refs #19409.

A snapshot upload can fail before object persistence. The caller then
removes its private archive. A retry reconstructs the same saved Git
workspace. If its administrative bytes differ, the snapshot owner
rejects the retry with
`remote_workspace_snapshot_operation_content_changed`. The pending
journal remains blocked even though no snapshot object or remote
authority was published.

## What Changed

- Add an optional unpublished recapture capability to snapshot storage.
Bind it to the original source operation. Existing callers keep the
exact manifest check.
- Enable this capability only for archived Git reconstruction. The
migration owner keeps the source recipe, task binding, ownership, and
publication checks.
- Verify every prior object before new capture admission. Unknown or
corrupt objects stop the retry. Reuse an available prior object,
including an object that appears while the replacement upload runs.
- Compare the observed upload journal under the snapshot row lock before
appending an attempt. Keep network I/O outside that lock. Preserve all
prior attempt metadata.
- Keep the first atomic publication as the winner. A losing uploader
verifies and returns that saved reference.
- Add real PostgreSQL, filesystem, and Git regression tests. Document
the retry behavior.

## Verification

- Baseline `70d23612dd9176cae8ee6b2b6dcf2f6eade9a42a`: the new real Git
fixture fails with
`remote_workspace_snapshot_operation_content_changed`. It fails the
first upload before persistence, removes the old source cache, and
advances unrelated remote refs. The saved task commit and restore point
stay fixed.
- Final frozen source: 20 tests pass in a fresh process. This includes
the real Git regression and all snapshot storage cases. Tests cover
prior object recovery, corrupt and unknown objects, normal caller
mismatch rejection, operation binding, stale journal admission, and late
upload publication in both orders.
- Reproduce the final focused check from `server/`:

  ```sh
node ../node_modules/vitest/vitest.mjs run --config vitest.config.ts
src/services/remote-workspace-snapshot-storage.test.ts
src/services/remote-workspace-migration.test.ts -t
'failed-upload-retry|snapshot storage' --reporter=dot
  ```

- An earlier six-suite run had 201 passing cases and one failing
late-object case. That run started before the final prior-object recheck
was added. The failing case passes in the fresh final run above. This
earlier run is not a clean full-family result for the final source.
- That run skipped 16 lifecycle cases because
`PAPERCLIP_REMOTE_WORKSPACE_LIFECYCLE_TEST_DATABASE_URL` was unset.
Migration, storage, reservation, and concurrency tests used real
disposable embedded PostgreSQL.
- `git diff --check` passes. Full repository typecheck passed before
rebasing. Full test, build, and current-head CI results are pending.

## Risks

- The optional capability permits different physical captures. The
migration owner must prove that the logical source and current ownership
are unchanged. Ordinary mutable workspace uploads keep the previous
mismatch guard.
- An old upload can complete after the last object check. The first
authenticated atomic publication wins. A late upload cannot replace its
object or snapshot reference. Every candidate remains in the retention
journal.
- Unknown storage results and journal changes fail closed. They can
require another canonical retry. This change adds no retry loop and
changes no database schema.

## Model Used

OpenAI Codex, GPT-6 family. The worker used reasoning, tool use, code
editing, and local test execution. The worker runtime did not expose an
exact model ID or context window.

## Checklist

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

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 08:04:35 -06:00
DottaandPaperclip 4696ff4990 fix: reserve agent In Progress display for active runs (#19453)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Task status must tell operators whether an agent is working or
waiting.
> - A task can retain its in-progress lifecycle status after its run
stops.
> - The shared read projection only handled scheduled monitoring waits.
> - The UI also ignored other projected statuses for in-progress tasks.
> - This pull request requires an active task run for the In Progress
display.
> - Operators can see waits without changing execution or recovery
authority.

## Linked Issues or Issue Description

**What happened?**

An agent task still showed In Progress after its run ended with an
access failure and left a pending connection request. Stale execution
locks and saved lifecycle state could show the same false activity.

**Expected behavior**

An agent task shows In Progress only with a task-linked queued or
running run. Otherwise, it shows Blocked for recovery, In Review for a
pending response or review, Monitoring for a scheduled check, or Todo
for remaining work. Human ownership keeps its selected status.

**Steps to reproduce**

1. Create an agent-owned task with an in-progress lifecycle status.
2. End its run and retain a pending human request.
3. Read the task, its summary, and search results. Before this change,
they still show In Progress.

## What Changed

- Derive visible activity from company-scoped, task-linked runs in the
existing shared SQL projection.
- Project inactive tasks to existing waiting or todo states. Preserve
lifecycle status, locks, admission, and recovery.
- Make the shared UI resolver use the projection while preserving
explicit and optimistic status changes.
- Cover stopped runs, stale locks, human waits, company isolation,
monitoring, filters, counts, search, and UI labels.
- Document the presentation rule in the product and execution contracts.

## Verification

- Red before the server fix: 7 new regression cases failed because they
returned in_progress. The 10 existing cases passed.
- Green after the server fix: 20 cases passed in
issue-display-status.test.ts, using embedded Postgres and real task
APIs/search. Seven nearby active-question cases also passed.
- Review regression: dismissed requests, historical questions, and
returned review work each failed before the follow-up fix. All three now
pass using the existing active-interaction rule and pending review
state.
- Red before the UI fix: 4 label cases failed. Five additional safety
cases caught stale projections hiding explicit transitions.
- Green after the UI fix: 43 tests passed across
task-status-presentation, issue-filters, and issue-detail-subissues.
- pnpm -r typecheck passed. All UI token gates and synchronization
checks passed.
- pnpm build passed. Server typecheck and build passed again after the
review fixes.
- Full CI passed on 45c4ababc5, including
typecheck, build, general/serialized tests, Runner tests, and browser
checks. The final rollup has 55 successful checks, one neutral check,
and three configured skips.
- The local pnpm test:run command passed all 1,931 chat integration
tests. Its remaining duplicate full-suite run was stopped once full CI
passed on the same commit. This is not a claim that the complete local
command finished.
- Greptile reviewed the final commit at 5/5. Both prior findings have
regression coverage and all review threads are resolved.
- Live acceptance at commit 45c4ababc5: started a fresh isolated local
instance, created an inactive agent task, and saved a pending human
request. API reads changed from Todo to In Review. An initial readiness
probe ran before startup finished; the doctor then verified the exact
serving revision before acceptance. Chrome showed In Review in the task
header, breadcrumb, and sidebar. Stored lifecycle status remained
in_progress. The instance was stopped after verification.
- The reported stopped state was inspected in the live UI. The fix is
not deployed there. Local API tests reproduce the failure shape without
replaying the real deployment task.

## Risks

- This changes visible status and display-status filter membership for
inactive agent tasks. Stored lifecycle filters keep their existing
meaning.
- The projection uses existing indexes on task runs, recovery actions,
and interactions. It adds no worker, timer, schema change, or execution
action.
- A queued run counts as active admission. Human-owned tasks retain
explicit ownership status.

## Model Used

- OpenAI GPT-6 through Codex, with reasoning, repository tools, and code
execution. A more specific model ID and context-window size are not
exposed in this session.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 07:59:38 -06:00
DottaandPaperclip 2f35eb1080 feat(dev): add incremental local verification and pass reuse (#19345)
## Thinking Path

> - Paperclip coordinates agents that make and verify repository
changes.
> - Local verification currently defaults to the complete Vitest suite.
> - Contributor instructions also require broad local checks at each PR
handoff.
> - CI already owns conservative affected-test selection and broad
final-revision checks.
> - This pull request adds local selection and pass reuse from those
existing contracts.
> - Agents can get focused feedback and use CI to close the remaining
coverage gaps.

## Linked Issues or Issue Description

**Subsystem affected**

Developer verification scripts and contributor guidance. Related
selection work: #18398 and #18464.

**Problem or motivation**

An agent that repeats an unchanged local verification step starts the
complete suite again. The handoff guidance also duplicates broad CI
coverage locally.

**Proposed solution**

Add `pnpm verify` for the actual working tree. Select affected Vitest
specifications with the CI task policy and dependency graph. Reuse
successful checks whose inputs are modeled. Run opaque checks fresh.
Report exit 2 when broader verification is required. Update the
contributor contract to accept applicable final-revision CI coverage.

**Alternatives considered**

Plain `vitest related` omits filesystem and opaque dependencies.
Commit-only cache keys miss dirty files. Running full local suites at
every handoff repeats CI work.

**Roadmap alignment**

This is repository development tooling. It does not change the product
roadmap.

## What Changed

- Add `pnpm verify`, `--explain`, `--base`, `--full`, and `--no-cache`.
- Reuse CI selection and exact runner ownership for dirty, staged,
committed, deleted, renamed, and untracked inputs.
- Keep native server suites and serialized suites in their local owners.
Keep six sequential chat shards.
- Cache selected passes within one worktree. Hash source, fixtures,
installed package contents, dependency bindings and installation
metadata, dotenv files, child environment, toolchains, platform, and
selection.
- Run opaque consumers fresh. Preserve failed, interrupted,
mutated-input, and partial-coverage outcomes.
- Update AGENTS.md, CONTRIBUTING.md, and DEVELOPING.md guidance. Record
the shared `verify` command as the sole exception to the package-script
rule. Register the new tests in the existing CI contract-test command.
- Add optional root/environment inputs to shared graph helpers. Existing
CI defaults stay the same.

## Verification

- Fourteen new integration tests pass locally across focused runs with
real Vitest selection and execution; CI colors are covered.
- 51 existing targeted selector/input, project-isolation, and
stable-runner checks passed. Four relevant runner contracts also pass
after the workspace change.
- Repeated unchanged execution skips reusable tests. Source,
environment, installed code, dependency bindings, and new untracked
tests invalidate results.
- Installed-code mutation with a restored modification time still
invalidates a cached pass. Internal skill-directory links preserve
reuse.
- Workspace B/C tests prove discovery, reusable/fresh partition
execution, pass reuse, and external-input failure. Chat selection
produces exactly six single-worker partitions.
- A deliberate unrelated failing test is omitted by narrow selection and
caught by `--full`.
- Red/green regressions prove fingerprints preserve file boundaries and
reject edits made between diff discovery and snapshotting.
- Opaque external-file changes still fail on a subsequent run. Source
mutation and process interruption cannot publish reusable passes.
- `pnpm verify --explain` works in the checkout and requests broad CI
coverage for this tooling change. No duplicate full local suite was
started.
- [Final-revision
CI](https://github.com/paperclipai/paperclip/actions/runs/42117291789)
passed on `ed85d96ddff82c05c325948680b593b0f48c473d` (attempt 2). The
scoped Canary retry preserved all passing test/build results.
- [Greptile
review](https://github.com/paperclipai/paperclip/pull/19345#issuecomment-6670050175)
is 5/5; all four review threads are resolved.
- The additional Paperclip review concluded neutral because its
execution stopped; it produced no assessment.

## Risks

- Pass reuse depends on conservative dependency classification. Opaque
consumers, external database/live settings, and non-isolated projects
retain fresh or broad coverage.
- Local passes cover selected Vitest checks. Required CI,
build/typecheck, release, browser, and platform acceptance remain
separate obligations.
- Cross-worktree caching and cross-agent scheduling are outside this
change.
- The initial installation scan reads dependency contents;
stat-validated, worktree-local digest memoization avoids rereading
unchanged contents. Subsequent scans still inspect file identity and
nanosecond change/modification times.
- No dependency lockfile or workflow changes.

## Model Used

OpenAI Codex, `gpt-6.1-sol`, max reasoning, with local shell and file
tools. Context window size is not exposed by this host.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 07:29:10 -06:00
DottaandPaperclip 70d23612dd fix: offer Interrupt when queued-message steering is unavailable (#19450)
## Thinking Path

> - Paperclip lets people manage work performed by AI agents.
> - Task chat saves follow-up messages while an agent runs.
> - A native runner can accept Steer only after an active provider turn
exists.
> - During preparation, or with an unsupported provider, the queue
showed a disabled Steer button and no delivery action.
> - The server also rejected Interrupt for ordinary native queues.
> - This change offers an explicit Interrupt fallback and uses the
existing cancellation and admission path.
> - People can send their saved messages while Stop confirmation,
cleanup, and execution gates remain enforced.

## Linked Issues or Issue Description

Related: #15865 covers mixed legacy queues. #15030 covers steering
acknowledgements. This change covers native queues without available
steering.

**What happened?**

A queued message had a disabled Steer button while the native runner was
still preparing. Clicking it did nothing. The server returned HTTP 409
for an explicit Interrupt request against the same native queue.

**Expected behavior**

Offer Steer when the provider can accept same-turn input. Otherwise
offer Interrupt, labeled with its actual effect. Preserve saved messages
until the previous execution stops and normal admission accepts the
successor.

**Steps to reproduce**

1. Start a task with the Paperclip Runner adapter.
2. Submit a follow-up while the run is preparing, before its provider
turn opens.
3. Observe the disabled Steer button above the composer.
4. With this change, click Interrupt. The existing Stop path cancels the
previous execution and submits the saved queue through normal admission.

**Paperclip version or commit**

Reproduced locally on master at 523818ea9b. The branch is rebased on
2f9d3d43c3.

## What Changed

- Offer Interrupt for unavailable or unsupported native steering when
the parent supplies an interrupt callback.
- Keep Steer for an available provider turn.
- Accept explicit Interrupt for a deferred native queue and record its
run target.
- Bind native continuation to the exact Interrupt queue, run, actor, and
cancellation receipt. This lets one click send the messages after Stop
completes.
- Add component, task-thread, real PostgreSQL route, and continuation
authorization regressions.
- Close the route fixture database pool before disposing its PostgreSQL
instance.
- Document the fallback and its Stop and admission requirements.

## Verification

- Red: two component cases failed because Interrupt was absent. Two
route cases returned HTTP 409, `This queue does not support
interruption`, before the repair.
- Red: stronger delivery assertions also caught `send_predates_stop`
after the first repair. The admission fix covers both the freshness
exception and later eligibility check.
- Green: both route regressions now wait for preparation settlement,
then deliver both saved messages exactly once without a second click.
The complete route suite passes 119 tests. The complete continuation
suite passes 376 tests, including wrong actor, queue, target, and
missing-receipt cases.
- UI: task-thread and queued-message suites pass 394 tests, with one
existing skip. The issue-page suite also passes (the earlier three-suite
run passed 644 tests before the final three UI cases).
- Token gates and `git diff --check` pass.
- Full workspace build and low-memory typecheck pass. The initial
typecheck caught a missing `membershipRole` in an unrelated fixture; the
rebase picked up the correction already on master, and the complete
typecheck then passed.
- The full local `pnpm test:run` passed all six chat shards (1,931
tests). It was stopped during general-server validation after the final
commit passed all PR CI gates. The complete local suite is not claimed
as a pass.
- Earlier route-suite teardown timed out with an open database pool.
Closing that pool fixed teardown; the complete route suite then passed.
- All final-head CI gates pass: 52 successful checks and two intentional
skips. Greptile is 5/5 on the same head with no unresolved review
threads.

## Risks

Interrupt stops the previous execution and starts a successor; it is a
different action from same-turn steering. The UI labels that action
explicitly. Cleanup or execution gates can keep the messages queued.
Existing company access, board permissions, queue revision, run target,
cancellation proof, and admission checks remain in place. No schema
migration is needed.

## Model Used

OpenAI Codex (GPT-6), with reasoning, tool use, and code execution. The
exact model ID and context window are not exposed by this client.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 07:11:38 -06:00
DottaandPaperclip 275b334fc5 fix(workspaces): skip redundant repository admission snapshots (#19444)
## Thinking Path

> - Paperclip manages AI agents and their execution state.
> - Remote workspace admission must preserve files before execution
starts.
> - Admission saved the entire workspace once per registered repository
on every run.
> - Existing snapshot receipts can prove that validated repositories are
already saved.
> - This change uses those receipts and saves new repositories once per
admission batch.
> - It also preserves fresh configuration metadata and reports actual
workspace reuse.

## Linked Issues or Issue Description

**What happened**
Remote follow-up runs uploaded an unchanged workspace for each
registered repository before starting the provider. Configuration
persistence also overwrote new evaluation metadata, and freshness logs
could claim replacement when the same workspace was reused.

**Expected behavior**
Validate existing repositories without uploads when a committed snapshot
covers their descriptors. Save acquired repositories before publishing
ready receipts. Retain current evaluation metadata and report the actual
workspace outcome.

**Steps to reproduce**
Use a remote workspace with two registered repositories. Prepare it,
then admit a follow-up run. Observe repository snapshot operations
despite unchanged repositories. The regression tests exercise this with
real Postgres, Git, and separate sandbox directories.

**Environment**
Server remote workspace admission at commit
c0214826b3. Related work: #19374 handles
interrupted clone staging; this change handles snapshot admission cost.

## What Changed

- Skip repository saves when the committed snapshot covers the validated
descriptor.
- Save all repositories in one admission snapshot before publishing
ready receipts.
- Retain descriptor coverage only when the new archive contains the
checkout. Drop deleted checkout coverage so admission can acquire it
again.
- Restore `needs_repair` status for failed batch saves and retry failed
uploads.
- Let fresh preparation metadata override stale reserved metadata while
preserving storage authority.
- Report actual reuse when freshness policy requested replacement.
- Add regression coverage and fix an existing routine user fixture
missing its membership role.

## Verification

- Red: original code fails unnecessary-snapshot, evaluation-metadata,
and reuse-message regressions.
- Green: 517 affected tests across six suites before the final coverage
correction. After that correction, all 102 remote workspace/concurrency
tests and 96 archive tests pass. Imported-repository, deleted-checkout,
and upload-retry cases pass.
- Additional red/green: deleted checkout coverage fails before the fix
and passes after it. All 96 archive tests pass, including manifest
digest and size bounds.
- `pnpm -r typecheck`, `pnpm build`, and `pnpm check:token-gates` pass
locally.
- All final-head CI verification jobs pass, including the aggregate `ci
/ verify` check. Greptile gives the final head 5/5.
- The monolithic local `pnpm test:run` passed all six chat shards and
continued through general server tests. It was stopped after final-head
CI passed; it did not complete locally. The targeted local regressions,
affected suites, typecheck, and build completed successfully.

## Risks

Snapshot receipts must refer to committed bytes. Legacy receipts require
one conservative save. Failed uploads must not publish repository
readiness. Replacement-class drift still retains the original
configuration for an explicitly reused workspace.

Existing directories with damaged or missing Git metadata still require
repair. Master and this PR both validate those directories without
cloning over them, preserving any surviving user files. Clearing a
snapshot descriptor would not change that path. Complete directory
deletion is different: this PR clears coverage so normal acquisition can
clone it again.

## Model Used

OpenAI gpt-6.1-sol through Codex. Used reasoning, repository inspection,
code editing, and local execution. The host does not expose the context
window size.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 07:08:59 -06:00
DottaandPaperclip c0214826b3 fix(workspaces): preserve pending repository requests during import (#19439)
## Thinking Path

> - Paperclip manages agent work and its saved files.
> - An ephemeral web server needs remote workspace authority before the
old volume can detach.
> - A task can have a repository request that has never prepared a
checkout.
> - The offline importer currently treats that pending request as
unfinished file publication.
> - This blocks migration even when the target does not exist and there
is no saved commit, branch or failure.
> - This pull request preserves that request in the database and imports
the existing files separately.
> - The next authorized sandbox preparation can acquire the repository
through its existing owner.

## Linked Issues or Issue Description

**What happened?**

On Core `253bf071f0a496554b61fff897ad3f11afaf8766`, offline migration of
a volume-backed workspace fails with `repository_preparation_unresolved`
for an untouched pending repository request. The requested directory has
never existed. The request has no pinned commit, branch or failure.

**Expected behavior**

Preserve the pending request and all existing files. Do not label the
absent checkout as part of a saved archive. Keep partial publication and
active preparation blocked.

**Steps to reproduce**

Create a local task workspace. Add an unmaterialized pending repository
request. Run the canonical offline importer after stopping writers. The
unchanged importer rejects the request before preserving the workspace.

Related migration work: #19429. A GitHub search found no duplicate
pending-repository import fix or matching public issue.

## What Changed

- Admit an untouched pending request only at the existing offline writer
fence and after proving its target is absent.
- Keep the database request unchanged and exclude it from materialized
snapshot descriptors.
- Recheck file absence and source identity before publication. Reject a
request that becomes ready during capture.
- Cover cold filesystem loss, real sandbox-side Git acquisition, and
eleven blocked states. Document the preservation contract.

## Verification

- Red: the new real PostgreSQL/filesystem regression fails on the
unchanged importer with `repository_preparation_unresolved`.
- Green: both affected migration/lifecycle suites pass, **236 tests**.
The cold test deletes the predecessor directory, restores saved bytes,
and acquires the pending repository through the full admission owner,
checking ready state and the pinned commit. Real Git runs against a
local HTTP origin through an exact-URL redirect at the sandbox transport
boundary. It does not claim live Daytona or GitHub qualification.
- Full workspace typecheck and build pass at the final head. Both
affected suites pass **236 tests**, including the full admission owner
and eleven blocked states. All final-head CI checks pass: **54 success,
2 skipped, 1 neutral**. Greptile is **5/5**, with no unresolved review
threads.
- The full local test command failed in `recovers lost inline and check
responses without duplicate GitHub publications` in the chat integration
suite. That exact case passes in a fresh isolated invocation. Its cause
remains unclassified; this is not a full-local-suite pass claim. The
original failure is retained alongside the isolated result.
- No candidate code has been applied to the serving instance.

## Risks

- The exception applies only to offline migration and a pending request
with no target, commit, branch or failure. Unreadable paths, symlinks,
partial directories and active/repair states retain their blockers.
- Requests retain normal repository authorization and acquisition
behavior. This change neither fetches repositories during migration nor
marks requests ready.
- A changed materialized source aborts publication and leaves original
files intact.

## Model Used

OpenAI GPT-6 through Codex, with reasoning and tool use. The exact
deployment variant and context-window size are not exposed in this
session.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 05:49:16 -06:00
DottaandPaperclip 21d2a577bb feat: show and change routine responsible users (#19128)
## Thinking Path

> - Paperclip helps people manage AI agents and their work.
> - Routines create tasks for repeated work.
> - Each routine has a responsible person.
> - The Overview did not show this person or permit a change.
> - People need to see who will be responsible for automatic tasks.
> - This pull request adds a small name picker beside Default agent.
> - Authorized people can adopt a routine or transfer it to an active
company member.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

The routine Overview and the routine update API.

**Subsystem affected**

UI, server, and shared contracts.

**Current behavior**

The routine stores a responsible user. The Overview does not show that
person. The update API cannot change that person.

**Proposed behavior**

Show Responsible user beside Default agent. Click the name to search
active company members. Save only the new person and the current
revision ID.

**Reason and benefit**

People can see who owns future automatic tasks. An authorized person can
adopt a routine without making a new routine.

**Breaking changes**

The detail response adds an optional actor-specific field. The update
API adds an optional responsible user ID. Existing requests keep their
current behavior.

Related: #17368 covers local Board ownership migration. This change adds
the routine control and explicit transfer.

## What Changed

- Show a quiet, searchable person picker beside Default agent.
- Explain that automatic tasks use this person. Manual runs use the
person who starts them.
- Permit human operators with `tasks:assign`, instance administrators,
and local operators to transfer responsibility.
- Check active company membership inside the update transaction.
- Save a revision and record the old and new person in the activity log.
- Keep existing task responsibility and ownership during revision
restore.
- Show errors for failed saves and stale revisions.
- Add tests for permissions, company boundaries, transfers, inheritance,
and picker states.
- Keep the picker disabled until saved data is refreshed. Refresh the
activity list after a transfer.
- Capture the actual previous person under the update row lock for the
activity record.
- Use shared query error handling and retain cached people during a
connection failure.

## Verification

- Focused checks cover 145 tests in 5 files. The first post-fix run
passed 144 tests. One existing Fireflies route test timed out during the
first import while the UI typecheck also ran. The same test passed in
the pre-fix run. The isolated route suite then passed all 21 tests with
no source change.
- The new tests reproduce slow detail refreshes, activity refresh, and
two concurrent transfers without a base revision. The concurrency test
uses PostgreSQL row locks.
- UI and server TypeScript checks passed at commit `1df9587eb`.
- The UI production build passed for the updated head.
- The branch includes master through `26391707f`.
- Design token gates, query-error policy, and Git whitespace checks
passed. The query-error policy reports zero findings.
- Local browser checks passed on desktop and mobile for the initial
feature commit. Search, selection, reload persistence, and revision
creation passed. No page errors or horizontal overflow were found. The
follow-up does not change the normal layout.
- Earlier full local checks could not finish: Cargo was absent, and
concurrent full test/build checks exceeded the 8 GiB memory limit. These
attempts are not passes.
- The first CI run failed on two error-rendering policy findings. That
failure is fixed locally. The three findings from the first Greptile
review are fixed and their threads are resolved.
- Greptile reviewed `1df9587eb2d3e034c2769b1f6955ac52323c95bb` and
scored it 5/5. It confirms all three prior findings are addressed. There
are no open review threads.
- [CI attempt
1](https://github.com/paperclipai/paperclip/actions/runs/42046369415/attempts/1)
passed 53 checks. The server 8/12 shard failed only because the existing
`history-http` runtime fixture did not become ready within 10 seconds
(99 refused connections). The fixture and runtime source are unchanged
from the merged master. That case passed locally on the same PR head
with no source change. Its full 75-test suite also passed locally. The
cause of the CI failure remains unknown.
- [CI attempt
2](https://github.com/paperclipai/paperclip/actions/runs/42046369415/attempts/2)
passed on `1df9587eb`: 54 successful checks, 3 skipped, and 1 neutral.
No failed or pending checks remained. The original fixture-readiness
failure remains recorded above.
- A later review found that viewers with an explicit `tasks:assign`
grant could see an editable picker even though writes were denied.
Commit `8b95ee707` now mirrors active-membership and viewer restrictions
in the GET capability flag. Instance-admin and local-operator behavior
is preserved.
- Three membership regressions failed on the incorrect editable flag
before the fix. After the fix, all 43 routine-route and company-access
cases passed (`--testTimeout 60000` accommodated this sandbox's cold
route imports; no repository timeout was changed). The first pre-fix run
timed out during the cold import in one case; the second pre-fix run
reproduced all three assertion failures.
- Layered server typechecking and whitespace checks passed after the
permission fix.
- [Fresh
CI](https://github.com/paperclipai/paperclip/actions/runs/42057752313)
passed on `8b95ee7075417527687b4a86cbaf63ba87408364` without a retry.
Greptile reviewed this exact head, scored it 5/5, and added no new
findings. All four review threads are resolved. The security status is
also successful.
- The separate Paperclip Review check is still queued because its
assigned review worker requires runtime recovery. This is the remaining
check before completion; the CI workflow itself passed.

## Risks

- A transfer changes the person used for future automatic tasks.
Existing tasks keep their current person.
- Only people can transfer responsibility. The server checks the
permission again on every write.
- A stale revision rejects the save. The user must reload before
retrying.
- No database migration is required.

## Model Used

OpenAI Codex, based on GPT-6, with reasoning, tool use, and code
execution. The exact runtime model ID and context window are not exposed
to this session.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 05:47:30 -06:00
DottaandPaperclip 253bf071f0 fix(workspaces): preserve resolved pending merges in snapshots (#19429)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Task workspaces must survive replacement of the web server.
> - Workspace snapshots therefore preserve both files and portable Git
state.
> - A resolved merge can still await its merge commit.
> - The archive rejected this state, so an existing workspace could not
migrate.
> - This pull request saves and checks the complete supported merge
state.
> - The benefit is that migration preserves the user's choice to
continue or abort the merge.

## Linked Issues or Issue Description

**What happened?**

A legacy workspace contained a resolved merge with `MERGE_HEAD`,
`MERGE_MODE`, `MERGE_MSG`, `ORIG_HEAD`, and `AUTO_MERGE`. Its index had
no unresolved entries. The snapshot owner refused the operation. This
kept the original safe but blocked migration.

**Expected behavior**

Save this supported state without changing the source. Restore it
without the original repository, borrowed object store, or linked
worktree. Retain the merge parents, selected index, later edits, and
Git's normal safety checks.

**Steps to reproduce**

1. Merge two branches that conflict.
2. Resolve and stage the conflict. Leave the merge uncommitted.
3. Capture the workspace. The base implementation rejects the merge
marker.

Related prior work: #19310 preserves an inactive rebase bookmark. No
duplicate open merge-preservation PR was found. This is a workspace
preservation fix, not a new roadmap feature.

## What Changed

- Admit only resolved pending merges with valid, bounded metadata and
available objects.
- Retain and verify the merge mode, message, parents, original head,
automatic merge tree, and index.
- Verify copied metadata and object closure before publication. Keep the
before/after source checks.
- Keep unresolved conflicts, autostash, rerere, mixed operations,
malformed state, and unsafe links blocked.
- Test real ordinary, linked, and borrowed repositories through local
and serialized capture. Delete all original sources before continuing or
aborting the restored merge.
- Document the supported state and the remaining refusal cases.

## Verification

- Six new continuation cases fail against the unchanged base at the
pending-operation guard.
- The first candidate restored and continued the merges, but six abort
assertions failed. The fixture rewrote the worktree without updating
Git's index cache. It now first verifies that abort protects later
edits, uses normal `git restore` to discard those fixture edits, then
aborts. No archive assertion was removed.
- The final archive and environment suites pass all 98 tests at
`3c87d13d1b`, including linked-metadata protections and pending merges
in ordinary, linked, borrowed, shallow, partial, and SHA-256
repositories. All 12 local/serialized continuation cases pass after
source removal.
- Full repository build and typecheck pass at `1544cc4a90`. The build
first found a nullability error; an explicit rejection return fixed it.
- The first full local test attempt stopped before tests ran because
omitted package postinstall steps left PostgreSQL library links absent.
Direct `initdb --version` reproduced the missing-library failure.
Restoring the package links made that probe pass; the full test rerun is
active.
- The initial typecheck also found missing excluded-plugin SDK links
after the same install. Running the repository postinstall link helper
corrected this, and the full typecheck now passes.
- All current-head CI gates pass at `3c87d13d1b`: 54 success, 2 skipped,
1 neutral. Fresh Greptile is 5/5 and no review threads remain open.
- Review requested shallow and SHA-256 coverage. The current commit adds
both plus partial-clone coverage; all 98 archive tests and the full
server typecheck pass.
- All six local chat shards pass. The monolithic local general-suite run
reports one viewer-permission test failure and remains active. That
exact test passes alone, and its full 505-test file also passes alone.
The original failure cause is unresolved; this is not a full local-suite
PASS. The complete current-head CI suite is green.

## Risks

- This widens capture eligibility only for the checked resolved-merge
state. No migration commits or aborts the source operation.
- A missing object or changed metadata still blocks publication and
retains the source.
- Unresolved merges and other operation formats remain unsupported.
Their preservation guard is unchanged.

## Model Used

OpenAI Codex, based on GPT-6, with reasoning, repository tools, and code
execution. The runtime does not expose a more specific serving model ID
or context-window size.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used with the available version and
capability details
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have described the issue in-PR following the relevant issue
template
- [x] I have not referenced internal or instance-local Paperclip issues
or links
- [x] My branch name describes the change and contains no internal task
id
- [x] I have run the affected tests locally and they pass; full-suite
limitations are recorded above
- [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 the 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 merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 04:20:32 -06:00
DottaandClaude Opus 5.5 8c578668b8 fix(heartbeat): start new queued runs promptly after restart and name the wait (#19431)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The heartbeat queue starts queued agent turns. Restarts and deploys
happen often.
> - After a restart, new queued turns waited minutes before they
started. The task showed only "Queued", with no reason.
> - The cause is startup order. The queued-run dispatch worker registers
only after many unrelated first passes finish one after another: tool
reviews, delivery workers, the terminal workspace sweep, native restart
recovery, and residual and issue recovery.
> - The server binds its listener before this recovery, and
`/api/health` answers during it. A deployment can route traffic to the
new server for minutes before queued work starts.
> - This pull request starts queued turns that never executed as soon as
plugin workers and adapters are loaded. It also names the wait while the
server starts, recovers, or shuts down.
> - The benefit is that tasks resume quickly after a restart and show a
visible reason while they wait.

## Linked Issues or Issue Description

**What happened?**
On a hosted instance, the server restarted twice for deploys. Both
times, queued runs on idle agents started about 3.5 to 4 minutes after
the restart. Before the restart, the runs also waited about 5 minutes
while the deploy drained the old server. During the wait after the
restart, the execution presentation showed `phase: queued`, `label:
"Queued"`, `cause: null`.

**Expected behavior**
Queued work resumes automatically and quickly after a restart. While it
waits, the task shows a reason.

**Steps to reproduce**
1. Start a task drain (`POST /api/instance/task-drain`). Assign issues
to idle agents. The runs stay queued.
2. Stop the server (SIGTERM) and start it again.
3. Make any startup first pass slow (a large instance does this
naturally). Queued runs start only after every earlier startup pass
finishes.

**Paperclip version or commit**
master at 7e3caa7bac.

**Deployment mode**
Hosted Railway deployment with fleet deploy drain. Reproduced locally
with embedded PostgreSQL and an injected 20-second startup step.

## What Changed

- `heartbeat/queue.ts`: add `resumeFreshQueuedRunsAtStartup`. It starts
queued runs on agents with no run left `running` and no queued retry or
continuation (`started_at`, `retry_of_run_id`,
`process_loss_retry_count`, `continuation_attempt`). Every run still
passes the ordinary per-agent claim and dispatch gates, and scheduling
suppression (including the task drain) still applies.
- `heartbeat.ts`: expose it. Count it as a pending wake, like
`resumeQueuedRunsAfterDrain`, so drain status and upgrade readiness see
it.
- `index.ts`: run that pass at startup after plugin workers and external
adapters load. It does not wait for the unrelated startup first passes.
It skips while a drain, warm standby, or shutdown is active. Log when it
completes and when the full queued-run pass completes, with process
uptime, so a hosted restart shows both timings.
- `execution-projection.ts`: while the server is `starting`,
`recovering`, or `draining` (shutdown), a queued run shows **Waiting for
server restart** (cause `server_restarting`) with a next action. A task
drain keeps its **Waiting for maintenance** reason.
- `doc/execution-semantics.md`: describe the startup fast path and the
new presentation.
- The full queued-run pass, its position after native restart recovery,
and the drain behavior are unchanged.

## Verification

- Red, then green, local restart with a 20-second injected startup step
(embedded PostgreSQL, three process agents, runs queued under a task
drain, SIGTERM, restart):
- Before: the listener bound at 09:41:50.6. Runs started at 09:42:10.8,
20.2 s later, after the slow step ended.
- After: runs started 0.2 to 0.3 s after the listener bound, while the
slow step still ran. Each original run executed once.
- During the before run, `GET /api/issues/:id/execution` showed `Waiting
for server restart` / `server_restarting` until dispatch.
- Red, then green: `execution-projection.test.ts` restart-phase cases
failed on the old code (`label: "Queued"`, `cause: null`) and pass now.
- New `heartbeat-startup-fresh-dispatch.test.ts` (real PostgreSQL): a
fresh run starts; an agent with a `running` row from the previous
process and an agent with a queued retry stay queued; nothing starts
while a task drain is active.
- `npx vitest run` for the new test, `heartbeat-task-drain*.test.ts`,
`execution-projection*.test.ts`, `startup-recovery-timing.test.ts`,
`heartbeat/queue.test.ts`, `liveness.test.ts`: 132 passed.
- `npx tsc --noEmit -p server/tsconfig.json` passed.

## Risks

- New queued runs can start before native restart recovery finishes. The
fast path excludes any agent with a `running` row or a queued retry or
continuation, and the claim checks still enforce issue ownership,
predecessor cleanup, budgets, pauses, and concurrency. A new wakeup
during startup already starts runs at this point, so this adds no new
class of early execution.
- The drain is unchanged. It still holds new runs while the deploy waits
for in-flight runs (the Cloud wait budget is at most 5 minutes). This PR
does not change Cloud.
- The new label depends on the process-local startup phase. It shows
only on the process that is restarting.

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

## Model Used

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

## Checklist

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 04:00:42 -06:00
DottaandClaude Opus 5.5 140fcf98ed fix(retry): name a declined task Retry and let a new click replace it (#19426)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A task interrupted by a restart or deploy must resume automatically,
or offer a Retry that works on first use
> - A board Retry saves an intent first. Dispatch then rechecks the
requester's authority and saved source before it starts a run
> - When that recheck refused the intent, dispatch returned a bare
"deferred". The intent stayed saved with no reason, and the task still
showed Retry
> - Each later click with a new key joined that same intent, so Retry
returned 202 and never started a run
> - This pull request names the refusal on the intent and lets a new
click replace an intent that can never start
> - The benefit is that a Retry button that is shown starts one run, or
explains why it cannot

## Linked Issues or Issue Description

**What happened?**
A native task in a managed sandbox lost its runner twice. Saved-work
recovery recovered the files. The task showed "Saved work recovered.
Retry or send a message to continue." and `canRetry: true`. A Retry
click returned 202 and logged `issue.retry_requested`, but no run
started. A second click with a new idempotency key returned the same
request ID and `state: failed`. No run started.

**Expected behavior**
If Retry is offered, a click starts exactly one run. If the Retry cannot
start, the task names the reason, and a later click is a fresh attempt.
A Retry accepted during the pre-deploy drain starts after the restart.

**Steps to reproduce**
1. Recover the files of a failed native run so the hold offers Retry.
2. Save a Retry intent whose dispatch-time authority recheck fails (for
example, the requester lost access).
3. Dispatch it: no run starts, and the intent has no wait reason.
4. Click Retry again with a new key: the response returns the first
request ID, and no run starts.

**Paperclip version or commit**
Reproduced on `b400914345` (staging served `4d49a31286`; the affected
code is the same).

Related: #19399 (automatic Retry after a lost runner), #19422.

## What Changed

- `heartbeat/queue.ts`: when dispatch declines a task Retry because the
requester can no longer change the task, or its saved board message
changed, record a named wait (`retry_authority_denied`,
`retry_source_changed`) and log it. Before, it returned "deferred" with
no record.
- `task-retry.ts`: a new click with a new key replaces an unadmitted
intent that dispatch declined for its requester's access
(`retry_authority_denied`). The old intent becomes `skipped` /
`task_retry_replaced`, and activity records `replacedRequestId`. Waits
on shared state (cleanup, recovery, an active run, preservation) still
coalesce. The restart hook never replaces a user's click. Chat-owned
intents keep their own owner.
- `task-interruption-presentation.ts`: fixed public messages for the two
new wait reasons. `retry_authority_denied` keeps Retry, because a new
click is a fresh attempt. `retry_source_changed` is terminal: an edited
saved message cannot be replayed, so Retry is hidden and the user sends
a new message.
- `doc/execution-semantics.md`: document the rule.

## Verification

- New regressions in `server/src/services/workspace-recovery.test.ts`
use the real recovery, Retry and dispatch services on embedded Postgres.
The fixture runs the automatic Retry from a lost runner, then that
Retry's own run loses its runner, and recovery saves its files:
- `starts exactly one run when Retry is pressed after the automatic
Retry's own run lost its runner` (also checks that a replayed dispatch
and a second click start no second run).
- `does not let a Retry that could not start swallow a later Retry`: red
before the fix in two steps. With no fix, the declined intent has no
wait reason. With only the wait fix, the second click returned the first
request ID. Green after the fix: one run, bound to the second click.
- `dispatches a Retry accepted during the pre-deploy drain once the
restarted server resumes`: green before and after. The drain does not
lose a saved Retry.
- `npx vitest run src/services/workspace-recovery.test.ts
src/services/task-retry.test.ts
src/services/task-interruption-presentation.test.ts
src/services/quarantined-task-retry.test.ts
src/services/explicit-native-continuation.test.ts`: 892 passed.
- Not verified on staging. After deploy, an intent that dispatch refuses
shows a named `retryWait` reason.

## Risks

- Low risk. Only intents that dispatch declined for their requester's
access are replaced. All other coalescing stays.
- A replaced intent's late dispatch finds it `skipped` and starts
nothing, so a double click still starts at most one run.
- The exact gate that held the staging intent is not confirmed from
records. If it was a shared-state wait, the new code names it only if it
is one of the two reasons above.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, 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
- [ ] 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-06 03:52:21 -06:00
DottaandClaude Opus 5.5 7e3caa7bac fix(tree-control): resume work a pause gate refused after release (#19421)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The board can pause an issue tree. A pause stops work. Resume or
release must continue that work once.
> - PR #19394 made release re-dispatch a run that a pause Stop
cancelled.
> - That rule covers only a run that an operator Stop ended and that the
hold snapshot named.
> - A scheduled retry that comes due during a pause is not stopped. The
dispatch gate cancels it with `issue_paused`. The release did not see it
as owed work, so the task stalled with no run.
> - This pull request makes every gate that refuses work for a pause
record the covering hold. Release then re-dispatches that work exactly
once.
> - The benefit is that a paused task with a parked retry or a queued
run continues after release, with no manual wake.

## Linked Issues or Issue Description

Refs #19394

**What happened?**
A child task had a `workspace_busy` scheduled retry. The board paused
the parent tree. The retry came due during the pause. The
scheduled-retry gate cancelled it with `issue_paused` ("Scheduled retry
suppressed because the issue is held by an active subtree pause hold").
The board released the hold. Four minutes later the child was still
`in_progress` with no run, no execution blocker, and no new activity.

**Expected behavior**
Release (or `mode: "resume"`) re-dispatches the refused work exactly
once. No reconciliation and no user message are necessary.
Stranded-issue recovery does not add a second run.

**Steps to reproduce**
1. Give an assigned child task a parked `workspace_busy` scheduled
retry.
2. Pause the parent tree.
3. Let the retry come due while the pause is active. The gate cancels it
with `issue_paused`.
4. Release the pause. No run starts for the child.

**Paperclip version or commit**
master at 4d49a31286 (includes #19394).

**Agent adapter(s) involved**
Not adapter-specific (core bug).

## What Changed

- `issue-tree-control.ts`: add `ISSUE_PAUSED_ERROR_CODE` and
`pauseSuppressionResult()`. A gate records `treeHoldSuppression: {
holdId, rootIssueId }` on a run that it refused for a pause.
- `findOwedPauseResume` now treats the latest run as owed when it is an
`issue_paused` cancellation under a pause hold. It uses the recorded
hold. For an unmarked run (a scheduled retry cancelled before this
change), it uses a snapshot pause that was active at cancellation. The
operator Stop rule does not change.
- Scheduled-retry gate (`run-dispatch/adapters/postgres.ts`,
`cancelSuppressedRetryInTx`): locks the pause row `FOR SHARE` and
records the covering hold. A concurrent release either sees the refusal,
or the retry sees no pause and runs normally.
- Queued-run claim gate (`heartbeat/queue.ts`): cancels with
`issue_paused` and the hold marker, after the agent start lock is
released. Before, the cancel re-entered that lock from inside it, and
the agent's queue stalled until the 30-second stale-lock timeout. After
the refusal is saved, the gate re-checks the hold and resumes the task
itself if release already finished (`resumeIfPauseAlreadyReleased`).
- Release (`issue-tree-resume.ts`, route): revisits the snapshot members
and every task with a run refused for that hold, also when `wakeAgents`
is set. This covers tasks created under the tree after the snapshot and
tasks moved out before release.
- `packages/db`: partial index
`heartbeat_runs_tree_hold_suppression_idx` (migration 0436, `CREATE
INDEX CONCURRENTLY`, registered as a top-level migration) for that
lookup.
- Docs: `doc/execution-semantics.md` (gate-refused work, race handling,
non-assignee and pre-fix scope) and `doc/DATABASE.md` (index).

## Verification

- Red before the fix, green after: `pnpm --filter @paperclipai/server
exec vitest run src/__tests__/issue-tree-pause-resume-run.test.ts` (21
tests).
- "resumes a parked retry the pause suppressed exactly once after
release / resume / legacy": before, `expected [] to have a length of 1`
(no resumed run). After, one `issue_tree_resumed` run with
`interruptedRunId` = the suppressed retry, no execution blocker, and the
task finished. `reconcileStrandedAssignedIssues` added no run during the
hold or after release.
- "resumes a queued run the claim gate refused during the hold exactly
once": before, the test timed out at 15 s (start-lock re-entry). After,
the run is cancelled with `issue_paused` and the hold marker, and
release re-dispatches it once.
- "resumes a refused retry on a task moved out of the paused tree before
release" and "... on a task outside the snapshot when release also wakes
members": review findings, one resume each.
- "leaves a due retry parked when the pause is released while the gate
refuses it": the test waits until the gate blocks on an uncommitted
release. The retry is not cancelled and runs once. Without the `FOR
SHARE` lock the gate does not wait and the test fails.
- "resumes work a gate refused after the release that should have
resumed it had finished": the late-refusal re-check resumes once, and a
repeated check does not dispatch again.
- "does not resume a paused-code cancellation that no pause hold
covered": negative case.
- Related suites pass locally: `src/__tests__/issue-tree*`,
`src/modules/run-dispatch`, `src/services/heartbeat/queue.test.ts`,
`heartbeat-dependency-scheduling`, `heartbeat-workspace-busy`, and
`packages/db` `client.test.ts`.
- I did not run a live staging check here. Staging APIs are not
reachable from this environment.

## Risks

- Migration 0436 adds a partial index on `heartbeat_runs`. It is built
concurrently and covers only cancelled runs with the marker, so it is
small.
- The scheduled-retry gate now takes a `FOR SHARE` row lock on the pause
hold before it cancels. Release updates that row, so the two serialize
briefly. Release does not lock issues or runs, so the lock order cannot
deadlock with the gate's issue-then-run locks.
- The queued-run claim gate now records `issue_paused` in place of the
generic `cancelled`. Stranded recovery already classifies `issue_paused`
as non-retryable. The pause resume owns that work, so recovery does not
add a second run.
- Scope limits (documented): a non-assignee run refused by a pause is
not replayed, the same as non-assignee wakes that admission drops during
a hold. Among pre-fix rows, only scheduled retries carry `issue_paused`,
so only those fall back to the snapshot. A pre-fix row whose hold is
already released does not self-recover; the staging task that showed
this bug needs one manual wake, or a new pause and release.

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

## Model Used

- Claude Opus 5.5 (Anthropic), model ID `claude-opus-5-5`, used through
Claude Code with tool use (shell, file edits, 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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 03:03:15 -06:00
Devin FoleyandPaperclip bf53060904 fix(recovery): continue saved input after verified workspace copy-back (#19422)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native execution preserves accepted results and task files after a
failure.
> - A later user message can request a new turn after the old execution
stops.
> - File recovery can finish by copying files back or by retaining a
verified archive.
> - The retained-result gate accepted the archive proof but missed a
verified copy-back receipt.
> - This change accepts that existing receipt for terminal failures and
preserves execution authority and prior results.

## Linked Issues or Issue Description

**What happened?**
A task keeps a user message queued with `finalizing_saved_result` after
its old native coordinator reached `terminal_failure`. The accepted
result and a successful, exact `workspace_recovery` copy-back receipt
are saved. The gate only checks the alternate
`workspace_recovery_snapshot` receipt. An older failed result can also
keep a newer failed turn's saved input blocked.

**Expected behavior**
A new user message or deliberate task Retry can continue from verified
recovered files after the old execution stops. Preserve the old result
and unknown action outcomes. Keep an active finalizer or missing stop
evidence blocked.

**Steps to reproduce**
1. Retain an accepted result when a native run reaches terminal failure.
2. Complete host copy-back with the exact run, allocation, descriptor
and final file digest.
3. Save a new user message or request task Retry.
4. Observe that admission still reports `finalizing_saved_result`.

**Paperclip version or commit**
Reproduced against `4d49a3128613b282fb344272ed7ca15f98dea2ac`.

**Deployment mode**
Authenticated native execution with retained workspace recovery. Local
regressions use disposable PostgreSQL and the real heartbeat queue. They
start no model provider or sandbox.

Related work: #19355 covers saved input after Stop. #18267 concerns
manual reconciliation of duplicate legacy holds; it excludes workspace
and system recovery and does not fix this receipt gate. I searched open
PRs and issues before starting this change.

## What Changed

- Reuse the existing host-verified repair receipt when an accepted
result belongs to a coordinator in `terminal_failure`.
- Retain the separate checks for active controllers, environment
termination, live executions, questions, approvals and current user
authority.
- Add message, saved-message and task Retry regressions, plus concurrent
queue delivery and invalid-receipt cases.
- Document this continuation path. Add no schema, API or background
worker.

## Verification

- All 483 tests pass across explicit continuation, task Retry,
quarantined Retry and finalized recovery migration suites.
- The final focused fixture has 17 cases. All pass with the change. With
the original production gate restored, all four positive cases fail and
all 13 safety cases pass.
- Negative cases cover missing, unfinished and mismatched receipts,
another task or source, unrecovered files, a live finalizer, an active
controller, missing remote stop and active execution.
- The real heartbeat path delivers one saved message under concurrent
recovery calls. Later recovery calls create no second turn. The old
accepted result and recovery budget stay unchanged; unknown outcomes
stay unknown.
- The first delivery fixture omitted the newer failed source needed by
the recovery scanner. I corrected the fixture to preserve that history
and repeated the unchanged-code comparison. Earlier failure logs remain
in the verification record.
- `pnpm -r typecheck`: passed.
- `node scripts/check-module-boundaries.mjs` and `git diff --check`:
passed.
- `pnpm build`: passed.
- Full local `pnpm test:run`: running with a 20-minute bound; no
full-suite pass is claimed. All hosted CI checks passed. Greptile scored
the exact head `d04fb98892fd77853ad03e6b44cd612e805abc13` 5/5 with no
findings or review threads.
- Hosted delivery of the already saved input still requires a qualified
staging build containing this change.

## Risks

The new path requires the existing exact repair receipt and a terminal
failure. A coordinator still accepting or committing a result keeps the
previous hold. Stop, owner and live-run checks still run before
admission. The old result is neither applied nor replayed. Receipt
verification adds bounded indexed reads only on this retained-result
failure path. No migration or provider configuration changes are
required.

## Model Used

OpenAI GPT-6.1 Sol (`gpt-6.1-sol`) through Codex in T3 Code, with xhigh
reasoning. The context-window size is not exposed. Used code editing,
tool calls and local test execution.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used, including the available runtime
details
- [x] I have checked ROADMAP.md and confirmed this is a scoped bug fix
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have described the issue in this PR using the bug-report fields
- [x] I have not referenced internal or instance-local Paperclip issues
or links
- [x] My branch name describes the change and contains no internal task
id
- [x] I have run the affected tests locally and they pass
- [x] I have added regression tests and updated relevant documentation
- [x] I have considered and documented the risks
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open findings
- [x] I will address all review findings before requesting merge

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 00:15:30 -08:00
Devin FoleyandPaperclip 11bc490cda fix(recovery): keep a stopped copy-back's files instead of holding the workspace (#19385)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A native run can have its result accepted before the copy-back of
its sandbox files finishes. If the copy-back fails, it retries.
> - A board Stop during those retries ends the run. It sets the run's
finalization to `terminal_failure` / `native_retry_cancelled`, and the
result stays accepted.
> - After that, nothing exports the files. The finalization worker skips
terminal rows, and cancelled-workspace preservation skipped any run with
an accepted result.
> - The task workspace stays dirty and bound to the stopped run. Every
later run on the task waits for the workspace, then fails setup with the
unfinished-writer error.
> - This pull request lets cancelled-workspace preservation also admit
an accepted result whose copy-back a Stop ended.
> - The benefit is that stopping a stuck copy-back saves its files and
frees the task, like stopping any other run.

## Linked Issues or Issue Description

Refs #16700, #17677, #18820, #19304 and #19305 (cancelled-workspace
preservation). This pull request adds the accepted-result case to that
path and keeps its other checks unchanged.

**What happened?**

On a hosted instance, a native run's result was accepted, but its
copy-back kept failing and retrying. A board operator pressed Stop to
unblock the task. The run became `cancelled`, its sandbox was stopped
and kept, and its finalization moved to `terminal_failure` /
`native_retry_cancelled` with the result still accepted.

The next run on the task was cancelled with "Shared execution workspace
is busy: run … ended, but its workspace save has not settled". About 24
automatic retries followed over an hour. The last one failed with "The
workspace still has an unfinished writer or save operation." After that
nothing ran: the task stayed `in_progress` with no run and no recovery
action. A Retry or a new comment would hit the same block.

The workspace stayed `dirty: true` with the stopped run as its writer,
and no `workspaceRestoreRecovery` was ever recorded. The reason:
`preserveCancelledNativeWorkspaces` requires `resultId IS NULL`, and
`registerCancelledWorkspacePreservationWork` and
`preserveNativeFailedWorkspace` both return early when `resultId` is
set.

**Expected behavior**

Stopping a run during its copy-back retries keeps its files, as stopping
any other run does. Paperclip's workspace recovery saves them and
releases the workspace. Nothing commits or replays the stopped result.

**Steps to reproduce**

1. Accept a native result for a run whose copy-back is in
`retryable_failure`.
2. Stop the run with the board Stop. The finalization becomes
`terminal_failure` / `native_retry_cancelled`.
3. Start another run on the task. It waits for the workspace and then
fails setup. No save of the stopped run's files is ever recorded.

The new database tests run steps 1 and 2 against PostgreSQL and check
that the save is recorded.

**Paperclip version or commit**

`master` at `9ceaec6f97e`.

**Deployment mode**

Any deployment that uses remote (sandbox) task workspaces. Seen on a
cloud-managed instance.

## What Changed

-
`server/src/services/native-runtime/native-failed-workspace-recovery.ts`:
cancelled-workspace preservation now also admits an accepted result when
the finalization is `terminal_failure` with `native_retry_cancelled`.
That is the state a Stop leaves after ending copy-back retries. All
three entry points use the same rule:
  - the startup scan `preserveCancelledNativeWorkspaces`;
  - the worker `registerCancelledWorkspacePreservationWork`;
- `preserveNativeFailedWorkspace`, only when it is called with
`requireCancelledStop`.

Preservation still requires everything it required before: a cancelled
run, an acknowledged Stop, no live finalization owner or controller
lease, a prepared workspace reference, and the exact retained source.
- An accepted result with any other failure code is still skipped; those
runs keep their own export repair path. Failed (not cancelled) runs are
unchanged.
- The accepted result stays recorded and uncommitted. Preservation does
not change the task's status or queue a run.
- `doc/execution-semantics.md`: describe the stopped copy-back case in
the paragraph on writers stopped mid-run.
- `native-cancelled-workspace-preservation.test.ts`: move the
accepted-result fixture into an `acceptResult` helper, and add a test
with three cases.

## Verification

- Failing first. The new test "preserves files of an accepted result
whose copy-back was stopped" has three cases: worker, startup scan, and
a sandbox that the Stop already stopped and kept, through the real
`prepareSandboxStopAndRetain` and `settleStopOnlyCleanup`. All three
fail on `master`, with no `workspaceRestoreFailure` recorded, and pass
with this change.
- Each case checks that:
- the save obligation and recovery action are recorded for the exact
lease;
  - `inspectWorkspaceRecoverySource` finds one source;
- the finalization keeps its `resultId`, `terminal_failure` and
`native_retry_cancelled`;
- the workspace stays held by the stopped writer until the save
publishes;
  - no run or wake is created, and the task status is unchanged.
- The existing "does not admit preservation for accepted-result" case
still passes. It uses an accepted result with a different failure code.
- `cd server && npx vitest run
src/services/native-runtime/native-cancelled-workspace-preservation.test.ts`:
44 passed.
- Related suites, 10 files and 854 passed:
- `native-failed-workspace-recovery`, `workspace-recovery`,
`heartbeat/run-control`
  - `native-cancellation-request` (unit and integration)
- `heartbeat-remote-workspace-busy`, `native-workspace-export-retry`,
`native-workspace-reconstruction`
  - `native-stop-accounting`, `task-workspace-binding`
- `cd server && npx tsc --noEmit -p tsconfig.json`: pass.
- Not run end to end on a live instance. Diagnosed from that instance's
run, finalization, lease, workspace and recovery-action rows.

## Risks

- On deploy, the startup scan and the worker will pick up existing
cancelled runs in this state and record their saves. That is the
intended repair, and it uses the same retention checks as other stopped
writers.
- A copy-back interrupted by a server restart can leave a
`remote_workspace_snapshot` operation marked `running` after its process
is gone. This change does not settle such rows. If one blocks the later
save, the save follows its existing retry and board hand-off.
- No schema migration.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`) through Claude Code, with tool use
(shell, file edits, test runs, read-only database queries). The runtime
does not expose the context window or reasoning configuration.

## Checklist

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

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 00:03:41 -08:00
DottaandPaperclip c1d567ff6a fix(workspaces): isolate archived nested Git reconstruction (#19409)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Durable workspace import must preserve archived task files and Git
history.
> - A saved nested checkout can be a linked worktree whose branch
belongs to its old absolute path.
> - Archived import reconstructs that checkout in a new scratch
directory.
> - Ordinary linked recovery rejects the old branch at that new path. It
can also write into a cache used by other import workers.
> - This pull request reconstructs archived nested checkouts as private
standalone repositories.
> - Each import preserves saved files and extra history without changing
shared worktree registrations or branches.

## Linked Issues or Issue Description

**What happened?**
Archived workspace import fails when a saved nested linked checkout has
a branch owned by the old path. The new scratch path fails the
linked-worktree ownership check. The existing cache lock is
process-local, so separate import processes also share mutable Git state
without a common lock.

**Expected behavior**
Archived import must create an independent portable copy of the saved
checkout. It must retain the saved commit, branch name, staged edits,
working files, ignored recovery files, settings, and extra saved
commits. It must keep the source cache unchanged and enforce source
authorization.

**Steps to reproduce**
1. Create a managed nested linked checkout with an unpushed commit and
staged and unstaged edits.
2. Save an encrypted restore point, including ignored files and an extra
history tip.
3. Restore its saved contents under a different absolute workspace path.
4. Before this repair, the new path cannot adopt the branch owned by the
old path.

**Paperclip version or commit**
The real Git regression fails before the repair at `fc1f5f3052`. The
final branch includes the merged interrupted-upload and nested-source
authorization fix from #19396.

**Deployment mode**
Archived remote workspace migration with real Git repositories,
disposable PostgreSQL, and encrypted object storage. Local integration
tests use the filesystem storage provider.

Builds on merged #19402 and #19396. Related #19374 concerns interrupted
clone staging; it does not implement private archived reconstruction. No
duplicate open repair was found.

## What Changed

- Enable private repository reconstruction only for archived migration.
Ordinary linked recovery keeps its path ownership guard.
- Add an object-only source to the managed checkout owner. A verified
cache supplies only the saved commit and authenticated bundle
prerequisites through scoped fetches. The private copy includes no
unrelated task refs or objects, hard links, object alternates, or
current cache files.
- Fetch missing authenticated bundle prerequisites before importing a
saved bundle in both private reconstruction and ordinary linked
recovery. A pruned cache can use the approved remote; unavailable
history still blocks restore.
- Keep every authenticated restore point's extra commit under a private
ref in root and nested private copies so portable archive capture and
garbage collection retain it. Ordinary recovery keeps its existing
history and expiration rules.
- Share the verified, bounded ignore-file copy between root and nested
reconstruction. Sources remain unchanged.
- Retain source authorization before and after reconstruction and the
final source-row locks from #19396.
- Test concurrent private copies, source rejection, foreign task refs
and objects, ordinary linked prerequisite recovery, a real migration,
and cold restore after removing all original repositories. Test hard
links, oversized excludes, unsafe info paths, and replacement during a
descriptor read.

## Verification

- Final affected run: 292 tests pass across ten suites. These cover real
Git repositories, encrypted restore points, disposable PostgreSQL
migration, portable archives, the restore-point ledger, source
rejection, and concurrent copies.
- Full repository typecheck and build pass after the scoped-object
repair. Final server typecheck and full build also pass after
restricting preserved refs to archive mode.
- RED then GREEN: a linked checkout at a different path; a pruned bundle
prerequisite still available from the approved remote; foreign task refs
and objects entering the archive; root extra history disappearing after
cold restore; and archive-only refs blocking ordinary standalone
expiration.
- Two concurrent private copies retain the saved head, branch, staged
and unstaged edits, ignored recovery bytes, settings, and modes. Source
refs, index, worktree registrations, and working files remain unchanged.
- Delete the original checkout, cache, remote, and private copies after
portable capture. Cold restore retains the saved files and extra history
after garbage collection. No alternates or unrelated task objects
remain.
- Ordinary linked recovery retains its path ownership check and recovers
pruned prerequisites. New and existing ordinary standalone destinations
have no archive-only refs and remain eligible for expiration after
normal object cleanup.
- Unsafe exclude tests reject hard links, oversized files, nonregular
paths, info-directory substitution, and replacement during the
descriptor read. They preserve the target and replacement source bytes.
- Preserve the failed attempts: an initial migration fixture changed its
home after constructing the source path. Later expiration fixture
attempts used a filtered current-head tip, omitted the local-source
callback, or used cleanup that generated Git server-info files or left
packed unreachable objects. Correct those setup assumptions before the
actual regressions fail. Final affected suites pass.
- Two earlier local full-suite attempts were interrupted before editing
valid review findings. Neither is reported as a full-suite PASS.
Final-head full CI passes; review is 5/5 with no open findings.

## Risks

Archived imports use additional private disk space for independent Git
objects. Missing, changed, or unsafe sources still block import. The
repair does not weaken the default linked-worktree ownership check,
change stored formats, or update live provider state. The shared
checkout addition is optional and has no effect for existing callers
that do not supply an object source.

## Model Used

OpenAI GPT-6 in Codex, with code editing, tool use, and test execution.
The exact runtime model identifier and context-window size are not
exposed in this session.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 00:56:06 -06:00
DottaandClaude Opus 5.5 90b8190c33 fix(native-runtime): detect half-open runner ingress and project Stop for live runs (#19407)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native runs in a managed sandbox talk to the server through a
provider-ingress WebSocket. The server opens this socket to the runner
(`paperclip-runnerd`) through the sandbox preview proxy.
> - A staging run ran a 20-minute foreground shell command that printed
nothing. After the first second, no run events arrived. The run stayed
"Working" for over 35 minutes. No watchdog acted, and the run projection
said `canStop: false`.
> - The server only detected a lost runner connection from a `close` or
`error` event. It never sent pings. A half-open route, for example a
proxy that drops the connection without a FIN, never emits `close`. The
run then waits forever and the 7-minute reconnect grace never starts.
> - A long silent tool call is legitimate. The fix must tell "quiet but
alive" apart from "nothing alive". A protocol ping does that: runnerd
answers every ping with a pong while its provider waits on the tool.
> - This pull request pings authenticated runner connections and
terminates the socket after 90 seconds of total silence. The existing
reconnect grace and transport-failure path then recover the run. It also
makes the execution projection report `canStop: true` for every queued
or running run.
> - The benefit is that a run with a dead runner connection now ends in
a bounded time instead of hanging. API readers also see the Stop that
the board already offers.

## Linked Issues or Issue Description

**What happened?**
A native-runner run (managed sandbox, OpenCode with an OpenRouter model)
ran a foreground shell command for about 20 minutes with no output. The
last run event came at about T+1s. The command finished on disk, but the
run never got another event and stayed `running` / "Working" for over 35
minutes. Its projection had `canStop: false` and `permittedActions:
["inspect_run"]`. A board cancel through the API worked.

**Expected behavior**
The server detects a dead runner connection and recovers or fails the
run in a bounded time. A runner that is alive but quiet stays attached.
The projection reports that a live run can be stopped.

**Steps to reproduce**
1. Start a native run in a managed sandbox.
2. Make the agent run a long foreground command that prints nothing.
3. Let the preview route drop the WebSocket without a close frame.
4. Before this fix, the run stays "Working" forever.

## What Changed

- `server/src/realtime/runner-prp-outbound.ts`: after PRP
authentication, `WsJsonWireConnection.monitorLiveness` sends a WebSocket
ping every 20 s. Any inbound frame or pong counts as liveness. After 90
s with nothing inbound, it terminates the socket. That emits an ordinary
`close`, so the PRP authority and the reconnect loop use the existing
7-minute grace and then fail the transport. If the server's own event
loop stalls, the window restarts instead of blaming the runner. The 90 s
bound is longer than runnerd's own 60 s connection lease, which already
requires runnerd to read the socket.
- `server/src/services/execution-projection.ts`: `canStop` is now `true`
for every `queued` or `running` run. The old failed-native-retry rule is
unchanged.
- `server/src/services/execution-projection.ts`: a queued run now shows
"Waiting for previous run to stop" (cause `previous_run_settling`) when
a predecessor that admission checks (the task execution owner, the
retried run, or the agent's latest settled run on the task) still has
pending cleanup by `hasPendingRunCleanup`. Before, a turn queued after a
Stop showed only "Queued" with no reason while the stopped run's
controller still waited on the dead runner. That in-process wait
(`claimQueuedRun` settling-owner gate) also made Retry a no-op. The
liveness bound above makes that controller settle. Its existing cleanup
then calls `startNextQueuedRunForAgent`, which starts the queued turn.
- `server/src/services/run-cancellation.ts` and
`server/src/modules/run-dispatch/adapters/postgres.ts`: a user-stopped
native run whose runner was unreachable can later settle as `failed`
(`native_provider_terminal_failed`) when its transport fails closed. The
dispatch gate's message deferral required a `cancelled` source and
compared the message with `finishedAt`. The user's follow-up message was
therefore marked `skipped`, and the task asked for a second message. The
new `recordedUserStop` helper honors the recorded user Stop on such a
failed source, so the gate saves the message for the deferred-receipt
worker.
- `packages/shared/src/types/execution-projection.ts`: documents the
`canStop` meaning.
- `doc/execution-semantics.md`: documents the ingress liveness bound and
the new `canStop` meaning.

## Verification

- Red then green: `runner-prp-outbound.test.ts` "detects a half-open
authenticated ingress and fails the run within a bounded window". On the
old code it failed because no ping was sent and the socket was never
terminated. It now passes and the active-run `failure` rejects with
`runner_ingress_unavailable`.
- Safety: "keeps a quiet but live runner attached through a long silent
tool call" checks that a runner answering pings stays attached for 20
simulated minutes with no termination and no failure.
- Red then green: `execution-projection.test.ts` "offers Stop for every
live run, including a long-silent native turn". On the old code it
failed with `canStop: false`.
- Red then green: `execution-projection-recovery.test.ts` "names the
wait when a stopped predecessor still holds the task's runner" (real
Postgres). On the old code the successor projected a plain "Queued". It
also checks that the wait clears on lease release and ignores another
agent's run.
- Red then green: `workspace-recovery.test.ts` "continues a message sent
after Stop when the stopped run later failed closed". On the old code it
failed 3 of 3 runs with the receipt `skipped`. It now passes, and the
full file passes twice (392 tests). The test now pins the message after
the recorded Stop. Before, the fixture could write both in the same
millisecond, which made the sibling variants flaky.
- Passed: run-cancellation, explicit-native-continuation, run-dispatch
and run-control suites (622 tests).
- Passed: `npx vitest run src/realtime/
src/services/execution-projection*.test.ts
src/__tests__/heartbeat-run-privacy-routes.test.ts`, the
native-runner-execution-driver tests, and the plugin-runner-task tests.
- Passed: `pnpm --filter @paperclipai/server typecheck`.
- I read runnerd (`runner-core/src/durable/transport.rs`) and confirmed
it answers `Ping` with `Pong` in its non-blocking read loop.
- Not run: a live managed-sandbox reproduction. Staging and Daytona were
not reachable from this environment. `plugin-runner-probe.test.ts` needs
a local cargo build of runnerd, and it did not start in this worktree.

## Risks

- A false termination is possible if a runner fails to answer pings for
90 s while it is still alive. runnerd already loses its own connection
lease after 60 s without reading, so this bound does not add a stricter
requirement. A terminated live runner reconnects within the existing
grace.
- If the preview proxy drops WebSocket control frames, ping alone does
not prove liveness. Inbound PRP frames (runnerd's lease renewals, about
every 30 s) still count, so a live runner stays attached.
- `canStop` now means "the board can Stop now" for live runs. The UI
already shows Stop for running and queued runs without reading this
hint, so the UI does not change.
- Out of scope: the inbound runner WebSocket server
(`runner-prp-ws.ts`), and a lost OpenCode SSE event while the runner is
alive. A stopped run whose controller hangs for a reason other than a
dead transport still holds the queued successor. The projection now
names that wait, but this PR does not force-release it, because the
controller has no termination evidence. A queued reason for the agent
concurrency cap is also not covered. The wait label calls the admission
predicate (`hasPendingRunCleanup`) for up to three predecessor runs per
queued run. This adds a few reads to projections of queued runs only.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 00:52:01 -06:00
Dotta 92452524f4 fix(server): recover authenticated empty retention checkpoints (#19417)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Ephemeral server enrollment must preserve existing task files.
> - Archived task directories can depend on encrypted retention
checkpoints.
> - A missing object blocks migration even when its authenticated
contents were exactly empty.
> - Metadata counters alone cannot prove that no saved files existed.
> - This change verifies the exact candidate against the original
encrypted key envelope before normal migration publication.
> - The benefit is lossless migration of this bounded case without
resetting task history or weakening ordinary restore.

## Linked Issues or Issue Description

**What happened?**
Migration stops at an archived empty directory when its retained
encrypted object is missing. The original encrypted key envelope still
authenticates the payload hash, byte length, company, workspace,
checkpoint ID, and generation.

**Expected behavior**
Administrative migration can reconstruct those exact original bytes when
storage confirms absence and every authenticated check passes. It must
keep missing nonempty, expired, corrupt, or ambiguous sources blocked.

**Steps to reproduce**
1. Capture and encrypt an empty task directory.
2. Retain its reference and encrypted key envelope.
3. Remove the old directory and storage object.
4. Run the normal remote workspace migration.

**Paperclip version or commit**
The regression fails before this change at `205ffcb606`. Related
preservation work is in #19396, #19402, and #19409. No duplicate open
fix was found.

**Deployment mode**
Administrative migration to external workspace storage. Tests use real
encryption, real filesystem storage, and disposable PostgreSQL.

## What Changed

- Let the encryption owner verify a candidate against the existing
scoped encrypted key envelope. Normal ciphertext validation keeps its
previous order and checks.
- Share the task-directory encoder with an exact empty payload
constructor.
- Add a default-off loader option for confirmed missing empty
checkpoints. Only the administrative migration caller enables it.
- Keep the original checkpoint reference and object unchanged. Publish
the recovered bytes through the existing snapshot and source-fence
owners.
- Reject forged counters, wrong identities, missing nonempty data,
present corruption, unreadable present objects, unknown absence, and
expiry.

## Verification

- Final affected run: all 238 tests pass across five crypto, snapshot,
retention, and migration suites. Full repository typecheck and build
pass; server typecheck and full build also pass after the cancellation
repair.
- RED then GREEN: exact missing-empty recovery and cancellation of a
stalled HEAD through the real storage-service boundary. Cancellation
retains the caller's original abort reason.
- Disposable PostgreSQL migration publishes a cold-restorable empty
archive. Archived status, task status, task binding, original reference,
and the absent original source stay unchanged. Replay keeps one
canonical snapshot.
- Negative cases reject forged empty counters, an authenticated
directory or file, every foreign identity field, expiry, present
corruption, unreadable present objects, unknown absence, and missing
nonempty data. Ordinary restore keeps its previous failure behavior.
- Initial verification retained one new fixture-path failure after 145
passes, and a typecheck rejected the fixture's widened schema literal.
Correct the fixture to use the returned staging path and its explicit
reference type. The final complete affected run passes.
- The new HEAD request runs only after a failed read with the
administrative option enabled. It uses the existing cancellation
contract and adds no polling or background work.
- Final-head full CI passes; review is 5/5 with no open findings. This
PR does not claim deployed enrollment or a live migration result.

## Risks

Recovery requires the original valid encrypted key envelope and an exact
format match. A format change or unavailable key continues to block
migration. Existing ciphertext hash, size, authentication, and ordinary
restore behavior remain enforced. This change has no schema,
provider-session, API, or UI contract changes.

## Model Used

OpenAI GPT-6 in Codex, with code editing, tool use, and test execution.
The exact runtime model identifier and context-window size are not
exposed in this session.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` 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 references)
- [x] My branch name describes the change and contains no internal
Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-11-06 00:50:28 -06:00
DottaandClaude Opus 5.5 a4bb059f59 fix(heartbeat): retry transient sandbox provider lease refusals and explain the block (#19412)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A task runs in an execution environment. A managed sandbox
environment gets a lease from a sandbox provider (for example Daytona)
before the agent starts.
> - When the provider answered sandbox create with a temporary error
(HTTP 502), the run failed as the generic `setup_failed`.
> - `setup_failed` is the fail-closed code for unknown setup errors. It
gets no automatic retry, so the task went to `blocked` at once. The task
showed only "Couldn't finish this attempt".
> - A user had to find and press Retry. The Retry then worked, which
shows that the failure was temporary.
> - This pull request classifies temporary provider refusals during
lease acquisition from structured fields. It retries them within the
existing bounded bootstrap budget, and it explains the block when the
retries are used up.
> - The benefit is that a short provider outage no longer stops a task.
When the outage continues, the task shows the cause and a Retry that
works.

## Linked Issues or Issue Description

**What happened?**
A task was assigned to an agent that uses a managed Daytona sandbox
environment. Sandbox creation returned `HTTP 502`, and the cleanup of
the provisional sandbox could not be confirmed. The run failed with
`setup_failed` and the error `Failed to acquire lease for environment
"Paperclip Sandbox" (sandbox): Daytona sandbox creation failed (HTTP
502); cleanup could not be confirmed for paperclip-create-…`. Three
seconds later the task was `blocked`. No automatic retry was scheduled.
The task showed no specific reason. A manual Retry started a new run
that completed normally.

**Expected behavior**
A temporary provider error (HTTP 5xx, 429, timeout) before the agent
starts must retry automatically with bounded backoff. The task must show
why it waits. If the retries are used up, the task must show the cause,
a next action, and a Retry that works on the first click.

**Steps to reproduce**
1. Configure an agent with a sandbox environment.
2. Make the provider return HTTP 502 on sandbox create.
3. Assign a task to the agent.
4. See that the run fails with `setup_failed` and the task goes to
`blocked` with no retry.

**Paperclip version or commit**
`master` at `612da1e8e0`.

## What Changed

- Plugin SDK: an `environmentAcquireLease` error now carries a closed
diagnostic envelope (HTTP status or a closed errno code) across the
worker RPC. It takes the status of the create call only, not of the
compensating cleanup lookup. The existing creation-cleanup ownership
envelope is unchanged. New module: `environment-acquire-error.ts`. The
resume diagnostic helpers are shared.
- Server: new `EnvironmentProviderUnavailableError` and code
`environment_provider_unavailable`. The environment run orchestrator
sets it as the cause of `lease_acquire_failed` only when structured
evidence shows a transient status (408, 425, 429, 500, 502, 503, 504, or
a closed connection errno). It never parses message text. Saved-sandbox
resume and stopped-worker refusals keep their own codes.
- Setup failure (`failRunSetup`): a fresh, pre-dispatch run records
`environment_provider_unavailable` with a `providerFailure` (phase
`startup`, retryability `transient`). The run uses the bounded bootstrap
retry (30 s, then 60 s). An accepted interaction continuation keeps its
own continuation retry, as for `plugin_worker_unavailable`.
- Recovery: the new code is registered with the other bootstrap codes.
It is not sent to the generic continuation retry. It does not create an
execution-reconciliation hold, because no provider work started. On
exhaustion the stranded notice has the title "Sandbox provider was
temporarily unavailable" and a specific next action.
- UI: `ProviderFailureNotice` labels the `sandbox` provider as "Sandbox
provider". The task notice shows the provider failure, "Paperclip will
retry automatically" while a retry is scheduled, and Retry after
exhaustion.
- Docs: `doc/execution-semantics.md` describes the policy and the
sandbox ownership rule.

## Verification

Red before the fix, green after the fix:

- `server/src/__tests__/heartbeat-process-recovery.test.ts` › "transient
sandbox provider refusal during lease acquisition":
- "retries automatically and runs exactly one successful successor
without user action": the real heartbeat. The lease runtime returns a
Daytona 502 once, then delegates. Before the fix: one `failed` run and
no successor. After the fix: one `scheduled_retry` successor with no
hold and a `retry_scheduled` presentation. After promotion, exactly one
successor succeeds.
- "explains the block with a working Retry once automatic retries are
used up": three refusals. The task is `blocked` with a "Sandbox provider
was temporarily unavailable" notice. The presentation is `{ state:
"failed", canRetry: true, providerFailure: { httpStatus: 502 } }`, and
no execution-reconciliation hold exists. One `requestTaskRetry` is
accepted, and its run succeeds.
- `server/src/services/heartbeat/run-completion.test.ts` › "routes a …
sandbox provider lease refusal through bounded bootstrap recovery": the
real orchestrator wrapping. Only the fresh, structured 502 is retried. A
message-only 502, a 400, a run after provider dispatch, and a retained
native run stay `setup_failed`.
- `packages/plugins/sdk/tests/environment-acquire-error.test.ts`: the
real worker RPC. The test checks the create status (not the 404 cleanup
lookup), the errno, no provider text, the unchanged cleanup envelope,
and that forged envelopes are rejected.

Commands:

```sh
pnpm --filter @paperclipai/plugin-sdk exec vitest run tests/environment-acquire-error.test.ts tests/environment-creation-cleanup.test.ts tests/environment-resume-error.test.ts
cd server && npx vitest run src/__tests__/heartbeat-process-recovery.test.ts src/services/recovery/ src/services/heartbeat/run-completion.test.ts src/services/heartbeat/retries.test.ts src/services/legacy-execution-recovery.test.ts src/services/task-interruption-presentation.test.ts src/services/task-retry.test.ts src/__tests__/environment-runtime.test.ts src/__tests__/heartbeat-retry-scheduling.test.ts
cd packages/plugins/sandbox-providers/daytona && npx vitest run
pnpm --filter @paperclipai/plugin-sdk typecheck && pnpm --filter @paperclipai/server typecheck
```

I did not run a live Daytona 502 on staging. Re-check on staging after
deploy (see Risks).

## Risks

- Sandbox leaks or double creation: each retry is a new run with a new
durable creation record and a new provider name
(`paperclip-create-<operationId>`). The failed attempt stays owned by
its own record, and the existing creation reconciler settles it. A retry
never reuses or abandons it. While a provisional sandbox waits for
reconciliation, up to two more sandboxes can be created for the same
task. This is the same as a manual Retry today.
- Plugins built with an older SDK do not send the envelope. Their
refusals stay `setup_failed`, which is the current behavior.
- An in-process error with `status`/`statusCode` 500–504 or a connection
errno during lease acquisition is now retried twice. The budget is
bounded and shared with the other bootstrap codes.
- An RPC timeout on the host side is not classified by this change.

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

## Model Used

- Claude Opus 5.5 (Anthropic), model ID `claude-opus-5-5`, used through
Claude Code as an agent with tool use (shell, file edits, test runs).
Extended reasoning was on.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 00:08:44 -06:00
DottaandPaperclip 66d3fa97a9 fix(workspaces): preserve ignored files in archived Git imports (#19402)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Durable workspace migration must preserve archived files and saved
recovery data.
> - An archived Git workspace can contain ignored recovery receipts and
bundles in its encrypted restore point.
> - The importer reconstructs a private repository from a verified
source before it applies that restore point.
> - Reconstruction omitted the source repository's local ignore rules.
The restored files then changed classification, and the exact
restore-point check failed.
> - This pull request copies the verified local ignore rules into the
private repository before restoration.
> - The source stays unchanged, and the original restore-point integrity
checks still apply.

## Linked Issues or Issue Description

**What happened?**
An archived workspace with saved files under an ignored recovery
directory failed migration with `restored_contents_mismatch`. The task
commit and staged index matched. The ignored files were restored, but
the private repository treated them as untracked work because its
`info/exclude` rules were missing.

**Expected behavior**
Reconstruction from an existing verified source must preserve its local
ignore policy. Saved ignored files must survive migration and a cold
object-store restore with their original classification. Import must not
follow a symlink to an unrelated ignore file or edit the source
repository.

**Steps to reproduce**
1. Exclude a recovery directory in the managed source repository's
`info/exclude` file.
2. Save staged edits, unstaged edits, an ignored recovery receipt, and
an ignored binary bundle in an encrypted restore point.
3. Archive and remove the task worktree.
4. Import the archived workspace. Before this change, restore-point
validation fails because the reconstructed repository lacks the local
exclusion rule.

**Paperclip version or commit**
Reproduced on master at `879140cbbd`.

**Deployment mode**
Cloud workspace migration with a retained verified Git source,
PostgreSQL, and encrypted durable object storage. The tests use real
disposable PostgreSQL, Git repositories, and encrypted filesystem-backed
object storage.

Related migration work: #18826, #19138, and #19396. No duplicate open
fix was found.

## What Changed

- Copy the verified source's `info/exclude` file into the private
reconstructed repository before applying its restore point.
- Bound the read to one MiB. Reject symlinked, non-regular, hard-linked,
or changing source files.
- Keep the original source bytes unchanged.
- Add regressions that check staged and unstaged edits, ignored recovery
files, cold restore, and rejection of a symlinked ignore file.
- Document the reconstruction behavior.

## Verification

- Before the repair, the saved-ignored-files regression fails with
`restored_contents_mismatch`.
- After the repair, both focused cases pass.
- At rebased head `81a0b1c041`, all 206 tests across migration, snapshot
storage, archive, and migration concurrency suites pass. The merged
interrupted-upload and nested-source approval regressions remain intact.
- Server package typecheck passes.
- Full repository typecheck and build pass at the rebased head. The full
local test suite is running. The prior local full-test run was
interrupted for rebase, and is not a passing result.
- Initial CI hit a 15-second timeout in unchanged
`activity-routes.test.ts`; the exact case passed in a focused local run.
The browser shard lost its self-hosted runner, as confirmed by its check
annotation; GitHub did not retain its full log. Those failures remain
recorded. An unchanged retry was superseded by the required conflict
rebase. At exact head `81a0b1c041`, all 53 CI checks and both required
aggregate gates pass; Greptile is 5/5. The full local suite remains in
progress, so no local full-suite PASS is claimed.

A second automated review noted the existing nested-checkout
counterpart. This PR fixes root reconstruction. The nested
linked-checkout repair and its saved-ignored-files/cold-restore
regression are implemented in #19409, which is required before the
migration rollout uses nested imports. This PR leaves that sibling
recovery path unchanged. The two identical scope warnings are addressed
through that linked repair.

## Risks

The copy applies only when an archived Git restore-point source is still
present and verified. It changes only the private reconstruction
directory. Missing sources retain the existing restoration path. Unsafe
or unstable local ignore files block import and retain the original
migration obligation. No database migration or saved format changes are
included.

## Model Used

OpenAI GPT-6 in Codex, with code editing, tool use, and test execution.
The exact runtime model identifier and context-window size are not
exposed in this session.

## Checklist

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

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-05 23:11:28 -06:00
DottaandClaude Opus 5.5 9668e73b4b fix(native): continue a turn automatically when reattachment loses its runner (#19399)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs in managed sandboxes recover through reattachment: after
a server restart, and for a same-run retry when a turn ends without a
semantic result
> - Reattachment fails closed when the original runner or sandbox is
gone (`native_remote_recovery_lease_mismatch`,
`runner_remote_process_identity_unavailable`)
> - Saved-work recovery then saves the files, but the task stays blocked
with "Saved work recovered. Send a message to continue." The recovered
hold offers no Retry
> - A restart, deploy or maintenance window must not need a user message
to continue work
> - This pull request requests the task Retry automatically, once, when
it is safe, and offers Retry on the recovered hold when it is not
> - The benefit is that interrupted work continues after infrastructure
events, and a user who must act gets a Retry that works on first use

## Linked Issues or Issue Description

**What happened?**
A native task in a managed sandbox was interrupted. In one case a server
restart claimed a queued user-comment turn for reattachment. In another
case a turn completed without a semantic result, and the same-run retry
claimed the run for reattachment with no restart. In both cases the
original runner or sandbox lease was gone. The run failed with
`native_remote_recovery_lease_mismatch` or
`runner_remote_process_identity_unavailable`. Recovery saved the work.
The task stayed `blocked` with "Saved work recovered. Send a message to
continue." The execution blocker had `canRetry: false`. A manual `POST
/api/issues/:id/retry` started a run that succeeded.

**Expected behavior**
After an infrastructure interruption, the task continues automatically
when the interrupted turn's outcome is known. Otherwise the task offers
a Retry that works on first use.

**Steps to reproduce**
1. Start a native task in a managed sandbox from a user comment.
2. Lose the sandbox lease or the remote runner process (for example,
restart the server during a maintenance window).
3. Let reattachment fail and saved-work recovery complete.
4. See that the task waits for a new message and offers no Retry on the
execution blocker.

**Root cause**
Reattachment (`reattach_remote_runner`) correctly refuses to replace a
missing runner or allocation. Nothing then continued the user's request.
The automatic continuation rule only allows a new user turn after
reconciliation. `getExecutionBlocker` hid Retry whenever a workspace
restore hold existed, also after the files were verifiably recovered.
The same-run retry path also re-enters the restart-recovery owner, so
its event said "after server restart" with no restart.

## What Changed

- New `server/src/services/heartbeat/restart-interrupted-turn.ts`: after
a reattachment claim executes, if the run failed because its runner or
allocation was lost, request the same task Retry that `POST
/issues/:id/retry` creates. It runs once per run (idempotency key
`restart-interrupted-turn:<runId>`), under the authority of the user who
sent the message, assigned the task, or clicked Retry. It requires that
the run recorded no provider turn or that its last provider turn is
settled. System wakes, in-flight turns, and a Retry that this hook
already requested do not continue automatically.
- `heartbeat/recovery.ts`: call the hook after each reattachment claim.
Label same-run retry reattachment as "Reattaching to the existing remote
runner to resume this run", and add `trigger: "same_run_retry"` to the
event payload.
- `execution-blocker.ts`: a failed run whose saved work is verifiably
recovered now offers Retry (`canRetry: true`) with "Saved work
recovered. Retry or send a message to continue." An intentional Stop
keeps "send a message" only.
- `task-retry.ts`: optional `initiatedBy: "restart_recovery"` records
the activity as actor `system`/`restart-recovery` with
`onBehalfOfUserId`.
- `doc/execution-semantics.md`: document the rule.

## Verification

- New regressions in `server/src/services/workspace-recovery.test.ts`
use the real recovery, Retry and dispatch services on embedded Postgres:
- `continues a user turn once ... after $title and recovery saved the
work` (3 cases: restart lease loss before the turn, same-run retry with
the runner gone after a settled turn, same-run retry with the lease gone
after a settled turn). Each asserts one queued successor bound to the
Retry and the user's authority after recovery, idempotent hook calls,
system activity attribution, and no second automatic attempt for the
automatic Retry's own failure.
- `does not continue automatically when $title, and offers a Retry that
works on first use` (in-flight turn, system wake, unrelated error). Each
asserts no successor, `canRetry: true` on the blocker and the
interruption presentation, and one successor after one Retry click.
- Red before the fix: all 6 tests fail (no successor, blocker `canRetry:
false`). Green after the fix.
- `npx vitest run src/services/workspace-recovery.test.ts
src/services/task-retry.test.ts
src/services/task-interruption-presentation.test.ts
src/services/execution-blocker-read.test.ts
src/services/heartbeat/recovery.test.ts
src/services/explicit-native-continuation.test.ts
src/services/quarantined-task-retry.test.ts
src/__tests__/workspace-restore-repair-route.test.ts
src/services/native-runtime/native-failed-workspace-recovery.test.ts`:
1098 passed.
- Not verified on staging. After the next deploy, check a task
interrupted by a restart: it should continue without a message, or show
Retry.

## Risks

- The automatic Retry uses the original user's authority. Admission
rechecks that user's current permission, stop proof, saved-work
recovery, approvals, pauses, budget and ownership. A refused request
leaves the visible Retry.
- A settled turn that ended without a result is continued with a fresh
Retry turn from saved work. A turn whose outcome is unknown (in flight)
is not.
- The hook runs after the claim's execution returns. If the server stops
between the failure and the request, the task shows Retry instead.
- Not in scope: same-run disposition recovery still cannot start a new
runner in the existing sandbox after the original remote runner exits.
This change continues the task with one fresh Retry turn instead. If
that Retry turn also ends without a result and loses its runner, the
task shows Retry.

## 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

Related: #19355

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 22:29:46 -06:00
DottaandPaperclip da34bcd48d fix(workspaces): resume archived imports from their saved upload (#19396)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Durable workspaces let a replacement web server restore saved task
files.
> - Legacy workspace imports keep a journal and an encrypted upload
receipt across interruption.
> - An archived Git import can rebuild a missing source from its saved
task commit and edits.
> - A fresh clone can also contain newer remote refs. Those refs change
the archive manifest even when the task files are unchanged.
> - This pull request recovers the exact authenticated upload for an
interrupted archived Git import.
> - The original journal, source binding, task binding, and execution
checks still control publication.

## Linked Issues or Issue Description

**What happened?**
An archived Git workspace import stopped after its upload persisted. A
retry rebuilt the source from the remote repository. New remote refs
changed the archive manifest, so the original upload receipt rejected
the retry with `remote_workspace_snapshot_operation_content_changed`.

**Expected behavior**
The retry must recover its original saved upload and complete the same
import. It must keep the saved task commit, staged edits, unstaged
edits, lifecycle state, and task binding. A changed source or task
binding must still block publication.

**Steps to reproduce**
1. Archive a Git task with staged and unstaged edits, save its
restoration recipe, and remove its local repository.
2. Start the workspace import and lose the upload reply after object
persistence, or interrupt its independent restore after the upload is
acknowledged.
3. Advance the remote default branch and add an unrelated branch.
4. Retry the original import. Before this change, both interruption
cases fail with the same content-change error.

**Paperclip version or commit**
Reproduced on master at `d8b0eae68772a3bd2bb4a8163b5ce95764b97cff`.

**Deployment mode**
Cloud workspace migration with external PostgreSQL and durable encrypted
object storage. The regression tests use real disposable PostgreSQL, an
HTTP Git remote, and encrypted filesystem-backed object storage.

Related merged work: #18826 and #19138. No duplicate open fix was found.

## What Changed

- Add scoped recovery of an acknowledged snapshot or a pending upload
whose object persisted. Authenticate the archive before returning it.
Recovery alone does not publish the snapshot.
- Resume archived Git imports from those exact saved bytes. Keep the
original operation and all publication checks.
- Lock and recheck each saved nested source’s company, project, and
repository identity in the final publication transaction.
- Keep mutable local sources on the existing capture and
content-comparison path.
- Add regressions for both interruption points, unrelated remote
changes, source revocation during recovery, and local edits after
interruption.
- Document the retry behavior.

## Verification

- Before the repair: both new archived retry regressions fail with
`remote_workspace_snapshot_operation_content_changed`; the
mutable-source control passes.
- After the repair: all 204 tests in the migration, snapshot storage,
archive, and archive environment suites pass. The suites use real
PostgreSQL and encrypted storage.
- Server package typecheck and full repository typecheck pass on
`bffebbab60edbdf50b42fc37a9e58beb01ceb94d`.
- An added source-revocation test initially expected a later expiry
error. The existing publication fingerprint rejected the change first.
The test now requires that earlier
`remote_workspace_migration_publication_changed` error and still checks
that authority was not published.
- The review finding was reproduced before the safety repair: changing a
saved nested repository URL after upload still allowed publication. The
new regression requires rejection, and unchanged approved nested sources
still restore.
- Full repository typecheck and build pass on the final head. The full
local test command stopped at one Slack manager authorization fixture
with `socket hang up`; the exact case then passed in isolation on the
same head, and all three CI chat shards passed. The original local
failure is retained.
- The first CI server shard run had 4,724 passes and one runtime-process
readiness failure in an unchanged fixture. The exact fixture passed in
isolation. One unchanged rerun of that shard passed. The first failure
remains recorded in the workflow history.
- The earlier candidate’s full local test run was stopped after the
confirmed review finding; that interrupted run is not reported as a
pass.
- All CI gates, including `ci / verify` and `ci / e2e`, pass on
`bffebbab60edbdf50b42fc37a9e58beb01ceb94d`. Greptile is 5/5 on that
head, with no remaining findings or unresolved review threads.

## Risks

Recovery applies to archived Git sources, whose saved recipe and
lifecycle checks still run before publication. A source change during
recovery must keep the snapshot unpublished. Missing or corrupt saved
objects still fail authentication or retain the existing import
obligation. Mutable local sources still reject changed contents. This
change adds no database migration and changes no saved archive format.

## Model Used

OpenAI GPT-6 in Codex, with code editing, tool use, and test execution.
The exact runtime model identifier and context-window size are not
exposed in this session.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-05 22:19:20 -06:00
DottaandClaude Opus 5.5 5c185db77c fix(tree-control): resume work a subtree pause interrupted (#19394)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Task tree control lets the board pause a task tree and then resume
it.
> - A pause stops the runs that are active in the tree. The release must
then continue the work that was pending.
> - A child task whose run was active at pause time did not start again
after release. The child became blocked and asked for a user message.
> - A pause is an operator hold, not a failure. It must not strand work,
and it must not require reconciliation for a run that never started.
> - This pull request re-dispatches interrupted work one time on
release, and it settles a legacy Stop that never entered the adapter.
> - The benefit is that pause and resume keep a task tree moving without
manual recovery, and real safety gates stay in force.

## Linked Issues or Issue Description

**What happened?**

A parent task delegated two children. One child's run was active (in
host workspace preparation, before the adapter started) when the board
paused the parent's tree. The pause cancelled that run. After the board
released the hold, no new run started for that child. The child became
`blocked` with the execution blocker cause
`legacy_execution_requires_reconciliation`. Later the next action became
"Send a message to continue." A sibling child that had no active run at
pause time resumed normally.

**Expected behavior**

A run that a subtree pause cancels is an operator hold. On release, the
pending work starts again one time, automatically. It does not go
through execution reconciliation when the run never entered its adapter.
It does not wait for a user message.

**Steps to reproduce**

1. Create a parent task and a child task that is assigned to an agent.
2. Wake the child. While its run is in host preparation (or queued),
call `POST /api/issues/:parentId/tree-holds` with `{ "mode": "pause" }`.
3. Call `POST /api/issues/:parentId/tree-holds/:holdId/release` without
`wakeAgents`.
4. Before this change, no new run starts, and `getExecutionBlocker`
reports `legacy_execution_requires_reconciliation` for the child.

**Root cause**

- (a) The pause records each interrupted run in the hold member snapshot
(`activeRunId`). Nothing used that record on release.
- (b) Release woke tasks only when `metadata.wakeAgents === true`.
Without it, release relied on stranded-issue recovery. That recovery
stands down when the latest run was cancelled by an operator
(`isOperatorCancelledRun`). So the interrupted child had no owner.
- (c) The pause Stop of a legacy run in preparation is written directly
as `cancelled`. Legacy terminalization classifies a cancelled run with
no bootstrap evidence as an unknown outcome. It records
`legacy_execution_requires_reconciliation`, although the gated dispatch
never entered the adapter.
- Native runs: the verified native Stop does not create the
reconciliation hold, but gap (b) applies to native runs too. The release
change is runtime-independent.

## What Changed

- `issueTreeControlService.findOwedPauseResume` decides if a task is
owed one resume. The task's latest run must be an operator Stop that a
pause hold member snapshot names, recorded while that hold was active.
The task must still be executable by the same agent, and no pause may
still cover it. The obligation belongs to the run, so overlapping pauses
keep it until the last covering release.
- New `services/issue-tree-resume.ts` re-dispatches owed work with wake
reason `issue_tree_resumed` (`holdId` and `interruptedRunId` in the
context). A compare-and-set claim (`treeHoldResume`) on the interrupted
run makes the resume happen at most once. A failed wake returns the
claim.
- Pause release and `mode: "resume"` call it for every member of the
released hold, also without `wakeAgents`. When `wakeAgents` is true, the
existing loop wakes every executable member.
- A task that still has an execution blocker is not replayed. The
release writes `issue.tree_resume_wake_skipped`, the blocker stays
visible, and the obligation stays unclaimed.
- `releaseHold` updates only an `active` hold. A repeated release gets
`409`.
- `settleCancelledLegacyPreparation`: when the executor that owned a
cancelled legacy run proves that dispatch never entered the adapter, it
records bootstrap evidence and resolves that run's
`legacy-execution:<runId>` hold as `false_positive`. It also corrects an
automatic no-replay disposition and restores a `blocked` status only
when that disposition made the change. Then it re-dispatches owed work,
for a release that happened while the Stop was unwinding. It requires a
requested Stop with no existing recovery receipt, and no process,
session, native finalization, unreleased lease, or execution-start
event.
- `settleUnrecoverableExecutions` waits while a legacy executor still
holds its controller lease, and records a `legacyCancellationBlock`
receipt when it blocks a task for a cancelled legacy run.
- Docs: `doc/execution-semantics.md` and `doc/composer-stop.md` describe
the resume rule.

## Verification

- New regression
`server/src/__tests__/issue-tree-pause-resume-run.test.ts` uses embedded
Postgres, the real heartbeat, and the real tree-control routes:
- A run paused during preparation: nothing runs during the hold, there
is no blocker, and release creates exactly one `issue_tree_resumed` run
that succeeds. A second release returns `409` and creates no run.
- Release before the stopped preparation finishes unwinding (with the
periodic disposition run in between): exactly one resumed run after the
executor settles.
- An automatic no-replay disposition that ran first: the blocker clears,
the status returns to `in_progress`, and release resumes once.
- Overlapping child and parent pauses: no resume while the parent pause
remains, then exactly one resume after it is released.
- A queued run cancelled by the pause: release creates exactly one
resumed run.
  - A task reassigned to a user during the hold: no resume.
- A mid-turn legacy Stop with unknown outcome: no replay, the
reconciliation blocker remains, and `issue.tree_resume_wake_skipped` is
logged.
- Red before each fix: the first 4 cases failed on master; the 3 review
cases failed on the first PR commit. All 7 pass now.
- Also passed locally: `issue-tree-control-routes`,
`issue-tree-control-service`, `heartbeat-workspace-busy`,
`heartbeat-undispatched-execution-wait`, `execution-control-work`,
`legacy-workspace-restore-recovery`, `legacy-continuation-authority`,
`task-retry`, `legacy-execution-recovery`, `agent-conversations`,
`heartbeat-retry-scheduling`, `satisfied-connection-intents`.
- Not verified here: a live staging pause/resume. Re-check on staging
after deploy.

## Risks

- Behavior change: release now starts the work that the pause
interrupted, also without `wakeAgents`. Before, that work stayed idle.
Other idle tasks still wake only with `wakeAgents`.
- The settlement trusts the executor's adapter-entry flag. The flag is
set only inside the atomic dispatch gate, which refuses a cancelled run.
The settlement also requires no process, session, lease, or execution
event.
- A native run whose Stop has not finalized at release time is not a
target. Stranded recovery and explicit wake still apply.

## Model Used

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

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 22:08:01 -06:00
DottaandClaude Opus 5.5 958f9aa9f3 feat(issues): limit agent-created task trees to parent and child (#19312)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Agents split work into tasks. A task can have a parent task.
> - Today agents can nest tasks to any depth: parent, child, grandchild,
and deeper.
> - Deep agent-made trees are hard to follow, and an agent that works on
a child task makes grandchildren by default.
> - This pull request limits agent-created trees to two levels: a
top-level task and its children.
> - A request for a grandchild does not fail. The server puts the new
task beside the requester and tells the agent.
> - Board users keep full control. They can still nest to any depth.
Existing deep trees stay valid.
> - The pull request also records which task created each new task, so
the audit trail stays complete when the parent changes.

## Linked Issues or Issue Description

No public issue exists. This is the feature description. Related: #16853
(budgets that include descendant work) also reasons about task trees;
this change does not alter its traversal.

**Subsystem affected**

Issues (tasks): create, child create and move in the issue service and
routes. Agent tools, chat suggestions and creation provenance use these
paths.

**Problem or motivation**

Agents create subtasks as deep as they want. An agent that runs on a
child task gets that child as the default parent, so its follow-ups
become grandchildren. The trees become deep and hard to read. The parent
link is also the only record of which task created a follow-up, so that
record is lost when the parent changes.

**Proposed solution**

Limit agent-created trees to a parent and its children. When an agent
asks for a grandchild, put the new task under the requester's parent and
return `parentAdjusted`. Do not reject the request. Keep board users
unrestricted. Record the originating task in a new server-set
`origin_issue_id` column. Keep `origin_run_id` as the originating run.

**Alternatives considered**

Reject grandchild requests with an error. We did not do this, because
agents then fail mid-task. Add a database constraint. We did not do
this, because existing deep trees must stay valid and nesting can return
later.

## What Changed

- Add `MAX_AGENT_TASK_HIERARCHY_DEPTH` (2) and the
`IssueParentAdjustment` type in `packages/shared`.
- Add `resolveNewTaskParent` and `planIssueParentChange` in
`server/src/services/issues.ts`. `create`, `createChild` and `update`
use the agent limit by default (`parentDepthPolicy: "flatten"`).
- The issue create, `/children` and PATCH routes pass `allow_nested` for
board users. For agents, the routes check authorization against the
parent that the server actually writes.
- The requester keeps its behavior when the server moves a task beside
it: `blockParentUntilDone` blocks the requester, and the new task
inherits the requester's workspace and privacy.
- The server never moves a task above a low-trust boundary root that
contains the requester.
- These subsystems keep any depth, because they find their work by
parent: watchdog reviews, monitor recovery, email-channel placement, and
routines that a person configured.
- An agent move of a task that has children skips only the parent change
and returns `reason: "has_children"`.
- Add `issues.origin_issue_id` (nullable, `on delete set null`, indexed)
in migration `0434`. Only the server sets it. The `createdFrom` detail
field, the `createdFromIssueId` filter and task-creation provenance read
it first.
- Chat prompts now let a chat agent create one task, a task with
children, or separate tasks. The conversation is never a parent.
- The runner `create_task` tool accepts an optional `parentTaskId`. A
chat agent can create a top-level task and then put subtasks under it.
The run must be able to read and change the parent. A conversation is
never a parent. The depth limit still applies. Project tools map the
field to `parentId` through the issue route. The protocol manifest,
semantic contracts and eval fixture are regenerated.
- Watchdog runs that have an active watchdog keep their requested parent
on all create paths, because their access is limited to the watched
subtree.
- Fix a bug that the test drive found: the chat (MCP) `create_task`
ignored the requested `status`, so a `backlog` task became `todo` and
started to run. It now uses the same rule as the native tool.
- Update `skills/paperclip`, `doc/SPEC-implementation.md`, and add the
plan `doc/plans/2026-11-05-two-level-task-hierarchy.md`.

## Verification

- `cd server && npx vitest run
src/__tests__/agent-task-hierarchy-depth.test.ts
src/__tests__/agent-task-hierarchy-depth-routes.test.ts`: 19 new tests
for service and route behavior, all pass.
- Related suites (64 files: issues service, thread interactions,
privacy, watchdogs, provenance, runner tool authority, decisions,
routines, project tools): 1208 tests pass. Two tests changed because of
the intended behavior and pass now. Two `email-channels.integration`
tests fail locally because the test cannot find a bundled skill file
from this worktree path. This change does not touch that code.
- `packages/db`: migration safety, snapshot drift and journal tests
pass.
- Typecheck passes for `server`, `packages/shared`, `packages/db`, `cli`
and `ui` (local `ui` errors come only from generated Storybook catalog
files that are absent).
- Local test drive on an isolated instance with real Claude agents: 29
of 29 use cases pass, including an agent on a child task (follow-ups
placed beside it, with the notice) and a chat that creates a top-level
task with three subtasks.
- I did not run the full `pnpm test:run` or `pnpm build` locally. CI
runs them.

## Risks

- Behavior change for agents: some follow-ups that were grandchildren
are now siblings. The parent of the requester is woken when all its
children finish, which now includes these tasks.
- The runner `create_task` input contract adds one optional field, so
the protocol manifest and the pack contract change. The tool output does
not change: it shows the actual `parentId`, and the activity log records
`parentAdjusted`.
- Migration `0434` adds a nullable column, a foreign key and an index.
It does not backfill rows.
- `IssueCreationSource.run` is now nullable.

## Model Used

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

## Checklist

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

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The Tasks page helps people inspect work across their company.
> - A new viewer currently starts in Mine and can miss other company
tasks.
> - The task table also shows an active sort header before the viewer
selects a sort.
> - This pull request starts new viewers in All tasks and keeps the
default sort header neutral.
> - People see company work in newest-update order and keep their saved
view choices.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

The initial task view and the task table's default sort state.

**Subsystem affected**

ui/ — React board UI.

**Current behavior**

Tasks opens Mine when the viewer has no saved view. The table highlights
Last updated even when the viewer has not selected a column sort.

**Proposed behavior**

Tasks opens All tasks when no saved view exists. The table keeps
newest-updated tasks first without an active header or arrow. A column
selection or an explicit sort URL activates the selected header.

**Reason and benefit**

New viewers can inspect company work immediately. The header
distinguishes default ordering from an explicit sort choice.

**Breaking changes**

The first-visit view changes from Mine to All tasks. Saved views and
explicit view URLs keep priority. Existing Inbox and Needs attention
preferences retain their meaning. The change does not alter task access
or database contracts.

**Additional context**

Refs #19363. This change builds on the grouped task menu from that
merged pull request. It uses the existing menu and design tokens.

## What Changed

- Default the Coordinator and combined task surfaces to All tasks.
- Preserve saved personal views and migrate previous picker choices.
- Keep implicit Last updated descending order separate from active
header state.
- Test default ordering, explicit sorting, reset behavior, view
selection, and preference migration.
- Update the product and implementation docs.

## Verification

- Passed 118 focused tests across task routing, table sorting, views,
and collection preferences.
- Passed `pnpm check:token-gates`, `pnpm -r typecheck`, and `pnpm
build`.
- Full `pnpm test:run` is running. Its result will be updated here.
- Greptile passed the current head with 5/5 and no findings or open
review threads.
- Browser check: All tasks uses the grouped My work and Organization
menu. Last updated has no active highlight by default. Selecting the
header adds the sort URL and active direction. Removing the explicit
sort restores the neutral header.
- Manual check: Open Tasks with no saved task view. Confirm All tasks
opens. Select Mine, leave Tasks, and return. Confirm Mine remains
selected.

## Risks

- The new default shows more accessible company tasks on the first
visit. Saved view choices remain intact.
- Task tables share the sort-state change. Explicit sort links and
column clicks retain active styling. Default row order stays
newest-updated first.
- No schema, API, permission, or execution changes.

## Model Used

OpenAI Codex, GPT-6 family. This session does not expose the exact model
ID or context-window size. Used reasoning, code editing, terminal
commands, and browser verification.

## Checklist

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

---------

Co-authored-by: Scott Tong <scott@scottsmbpm5max.lan>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-05 18:44:59 -08:00
Devin FoleyandPaperclip 8f79a32898 fix(connections): keep agent access consistent through setup and permission changes (#19352)
Task-hosted tool setup kept requester-only defaults, and audience edits could leave runtime installations restricted after permission bindings were widened. Large provider catalogs could also fail before creating an access card.

Use the normal agent audience defaults for new tool setup, preserve explicit selections through verified OAuth and draft resumption, and synchronize permission bindings and runtime installations atomically. Enforce agent-edit authority and preserve existing access during additive account reuse.

Validation: 915 focused regression tests, typechecks, and builds passed. All 54 CI checks passed and Greptile Apex scored the reviewed head 5/5 with no unresolved threads. Broad local test failures, recoveries, and incomplete heartbeat coverage are documented in the PR.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-11-05 17:38:32 -08:00
4550253dc9 feat(ui): rename Needs you to Inbox and move Mine into Tasks (#19363)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Coordinator gives people navigation for conversations, tasks, and
decisions.
> - The decision entry is called Needs you, while a separate Inbox
remains under More.
> - Coordinator's personal task filter does not retain the full behavior
of the original Mine view.
> - This pull request names the decision entry Inbox and hosts the
original personal work views inside Tasks.
> - People keep their triage tools and saved choices without moving
between two task destinations.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

Coordinator navigation and the personal Mine view in Tasks.

**Subsystem affected**

UI navigation, task view routing, task collection preferences, and
browser coverage.

**Current behavior**

Needs you opens the decision feed. More contains another Inbox
destination. The default Coordinator task filter matches task creators
and assignees. It does not use the full personal participation rules,
archive behavior, shortcuts, grouping, or saved columns of the original
Mine view. The organization task table also resets its column choices on
reload.

**Proposed behavior**

Inbox opens the same decision feed with the inbox icon. Tasks defaults
to Mine and remembers a person's selected view per company. Mine uses
the existing personal Inbox implementation and preferences. It retains
read/unread title weights, keyboard navigation, read/unread shortcuts,
archive and undo, mobile swipe-to-archive, grouping, and saved columns.
Old Inbox links open the matching task view. The organization task table
saves its columns and order per person and company.

**Reason and benefit**

People get Chat, Tasks, and Inbox as distinct navigation entries.
Personal work remains a view within Tasks without losing the existing
triage behavior.

**Breaking changes**

Coordinator no longer offers the previous Inbox destination under More.
Old Inbox URLs redirect into Tasks and retain query parameters and
fragments. The Needs you label becomes Inbox; its decision feed, count,
history, and resolution paths stay in place. Classic retains its
separate optional combined-list setting.

**Additional context**

Refs: #19360, the workspace-defaults PR now integrated from master.
Refs: #10620, related Inbox attention ordering. Duplicate searches found
no open PR for this navigation and Mine integration. Roadmap review
found no separate new execution feature in this scope.

## What Changed

- Rename Coordinator's desktop and mobile decision entry and page to
Inbox. Use the inbox icon. Remove the old Inbox item under More. Clarify
that the combined-list experiment applies only to Classic.
- Add one Coordinator task-view router. Host Mine, Unread, Recent,
Blocked, and Everything through the existing Inbox owner. Keep
organization views in the task table and board. Normalize legacy
filtered URLs before mounting a collection so clearing the last filter
preserves the current layout and local filters.
- Reuse the existing Mine API, participation, read state, archive cache,
undo, keyboard, mobile gesture, grouping, and preference paths. Existing
personal column choices carry over.
- Save organization task-table columns and order per person and company.
Keep controls usable if browser storage fails.
- Update navigation tests, routing and persistence regressions,
exact-run retry browser coverage for each workspace variant, product
documentation, and the affected Storybook notes. Keep existing story
IDs.

## Verification

- Preference, routing, navigation, task table, and inherited Inbox
regression tests pass in focused runs. After integrating master and
addressing the review finding, all 187 focused tests, UI typecheck, and
token gates pass.
- Full build and repository typecheck pass after integrating master. The
full UI suite previously passed: 894 files, 11,169 tests passed, and one
skipped. All 14 desktop/mobile triage and exact-run retry browser cases,
including a fresh production UI build, pass after integrating master.
- The full repository test suite was not run locally for this UI-only
change; full PR CI is running.
- Browser checks use a fresh isolated local instance and a deterministic
process agent for real run-attributed unread activity. They exercise
legacy URLs, title weights, saved grouping and columns, read/unread
shortcuts, archive/undo, and a real mobile touch gesture.
- Earlier browser fixtures were corrected: own activity is read, agent
comments need valid run attribution, and undo uses `u` after the archive
refresh completes. The Inbox content heading is scoped separately from
the layout heading.
- The final settings wording correction passes all 63 settings tests.
- The [next CI browser
run](https://github.com/paperclipai/paperclip/actions/runs/42137377960/job/127629100535)
caught an old URL assertion for denied run retries. The test now pins
each workspace variant and covers Coordinator Tasks explicitly. The old
expectation fails deterministically for Coordinator; all 12 retry cases
pass with the corrected destination, preserving exact run identity and
truthful queued, deferred, and denied feedback.
- Token gates, map checks, and Storybook catalog checks pass. All 30
map/catalog script tests pass. Storybook play tests were not run.
- Greptile reviewed [the final
commit](https://github.com/paperclipai/paperclip/commit/5f2f3789b83af259f694f379afb769465688fb72)
at **5/5** with no new actionable findings. Its sort-only link finding
is fixed and resolved: three cases fail before the fix and pass after
it. `sortField` and `sortDir` keep organization links on the
organization table. [Current-head
CI](https://github.com/paperclipai/paperclip/actions/runs/42139668774)
is running.
- [CI exposed a routing
race](https://github.com/paperclipai/paperclip/actions/runs/42135293421/job/127621981587)
when removing the last label from a legacy Everything link. Four
deterministic regressions failed before the fix and now pass:
collections only mount after URL normalization. The existing end-to-end
component cases confirm local filters, board layout, fragments, and
history survive the change.
- The [initial server shard 4
run](https://github.com/paperclipai/paperclip/actions/runs/42131163087/job/127608751620)
failed an unchanged monitor-drain idleness assertion: two active
operations remained instead of zero. The complete scheduling suite
passes locally (54 tests). One isolated CI retry passed, including its
dependent verification gate. The original timing failure's root cause is
not established.

## Risks

- Mine includes the existing tasks, approvals, failed runs, and join
requests, preserving the original personal participation and attention
rules.
- Shared URLs with organization filters or table sort parameters still
open the organization task collection. Task back-links retain their
original fragments.
- Saved preferences are browser-local. Mine keeps its existing storage
keys; the organization table and selected Coordinator view use
person/company scopes.
- No database or server API contract changes are included. Existing
company access, personal participation, and decision permissions retain
their owners.

## Model Used

OpenAI Codex, based on GPT-6, with reasoning, repository tools, code
execution, and browser automation. The exact serving revision and
context-window size are not exposed in this session.

## Checklist

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

---------

Co-authored-by: Scott Tong <scott@scottsmbpm5max.lan>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-05 17:07:24 -08:00
Devin FoleyandClaude Opus 5.5 33269e1fec fix(observability): report directory-merge lock holder diagnostics (#17755)
A directory-merge lock timeout (ERR_WORKSPACE_RESTORE_LOCK_TIMEOUT) already
carries allowlisted holder evidence, but server Sentry capture dropped it.

- Add directoryMergeLockTimeoutDiagnosticsFor in adapter-utils: closed
  labels, booleans and bounded durations; no owner PID or paths.
- server captureException: lock timeouts get an event-local
  directory_merge_lock context and error_code=workspace_restore_lock_timeout
  tag; default grouping and message unchanged.
- run-failure-diagnostics restoreLock* fields use the shared reader
  (same allowlist and order).
- Document the record in doc/workspace-restore-locks.md.

Diagnostics only; does not change lock acquisition, timeouts or retries.

Refs PAPERCLIP-BACKEND-4Q

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 16:48:46 -08:00
Nicky LeachandClaude Opus 5.5 3a8f95d928 fix(ci): let verified Dependabot lockfiles merge through the reporting-lock gate (#19356)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Only trusted environments may change `pnpm-lock.yaml`: the lockfile
refresh workflow and Dependabot. Contributors never commit it.
> - Paid evaluation jobs also install a lock only when the
reporting-lock proof (`tests/runner-e2e/report-lock.json` and `.patch`)
admits it.
> - #19283 added a CI test that checks the committed lock against that
proof. Only the refresh workflow writes the proof, so every Dependabot
lockfile pull request now fails CI. Examples are #17364, #17172, and
#17911.
> - A merged Dependabot lockfile also breaks the refresh workflow.
`prepare-report-lock.mjs --refresh` refuses to renew the proof for a
lock that the proof does not already admit.
> - This pull request lets a verified Dependabot lockfile pass, and it
keeps both lockfile rules strict. It also closes a gap in the
contributor rule.
> - The benefit is that Dependabot security updates can merge again, and
only trusted environments write lock content.

## Linked Issues or Issue Description

Refs #19283, #17364, #17172, #17911, #19303.

**What happened?**
`server/scripts/reporting-lock-policy.test.mjs` fails on each Dependabot
pull request that changes `pnpm-lock.yaml`. The error is "Reporting lock
changed; review the dependency correction before install". A Dependabot
pull request cannot change the proof files, so it can never pass. After
a merge, the lockfile refresh also stops at "Committed report lock is
not reviewed; refusing to refresh its proof".

**Expected behavior**
- Contributors cannot change `pnpm-lock.yaml` or the reporting-lock
proof.
- Dependabot can change `pnpm-lock.yaml`.
- The lockfile refresh workflow can change `pnpm-lock.yaml` and the
proof.
- After a Dependabot lockfile merges, the refresh workflow admits it and
renews the proof.

**Steps to reproduce**
1. Open a Dependabot pull request that changes `pnpm-lock.yaml`, such as
#17364.
2. Let CI run. The `server (9/12)` shard fails on the reporting-lock
policy test.

**Security gap found during this work**
The old rule skipped the lockfile check when `github.head_ref` was
`chore/refresh-lockfile`. A contributor could open a pull request from a
fork branch with that name and commit a lockfile change. The old rule
also exempted any pull request that Dependabot opened, including commits
that other people pushed to its branch.

## What Changed

- Add `.github/scripts/lockfile-trust.mjs`. It decides which writers may
change lock content:
- The refresh workflow: the head is `chore/refresh-lockfile` in this
repository, and `github-actions[bot]` opened the pull request.
- Dependabot: Dependabot opened the pull request from this repository,
and the head `pnpm-lock.yaml` matches a Dependabot commit. That commit
must have `dependabot[bot]` as its author, GitHub (`web-flow`) as its
committer, and a verified signature. The script reads these fields from
the GitHub API.
- The reporting-lock proof files count as lock content. Only the refresh
workflow may change them.
- `pr-trusted.yml` policy job: replace the inline `head_ref` and author
exemption with `lockfile-trust.mjs pr`. The job runs the script copy
from the base commit, so a pull request cannot change the check that
judges it. If the base predates the script, the job uses the earlier
rule.
- `refresh-lockfile.yml`: add `lockfile-trust.mjs merged`. It checks
that the last lockfile change on `master` is a GitHub-signed merge of a
Dependabot pull request, and that the lockfile at the pull request head
matches a verified Dependabot commit. If both checks pass, it gives the
lockfile SHA-256 to `prepare-report-lock.mjs --refresh
--trusted-lock-sha256`.
- `refresh-lockfile.yml` PR step: check `pnpm-lock.yaml` and both proof
files before it exits as unchanged. A refresh that renews only the proof
now opens a pull request.
- `prepare-report-lock.mjs` and its `.d.mts` declaration:
`refreshReportLock` accepts a trusted lock hash. If the trusted lock
already resolves, the proof admits it exactly with an empty patch. If
not, the proof records the resolution as the reviewed correction, as
before.
- `reporting-lock-policy.test.mjs`: skip on Dependabot pull requests.
The policy job verifies those lockfiles. The general test job gets
`PAPERCLIP_CI_PR_AUTHOR` for this purpose. This keeps the test, so it
still merges cleanly with #19303.
- `check-pr-lockfile.mjs` (advisory gate): allow Dependabot lockfile
changes, and flag changes to the proof files.
- `CODEOWNERS`: add the two proof files.
- `doc/RELEASE-AUTOMATION-SETUP.md`: describe the writers, the
`@dependabot recreate` path, and the proof renewal after merge.

## Verification

- `node --test '.github/scripts/tests/*.test.mjs'`: 707 passed. This
includes 17 new tests in `lockfile-trust.test.mjs` and 3 new tests in
`check-pr-lockfile.test.mjs`. The new tests run the `pr` command against
real git histories with a stub `gh`.
- `tests/runner-e2e/prepare-report-lock.test.ts`: 10 passed, including 3
new trusted-lock refresh tests. I ran it with a minimal Vitest config,
because the runner-e2e config builds a provider bundle first.
- `server/scripts/reporting-lock-policy.test.mjs`: passes normally, and
skips with `PAPERCLIP_CI_PR_AUTHOR=dependabot[bot]`.
- Live check of the `pr` mode against #17364 with the real GitHub API:
- With the real author, it reports "pnpm-lock.yaml matches verified
Dependabot commit 83c328c15c…" and `writer=dependabot`.
- With a contributor as the author and `chore/refresh-lockfile` as the
head ref, it fails.
- Live check of the `merged` mode at the real merge commits of #16018,
#16020, and #15361. All three report a trusted lock hash. For #16018 and
#15361, `master` changed after Dependabot's commit, so GitHub's merge
produced different lockfile bytes. An exact-bytes rule would refuse
them. That is why the rule trusts GitHub-signed merges of a verified
Dependabot head.
- Greptile reviewed `513ca2def8` at 4/5 with one P1: a proof-only
refresh was never committed, because the PR step checked only
`pnpm-lock.yaml`. `edbeb1ea0d` fixes it. A new test in
`lockfile-refresh-merge.test.mjs` runs the step's change check on a
proof-only change. The test fails without the fix.
- CI `Typecheck + Release Registry` failed at `edbeb1ea0d`:
`prepare-report-lock.d.mts` did not declare the new options argument
(TS2554 twice in `prepare-report-lock.test.ts`). `9139aaad85` updates
the declaration. A local `tsc -p tests/runner-e2e/tsconfig.json` shows
the 2 errors with the old declaration and none with the new one.
- CI at `9139aaad85` failed in six general test shards. None of them run
code that this pull request changes, and PR CI loads `pr-trusted.yml`
from `master`. The failures:
- `heartbeat-plugin-environment.test.ts`: "expected 'running' to be
'succeeded'"
  - `status-card-evaluation-work.test.ts`: a deadline comparison
  - `isolated-repository-rehome.test.ts`: timed out after 15 seconds
- `chat-channels.integration.test.ts`: GitHub webhook recovery
scheduling
- `company-import-export-e2e.test.ts`: child server start timeout, which
also fails on unrelated branches
- `workspaces-b`: every test passed, but an `EPIPE` error came after
`acpx-engine/execute.test.ts`
  I started a rerun of the failed jobs.
- Superagent found a P1 at `9139aaad85`: a verified signature proves who
committed, not who authored. A collaborator could sign a commit with
their own key and set Dependabot as the author. `4fe180d6c1` also
requires GitHub (`web-flow`) as the committer. New tests cover the
spoofed-author case. #17364, #16018, #16020 and #15361 still verify
against the GitHub API.
- `git merge-file` merges this test change with #19303 without
conflicts.
- Not run: the full repository test suite and typecheck. These changes
touch only CI scripts, workflows, one helper, and tests. GitHub CI
covers full regression.

## Risks

- `pr-trusted.yml` loads from `master`, so the new policy takes effect
only after merge. This pull request's own CI uses the earlier rule.
- A Dependabot pull request that is behind `master` now fails if someone
merges `master` into it and the lockfile changes. Use `@dependabot
recreate`. The policy job error says this.
- Paid evaluation jobs refuse a merged Dependabot lockfile until the
refresh pull request merges. The refresh merges without review when it
adds no new package versions. If the release-age gate holds it, the wait
lasts until a maintainer merges it or the daily refresh runs. During
that time, the reporting-lock policy test also fails on other pull
requests, which is the current behavior for any unrenewed lock.
- The rule trusts GitHub as the signer of Dependabot commits and of
merge commits (`web-flow`). Superagent uses the same trust root.
- People with write access can still push to `chore/refresh-lockfile`.
This was already true. The bot merges only its own head commit
(`--match-head-commit`), and code-owner review covers anything else.
- A future manual proof renewal, such as #19283, now needs the refresh
workflow or an admin merge. A contributor pull request that changes the
proof files fails.

## Model Used

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

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass (focused tests; see
Verification)
- [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 16:42:26 -08:00
Nicky LeachandClaude Opus 5.5 e6c74982fe fix(workspaces): report why a workspace provider operation failed (#19362)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A project can store its task workspaces with a workspace storage
provider plugin. Core calls the plugin's `workspaceOperation` method to
prepare, inspect, finalize and remove a workspace.
> - `callWorkspaceProvider` catches every rejection of that call and
throws the same conflict: "Workspace storage operation is incomplete;
retry the same workspace".
> - A provider error, an RPC timeout, a crashed worker and a worker that
never got the call all look the same in the run, the log and the API.
The message tells the user to retry, also when the failure is a fixed
configuration problem that a retry cannot fix.
> - In one diagnosis, three different configuration failures in a row
showed this same message. Finding each cause took trace span counts and
database reads.
> - This pull request lets a provider send a reason from a closed set,
and makes Core tell transport failures apart from provider failures.
> - The benefit is that a failed run says what failed and whether a
retry can help. Provider text and values still never cross into Core.

## Linked Issues or Issue Description

**What happened?**
A workspace provider's `workspaceOperation` failed. The run failed with
"Workspace storage operation is incomplete; retry the same workspace",
`errorCode: setup_failed`, and no reason in the run record or the
`heartbeat execution setup failed` log. Retrying did not help, because
the cause was a provider configuration problem.

**Expected behavior**
Core tells these cases apart: the worker did not receive the call, the
call timed out, the worker stopped during the call, or the provider
reported a failure. For a provider failure, Core shows a bounded reason.
Core tells the user to retry only when a retry can help.

**Steps to reproduce**
1. Install a workspace storage provider plugin whose
`onWorkspaceOperation` throws during `prepare` (for example, because of
missing provider settings).
2. Run an agent task in a project that uses that provider.
3. See the failed run: the error is the generic retry message, and the
run record and logs do not show the cause.

**Paperclip version / commit**
`master` at 5ac05871b5.

**Deployment mode**
Any deployment with a workspace storage provider plugin.

## What Changed

- **SDK reason contract.** New
`packages/plugins/sdk/src/workspace-operation-error.ts`:
`WORKSPACE_OPERATION_FAILURE_REASONS` (11 reasons),
`PluginWorkspaceOperationError`, `workspaceOperationErrorData` and
`readWorkspaceOperationFailureReason`. It uses the same envelope design
as `environment-task-error.ts`. The worker host attaches the envelope
only to `workspaceOperation` errors. It copies only the reason, never
the message.
- **Duck-typed reason.** The worker host also forwards an own
`workspaceFailureReason` property from any error, when the value is in
the closed set. A provider that builds against an older SDK can set the
reason before it can import the class. An inherited or unknown value is
ignored. The value can only select one of Core's fixed messages.
- **Core classification.** New
`server/src/services/workspace-provider-failure.ts`.
`callWorkspaceProvider` maps each rejection to a
`WorkspaceProviderFailureError` (409) with `kind`, `reason` and
`retryable`:
- `worker_unavailable`: no request was sent. The
`PluginWorkerUnavailableError` stays as the cause, so run setup retries
it as `plugin_worker_unavailable` (the existing bounded bootstrap
retry).
- `timeout` and `worker_exited`: the outcome is unknown, so the message
still says to retry the same workspace.
  - `transport`: any other rejection. The message is the old message.
- `provider` with a reason: a fixed message per reason. Only transient
reasons say to retry.
- `provider` without a known reason: "Workspace storage provider
reported a failure; retry the same workspace".
- **Run settlement.** `failRunSetup` records
`resultJson.workspaceProviderFailure` (`kind`, `reason`, `retryable`).
The `heartbeat execution setup failed` log carries the same fields and
the run's `errorCode`. The run keeps `errorCode: setup_failed` (except
for the worker-not-running case above), so the existing setup-failure
retry, recovery and chat paths do not change.
- **Span allowlist.** The host accepts a `plugin.workspace.failure` span
and a `plugin.failure_reason` attribute. The value must be a reason from
the closed set or `unknown`.
- **Docs.** `doc/plugins/PLUGIN_RUNNER_EXECUTION.md` describes the
failure kinds and the reasons. `doc/observability.md` lists the new span
and attribute.

## Verification

- `cd packages/plugins/sdk && npx vitest run`: 21 files, 261 tests pass.
The new `tests/workspace-operation-error.test.ts` sends errors through
the real worker RPC host.
- `cd server && npx vitest run
src/__tests__/workspace-provider-failure.test.ts
src/__tests__/plugin-host-services-span.test.ts
src/services/heartbeat/run-completion.test.ts`: 3 files, 118 tests pass.
These tests cover:
- every transport kind, and that a worker refusal is still found as the
cause;
  - a real worker RPC rejection with a reason, and without a reason;
  - a fixed message and retry decision for every reason;
  - span clamping of `plugin.failure_reason`;
- run setup for a provider reason (no retry, reason in `resultJson` and
in the log) and for a stopped worker (bounded retry).
- `cd server && npx vitest run` for the 4 plugin workspace integration
suites (`plugin-workspaces`, `plugin-fresh-workspaces`,
`project-workspace-provider`, `workspace-delivery-cleanup`): 91 tests
pass. Their lost-reply fixtures still get the old "incomplete" message.
- `npx tsc --noEmit` in `packages/plugins/sdk`: no errors. `npx tsc
--noEmit -p server/tsconfig.json` shows no errors in the changed files.
The remaining errors come from workspace packages that I did not build
locally.
- Not run locally: the full `pnpm -r typecheck`, `pnpm test:run` and
`pnpm build`. CI runs them.

## Risks

- **Behavior change for a stopped worker.** A `workspaceOperation` call
that the worker did not receive now gets `errorCode:
plugin_worker_unavailable` and the bounded bootstrap retry, as other
plugin calls already do. Before, it was a `setup_failed` run with no
retry. No request reached the provider, so the retry is safe.
- **Message changes.** Provider-reported failures and timeouts get new
fixed messages. Code that matches the exact old message only sees it for
`transport` and `remote_incomplete` failures. I did not find a match on
this message outside tests.
- **No new error codes.** Provider failures keep `setup_failed`. Retry,
recovery and chat-retry routing does not change.
- **Duck typing.** Any error with an own `workspaceFailureReason` from
the closed set is forwarded. A provider can only select one of Core's
fixed messages. It cannot send text or values.
- **Observability.** The new span attribute is a closed set and stays
inside the host allowlist. No Telemetry change.

## Model Used

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

## Checklist

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

Related:
[paperclipai/paperclip#19326](https://github.com/paperclipai/paperclip/pull/19326)
added the same envelope design for Runner revision mismatches. I found
no duplicate PR for this change.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 16:28:49 -08:00
f2593b7905 feat: make Coordinator and Agent Chat standard and speed up navigation (#19360)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - People enter that work through the workspace, agent conversations,
and tasks.
> - Coordinator and Agent Chat still require experimental enrollment.
> - Task link prefetches do not warm the compact queries used by the
default task view. Chat also waits for a request when its navigation
list already holds the conversation.
> - This pull request makes Coordinator and Agent Chat standard and
reuses the reads needed for navigation.
> - People can choose Classic workspace in Experimental settings. Cached
work can appear while fresh data loads.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

Workspace selection, Agent Chat availability, and navigation between
Chat, Tasks, and task details.

**Subsystem affected**

Cross-cutting: UI, workspace preferences, instance settings, and the
database migration.

**Current behavior**

People must enable Coordinator and Agent Chat in Experimental settings.
Opening a task can repeat requests because prefetch warms the full
detail and comment queries, while the default view reads compact task
data and a recent comment window. Opening Chat waits for a detail
request even when the current person's conversation list contains it.

**Proposed behavior**

Coordinator is the standard workspace for everyone. Experimental
settings offer Classic workspace, off by default. Old Coordinator
experiment values do not select Classic. Agent Chat is available to
everyone, including instances with an old saved or managed off value.
Task link intent warms the queries used by the mounted view. Chat can
use the current person's company-scoped conversation list during
refresh.

**Reason and benefit**

People can use the standard workspace without setup. Repeat navigation
can show cached content while the server refreshes it.

**Breaking changes**

Existing Coordinator experiment and Agent Chat off values no longer
disable these surfaces. Classic requires an explicit new opt-in. The
workspace API keeps its existing response shape and audit checks. The
migration retains legacy values and adds a separate Classic preference.
Execution permissions, company access, and chat privacy checks still
apply.

**Additional context**

Roadmap check: this graduates existing surfaces and improves their
navigation. It does not add a new agent execution capability. Duplicate
searches found no open Coordinator workspace PR. Related server cache
work, Refs: #17828, addresses compact issue-list invalidation; this PR
addresses client navigation queries.

## What Changed

- Add a default-off Classic workspace preference. Derive the existing
workspace API response from that choice. Keep personal persistence,
account isolation, and audited writes in the workspace preference
service.
- Replace the Coordinator experiment control with Classic workspace. Use
Coordinator while preferences load. Update the existing settings
examples and navigation recipe.
- Retire the Agent Chat experiment control. Ignore old stored and
managed off values. Retain the compatibility key and the existing
execution and access checks.
- Reuse account- and company-scoped chat list data with its original
freshness timestamp.
- Share compact task and recent-comment query options between prefetch
and the task view. Keep full-history prefetch for Classic tasks and
expanded history.
- Add upgrade, replay, cache, account isolation, and browser switching
regressions. Update product, database, development, and roadmap
documentation.

## Verification

- Targeted preference, settings, migration, cache, and UI regression
tests passed during implementation. The cache tests hold refresh
requests pending and check that content still renders. They also check
account isolation and stale refresh behavior.
- A real browser test verified fresh Coordinator defaults, Classic
opt-in persistence, and return to Coordinator. It used an isolated
source checkout with fresh data on macOS.
- Token, migration numbering and safety, map, and Storybook catalog
checks pass after the rebase.
- Repository typecheck and build pass. Typecheck and token gates also
pass after review repairs. The broad local run passed all six chat
integration shards (1,931 tests). It then found the host-CLI-dependent
recovery fixture described below. Its remaining duplicate work was
stopped after the full current-head CI matrix passed; this is not a
claim that the broad local command completed green.
- Review repairs passed 419 UI/migration fixture tests, 19 Coordinator
navigation tests, and all 214 tests in the five affected server suites.
The 24 Chat/workspace browser tests pass, including standard Coordinator
switching and explicit Classic persistence.
- The broader browser repair run passed 85 tests. It retained two failed
assertions and skipped five later serial cases. The affected files then
passed all 11 tests after the fixes: reopen the desktop panel after
reload and open More for Audit and Connectors. This includes the five
serial cases that had not run.
- The broad local suite exposed a recovery fixture that executed the
host Codex CLI during a simulated remote probe. On a host with Codex
0.154.0, model fallback broke its fixed-model assertion. The fake
sandbox now reports Codex 0.160.0. All 20 selected recovery/provider
compatibility cases pass after the repair.
- A CLI company import/export suite timed out during server startup in
the second CI run. The port fallback is intentional in that fixture. No
root cause was established. That suite passes on the final commit in CI.
This records recovery, not a claimed product fix for the earlier
timeout.
- The first CI run exposed remaining assumptions about the retired flags
and Classic UI. The repair commit updates those fixtures and navigation
steps. It also restores the missing enabled Cases destination in
Coordinator. Earlier local recovery attempts used the wrong fixture
notification and advanced fake time during live execution; the corrected
tests use the subscribed recovery queue and wall time during execution.
- Greptile reviewed commit `98972b030d4505d838427a2fca6b196b8cad6d47` at
**5/5** with no new comments. The original finding is resolved.
- Current-head CI is green: **56 successful checks**, with three checks
skipped by workflow filters. This includes typecheck, build, the
complete Vitest matrix, all eight browser shards, Runner verification,
release checks, and security checks. [Final CI
run](https://github.com/paperclipai/paperclip/actions/runs/42132651414).
- Typecheck caught missing `pid` and `startedAt` fields in the simulated
command result. The fixture now returns the complete process contract,
and the server test-layer typecheck passes.
- Storybook catalog alignment was checked. Storybook play tests were not
run. Hosted latency and cold bundle loading were not measured.

## Risks

- The new default changes the workspace for people who previously left
the Coordinator experiment off. They can opt into Classic in
Experimental settings.
- The additive migration must run before the new server code. It is safe
to replay and preserves explicit Classic choices.
- Cached content can be stale during background refresh. Existing query
invalidation and source timestamps still control freshness.
- These changes reduce avoidable loading waits. They do not establish a
measured improvement in hosted request latency or initial bundle
download time.

## Model Used

OpenAI Codex, based on GPT-6, with reasoning, repository tools, code
execution, and browser automation. The exact serving revision and
context-window size are not exposed in this session.

## Checklist

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

---------

Co-authored-by: Scott Tong <scott@scottsmbpm5max.lan>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-05 16:28:25 -08:00
DottaandPaperclip 7c0922a7b6 fix(native): preserve provider backups during slow workspace saves (#19359)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Remote native sessions save provider state so another web server can
continue the same conversation.
> - A provider can suspend while its accepted result is still saving a
large workspace.
> - The run has released its live lease at that point. A new
backup-owner lookup rejects this uncommitted state.
> - This pull request captures the real owner during backend creation
and retains it with the current session authority.
> - The existing database checks still reject a changed boot,
generation, result, or successor before inspection and during
publication.
> - The benefit is that a slow workspace save no longer prevents the
provider backup.

## Linked Issues or Issue Description

**What happened?**

A remote session can fail to save its provider backup with
`native_harness_storage_owner_missing`. The provider has suspended, but
its accepted result is still in `workspace_finalizing`. The close
callback tries to acquire a new owner after the run released its lease.
A later allocation cannot use a backup that was never published.

**Expected behavior**

Save the suspended provider with the authority acquired during
execution. Recheck that authority before provider inspection and before
and after upload. Keep result finalization and provider-work permissions
unchanged.

**Steps to reproduce**

1. Start a remote native session with durable provider storage.
2. Accept its result and release the live lease while its workspace save
remains pending.
3. Close the provider transport. The old owner lookup rejects the save
before provider inspection.

**Paperclip version or commit**

Reproduced on master `bf10bc695abaef6b20e85b287f456b5ce2edb266`.

**Deployment mode**

A web server with object storage and remote managed Runner sessions.

Related: #19225 retains failed-turn ownership through close. #18436
restores an external controller checkpoint before a changed warm process
is replaced. This change fixes late backup-owner acquisition while an
accepted result saves its workspace.

## What Changed

- Capture the provider backup owner when the remote backend still owns
its execution lease.
- Bind that owner to the current session authority. An older close
callback selects the current warm attachment's owner. Revalidate the
captured fence before inspecting or skipping provider state.
- Add a regression for the close callback during workspace finalization.
- Restore encrypted provider files with a captured owner while workspace
finalization remains pending. Reject boot, generation, result, and
successor changes during upload.
- Document the ownership contract.

## Verification

- Observed the callback regression fail on the old code with
`native_harness_storage_owner_missing`. The same test passes with this
fix.
- `cd server && node ../node_modules/vitest/vitest.mjs run --config
vitest.config.ts
src/services/native-runtime/native-harness-backup-storage.integration.test.ts`:
59 passed, using real PostgreSQL and encrypted object write/readback.
- `node scripts/check-module-boundaries.mjs`: passed.
- Full workspace typecheck: passed with the complete production fix.
- Native execution: 1,151 passed. Managed warm lifecycle: 9 passed.
These tests require a lost-owner error during shutdown, including before
an incomplete provider checkpoint is skipped. The final change to the
lifecycle test also requires rejection after lease renewal is lost.
- Full local `pnpm build`: passed on the final head.
- The full local `pnpm test:run` stopped in chat shard 6 on five
existing deadline assertions. Earlier chat shards passed; the PR CI chat
shards passed. This is not a full local-suite pass.
- The first CI server shard 4 attempt failed the unchanged legacy
environment-inheritance fixture at its five-second completion wait. The
same failure reproduced locally on the repair head and on an earlier
checkout without this repair. One unchanged CI retry passed. No fixture
assertion or timeout change is shipped.
- Latest-head CI has passed. Greptile reviewed the final head at 5/5
with no open threads. The separate Paperclip Review is paused. This PR
does not claim staging acceptance.

## Risks

The backup owner now lives with its session authority. A wrong authority
selection must fail rather than publish provider state. Database
publication still checks the original company, task, run, boot,
generation, and accepted result before and after upload. Tests change
the owner during upload and verify that the selected backup stays
intact. No schema, data migration, or new provider-work permission
changes.

## Model Used

OpenAI Codex, GPT-6 model family, with reasoning and code execution
tools. The host does not expose the exact runtime model ID or
context-window limit.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-05 18:04:47 -06:00
Devin FoleyandPaperclip 5ac05871b5 fix(onboarding): unify sign-in actions and recover failed logins (#19336)
## Thinking Path

> - Paperclip lets people create agents and connect their AI accounts.
> - Onboarding uses the shared login panel to connect Claude and OpenAI
accounts.
> - The sign-in link did not update the footer, so the footer could open
sign-in again.
> - A failed or expired sign-in also left onboarding without a retry
action.
> - This pull request gives both sign-in controls one action and makes
failed attempts recoverable.
> - Users can return with a code or start a fresh attempt while keeping
their setup.

## Linked Issues or Issue Description

**What happened?**

The onboarding instruction link opens Claude but leaves the footer
offering Sign in to Claude. If a code is entered without a paste event,
the footer opens provider sign-in again. A failed code, expired session,
or failed sandbox request can also leave an error with no retry control.
Component tests reproduce these failures.

This PR addresses that explicit footer action. It does not establish a
fix for unsolicited navigation of the original Paperclip tab to Claude
before code entry. That reported hosted-Cloud behavior remains under
investigation and needs browser-level evidence.

**Expected behavior**

The link and footer should use the same sign-in action. Both should open
one provider tab and put the footer into a disabled Waiting for code…
state. The code field should stay available. Failed or expired attempts
should show a clear message and an enabled Retry sign-in button.

**Steps to reproduce**

1. Choose Claude during onboarding with no saved subscription login.
2. Open Sign in to Claude from the instruction link.
3. Return and enter the authorization code without a paste event, such
as by typing it.
4. Press the footer action. Before this fix, it opens Claude again.

**Paperclip version or commit**

Reproduced in component tests against master at 31bf6e4482.

**Deployment mode**

The fixed footer defect is reproduced in the hosted onboarding component
path. The regression tests use the real wizard and login panel with
simulated API responses. No live Claude authorization was performed.

Related merged work: #13193 added immediate paste feedback; #12921 kept
the pasted code visible. Neither handles this footer path. The duplicate
search found no open PR for this defect.

## What Changed

- Use one handler for the sign-in link and footer. Opening either enters
Waiting for code… and disables the footer. The code field stays
available. The inline link can reopen the same provider URL if the
provider tab was closed.
- Submit an entered Claude code from the footer or Enter. Preserve
automatic submission on paste and block duplicate submissions.
- Replace raw red onboarding errors with a short failure or expiry
message. Hide the stale sign-in link and old code field.
- Add Retry sign-in in the onboarding footer. The login panel owns
recovery: release the old session, recheck the owner’s active session,
then resume it or start a fresh attempt. Clear the old code and keep the
onboarding choices.
- Pause polling during recovery. Treat an already-removed session as
released, but keep other cancellation failures retryable. Keep missing
resumed sessions recoverable.
- Give shared-card hosts without a retry footer an inline Retry sign-in
action.
- Add coverage for both sign-in controls, reopening the current provider
URL, failed codes, startup, lookup, prompt, status, expiry, missing
sessions, completion, cancellation, resume, and hosts without a retry
footer.
- Add failed and expired Claude sign-in stories to the production
onboarding suite. Their scoped fixtures support draft-agent saves,
cancellation, and a fresh second attempt.

The login panel owns code validation, session lifecycle, and retry. The
wizard owns the footer and opens the provider tab. Optional action
callbacks let each host own its controls; existing panel hosts retain
their session behavior.

## Verification

- The original four link and footer regressions failed before the
initial fix.
- `pnpm -r typecheck`: passed across the workspace. UI typecheck was
repeated after the final UI changes.
- From `ui/`, `pnpm exec vitest run --config vitest.config.ts
src/components/OnboardingWizard.test.tsx
src/components/AgentConfigForm.render.test.tsx
src/components/AdapterLoginChrome.test.tsx`: 405 tests passed.
- Component checks confirm that both controls open the same URL once,
disable the waiting footer, and leave the code field usable. Retry waits
for cancellation, clears the old code, and submits the next code to the
new session. Repeated clicks do not create duplicate attempts. Back
still prevents a late failure or success from reopening the card or
hiring.
- `pnpm check:token-gates` and `git diff --check`: passed.
- `pnpm build`: passed across the workspace before the final shared-card
fallback. The UI build was repeated after that change.
- Full local `pnpm test:run` was not repeated for this revision. The
earlier local run on `11fca74` passed all six chat integration shards
(1,931 tests), then was stopped after that head’s complete CI plan
passed. It did not finish locally.
- CI is pending on the updated head. Greptile reviewed
`876142d3877cdfe63e8438d00b605f4b66aded05` and scored it 5/5 with no new
actionable findings. All review threads are resolved.
- The earlier review of `4283b1c` scored 4/5 and found that the link
could not reopen a closed provider tab. That finding was fixed in
`876142d`. The handler accepts the waiting state while the footer stays
disabled. Regression assertions cover reopening the same URL without
starting another session.
- Storybook browser inspection used the real wizard with simulated APIs
in dark mode at 760 × 900. Both new story play functions reached their
completed state. Clicking Retry in the expired story restored an empty
code field and the second attempt’s authorization URL. No provider page
was opened.
- New story IDs: `onboarding-agent-arc--claude-sign-in-failed` and
`onboarding-agent-arc--claude-sign-in-expired`. Run `pnpm storybook` and
select Agent arc → Claude Sign In Failed or Claude Sign In Expired.
- Storybook catalog validation and its 30 node tests passed. This proves
the story structure and the local simulated interaction; it does not
prove a live provider exchange or Linux visual-baseline parity.
- No live provider sign-in or deployed browser acceptance was run. The
original unsolicited main-tab navigation report remains unverified.

Earlier CI evidence remains recorded: on `11fca74`, server shard 2/12
failed in the unchanged `remote-workspace-migration.test.ts`
orphan-index fixture. Creating its Git worktree lock file returned
`ENOENT` before the behavior under test ran. The test and migration
implementation have no diff in this PR. The focused local test passed (1
passed, 124 skipped). The same-head CI rerun and aggregate verification
passed on attempt 2. The cause of that initial failure was not
established.

Current-head CI failure retained: on `876142d`, server shard 11/12
failed in the unchanged `status-card-evaluation-work.test.ts` test
“coalesces one hundred committed source writes without delaying another
company's saved deadline.” The clock assertion at line 384 finished 610
ms after the saved deadline, against a limit of less than 500 ms. There
are no server changes in this PR. The focused local test passed (1
passed, 35 skipped). The aggregate verification job failed because
`general_tests` failed; it reported no separate test defect. The first
shard rerun request was rejected while the workflow was still running.
After the workflow completed, GitHub accepted a rerun of server shard
11/12 and its dependent verification job on the same head. That rerun is
pending. The cause of the initial timing overrun is not established; a
passing local test does not explain it. [Initial failed
job](https://github.com/paperclipai/paperclip/actions/runs/42123655875/job/127583449952).

## Risks

- The onboarding footer now tracks sign-in, code submission, and retry.
The ordinary paste flow stays automatic; typed codes still need an
explicit action.
- Retry cancels the current attempt before starting another one. A
cancellation failure keeps Retry available and prevents a second start.
- The failure card uses short, fixed text instead of raw provider
errors. Standalone configuration panels retain their existing error
presentation.
- No API, credential storage, authorization, schema, or deployment
changes.

## Model Used

OpenAI Codex, `gpt-6.1-sol`, xhigh reasoning. Used repository
inspection, code editing, and local command execution. Context window
size is not exposed by the harness.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-05 15:10:50 -08: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
Nicky LeachandClaude Opus 5.5 f9d366acdf fix(runner): require a managed AI connection for plugin Runner-task environments (#19341)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A plugin can provide Runner execution through an environment driver
that declares `supportsTasks: true`. Agent computers use this path.
> - Core let an agent without a managed AI connection run on that path.
The run then used the account that was installed on the plugin's
execution host.
> - That is not a supported credential source. When the host Runner did
not match, the run failed three times, 30 seconds apart, and then
blocked the task with a generic error. The error did not name the fix.
> - The plugin cannot refuse these runs itself. `describe` has no
connection data, and Core replaces plugin error text before the user
sees it.
> - This pull request makes a managed AI connection a requirement for
every plugin Runner-task environment. Core refuses a run without one
before it acquires a lease or calls the plugin.
> - The benefit is one clear notice that tells the operator to choose an
AI connection, and no wasted retries or plugin work.

## Linked Issues or Issue Description

Refs #19326. That pull request makes a Runner revision mismatch retry.
It noted that runs without a managed AI connection reach the Runner
check on a different path, which still blocks after 3 attempts. This
pull request removes that path for plugin Runner-task environments.

**Problem or motivation**
A `paperclip_runner` agent with no managed AI connection can run in a
plugin environment that provides Runner tasks. The run uses the account
installed on the execution host. If the host Runner does not match, the
native session retries 3 times and then blocks the task with
"Plugin-provided Runner execution operation unavailable". The operator
cannot tell what to fix.

**Proposed solution**
Refuse these runs in Core before any lease, plugin call, secret
resolution, or provider work. Use one stable reason,
`ai_connection_required`, for run admission, the setup probe, and agent
verification. Show a task notice that names the fix.

**Alternatives considered**
A refusal in the plugin. It cannot see the connection at `describe`,
which runs first. Its error text is also replaced twice before the user
sees it. A new structured plugin error code would still not cover
`describe`.

## What Changed

- Add `server/src/services/plugin-runner-ai-connection.ts`. It holds the
`ai_connection_required` code and message, the probe result, and the
manifest lookup for a Runner-task driver. The lookup moves here from
`agent-runner-selection.ts`, so run admission and runner selection use
the same rule.
- `heartbeat/execution-configuration.ts`: refuse a new
`paperclip_runner` run in a Runner-task environment when it has no
managed AI connection. This also applies when `homeMode:
"preconfigured"` drops the connection. The run fails as
`configuration_incomplete` with reason `ai_connection_required` and an
action URL to the agent's runtime settings. The check runs before
observability, secrets, the managed runtime, the workspace, and the
lease. A resumed native run keeps its admitted contract.
- `recovery/stranded-notice.ts`: add an "AI connection required" task
notice.
- `run-failure-report.ts`: treat this pre-dispatch refusal as an owner
action, not an unexpected failure.
- `adapters/registry.ts`: the unmanaged probe for a Runner-task target
now returns a failed `ai_connection_required` check. It calls no task
operation. Managed probes still go through `testManagedEnvironment`.
- `agent-harness-verification.ts` and
`agent-lifecycle/application/failure.ts`: classify the check as
`ai_connection_required` with a fixed message. Verification waits for
the configuration to change instead of retrying.
- Docs: `PLUGIN_RUNNER_EXECUTION.md` states the requirement.
`PLUGIN_ENVIRONMENT_TASKS.md` no longer mentions installed accounts.
`agent-config-ui.md` notes the requirement.
- Tests: the "plugin-owned account" heartbeat test described the
behavior that this change removes. It now checks the assembled heartbeat
refusal: no plugin call, no native boundary, no adapter, no workspace
files.

## Verification

- `npx vitest run` in `server/` for these files. All tests passed:
- `src/__tests__/heartbeat-execution-configuration.test.ts`. New cases:
refused without a connection, refused for a preconfigured home, managed
passes, resumed native input passes, non-task plugin driver passes. The
refused cases also check that observability, secrets, and the managed
runtime did not start.
- `src/__tests__/heartbeat-plugin-environment.test.ts`. One test,
"inherits the instance default environment across companies…", also
fails on unmodified `master` in my environment. This change does not
touch it, because its driver does not declare `supportsTasks`.
- `src/adapters/registry.test.ts`,
`src/__tests__/agent-harness-verification.test.ts`,
`src/modules/agent-lifecycle/__tests__/diagnostics.test.ts`,
`src/services/recovery/stranded-notice.test.ts`, and
`src/services/__tests__/run-failure-report.test.ts`.
- 17 more suites that use plugin Runner-task environments, with 705
tests: `plugin-runner-task.integration`,
`plugin-warm-tasks.integration`, `task-creation-recovery.integration`,
`native-startup-cleanup-recovery`,
`environment-task-authority.integration`,
`plugin-task-credential-writeback`, agent-lifecycle `lifecycle`,
`setup`, and `environment-plugins`, `workspace-preparation`,
`agent-runner-selection`, `agent-harness-verification-provider-api`,
`agent-harness-verification-remote`, `plugin-environment-driver-seam`,
`environment-task-runtime`, `agent-test-environment-routes`, and
`adapter-registry`.
- `pnpm typecheck` in `server/` reported no errors in the changed files.
Its only errors came from sandbox-provider packages that my local
install did not link.
- Not run: `plugin-runner-probe.test.ts`, because it needs a Cargo
build. This change does not modify `probePluginRunnerEnvironment`. Also
not run: the full `pnpm test:run` and `pnpm build`. CI runs them.

## Risks

- Behavior change: an agent in a plugin Runner-task environment with no
managed AI connection now stops before dispatch. Before, it ran with the
host's installed account. The agent's owner must choose an AI
connection. This includes agents with `homeMode: "preconfigured"` in
these environments.
- Runs that were already admitted and resume from saved native input are
not refused. They keep the earlier behavior until they finish.
- The check reads the plugin manifest on each new run admission for
plugin environments. This is the same lookup that runner selection
already does.
- This pull request and #19326 both edit `stranded-notice.ts` and
`failure.ts`. The changes are additive, but whichever merges second
needs a small rebase.

## Model Used

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

## Checklist

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

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents run through a harness (Codex app-server, or Claude through
ACPX), and the runner turns harness events into Paperclip events
> - Both harnesses report how full the model's context window is. Codex
sends `modelContextWindow` with its token usage. Claude sends `used` and
`size` in ACP `usage_update`
> - Paperclip dropped both values. The task view could not show how
close an agent is to compaction, and a compaction left no visible mark
in the task chat
> - This pull request carries the context-window occupancy through the
runner, stores it with the existing usage events, and shows it as a
"Context" row in the task Properties panel
> - It also shows a "Context compacted · 230k → 38k tokens" divider in
the task chat when the agent compacts its context
> - The benefit is that operators can see when a long task is near its
context limit and when the agent lost detail to a compaction

## Linked Issues or Issue Description

Refs #19252 (same pattern: a canonical harness event turned into
task-chat UI). Refs #19267 (status notices; it turns Claude's compaction
banner into a notice, and this PR reads that notice too).

**What existing behavior does this improve?**
The task view shows no context-window information, and a compaction is
invisible in the task chat.

**Subsystem affected**
Runner usage normalization (Rust and TypeScript transport), the PRP
usage schema, a read-only issue API, the task Properties panel and the
task-chat transcript.

**Current behavior**
Codex `modelContextWindow` and ACP `used`/`size` are dropped in the
runner. `context.compacted` always has null sizes, Claude compactions
emit no `context.compacted` event at all, and the live runner turn hides
every marker except "interrupted".

**Proposed behavior**
`usage.reported` carries an optional `context {usedTokens,
windowTokens}`. The task Properties panel shows a Context row ("10% of
258k") with a hover breakdown (tokens used, input and cached input,
output, cost, compactions). The transcript shows a compaction divider,
with sizes from the usage before and after the compaction.

**Reason and benefit**
Operators can see when a long task is close to its context limit, and
they can see where the agent's earlier context was summarized.

**Breaking changes**
None. The new schema field is optional, and the new endpoint is
read-only.

## What Changed

- PRP `usage.schema.json`: add an optional `context {usedTokens,
windowTokens}` object. Regenerate the schema bundle and the protocol
manifest.
- Rust `provider_events.rs`: add `codex_context_usage` (last request
`totalTokens` and `modelContextWindow`) and `acpx_context_usage` (ACP
`used`/`size`). ACPX `usage_update` now sets `context`.
- Rust `provider_backend.rs`: Codex `thread/tokenUsage/updated` sets
`context` on live (non-historical) usage.
- Rust `provider_events.rs`: emit `context.compacted` for the completed
`context_compaction` provider notice that #19267 produces from Claude's
compaction banner. Only this path marks a Claude compaction, so the
server counts each compaction once.
- `runnerd-codex-transport.ts`: `rehydrateRunnerdUsageNotification`
keeps a valid `context` on `tokenUsage`, so it reaches the stored
`item.completed` usage item.
- Server: new `GET /api/issues/:id/context-usage` (issue read access is
required) and `issueContextUsage` service. It reads only the assignee's
current session (the session of its newest usage report), and returns
that session's latest context reading, non-zero totals, cost, and the
count of compactions across all of the session's runs. Added to OpenAPI.
Shared type `IssueContextUsage`.
- DB: migration 0432 adds two partial indexes on `heartbeat_run_events`,
built concurrently. One indexes usage items by run and time. The other
indexes `context.compacted` events by company and session. `EXPLAIN`
shows that both lookups use them.
- UI: new `ContextUsageMeter` (bar, percentage, hover card) and
`IssueContextUsageRow` in the Execution section of Properties. The row
refreshes every 8 s while a run is active, and once more when the run
ends.
- UI transcript: `native-run-events.ts` maps `context.compacted` to a
`context` activity entry with pre/post sizes from neighbouring usage
items. `transcript-adapter.ts` turns it into a new `context_compacted`
marker variant. The live runner turn keeps that marker as its own
timeline row.
- Storybook: `task-context-window.stories.tsx` (Properties panel in a
task, all states, hover breakdowns, divider).

## Verification

- `cargo test -p paperclip-runner-core --lib provider_events` (new
tests: ACPX context, Codex context, banner compaction, notice
compaction)
- `pnpm --filter @paperclipai/paperclip-runner exec vitest run
src/live/runnerd-codex-transport.test.ts`
- `cd ui && npx vitest run src/components/ContextUsageMeter.test.tsx
src/components/transcript src/components/task-chat
src/components/issue-properties`
- `cd server && npx vitest run
src/__tests__/issue-context-usage.test.ts` (embedded Postgres)
- `node scripts/typecheck-low-memory.mjs`, `pnpm check:token-gates`,
`pnpm --filter @paperclipai/paperclip-runner run check:protocol-types`
- Live check on a local worktree instance through the runnerd path:
- A Codex `gpt-5.5` task showed "10% of 258k" (27k of 258k, input 77k
with 49k cached, output 1.1k).
  - A Claude Sonnet 4.6 task showed "17% of 200k" (34k of 200k).
- Storybook: Tasks and conversations / … / Task chat / Context window.
- Not verified live: a real compaction. The divider is covered by unit
tests through the same projector path the task chat uses.

## Risks

- The schema field is optional, and older runners omit it. The UI
renders nothing until a harness reports a window.
- Migration 0432 builds two partial indexes concurrently on
`heartbeat_run_events`, which is a large table. The indexes cover only
usage items and compaction events, so they stay small. They add a little
write overhead to those events.
- Claude in-progress usage rows still record zero tokens with `requests:
1`. The final turn usage is correct. This PR does not change accounting.
- A "Compact now" action is out of scope. It needs a compaction-only run
that does not produce a run result. It will come in a follow-up.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code with tool use (shell, file edits, in-app browser for Storybook and
the live instance).

## 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-05 16:54:27 -06:00
Nicky LeachandClaude Opus 5.5 a6ab6a301c fix(server): retry a run when its remote Runner is out of date (#19326)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - An agent can run on a remote Runner that an environment plugin
provides
> - A release can update the app before the remote Runner, so for a
short time the app sees an older Runner
> - In that window the plugin can refuse the task, but Core cannot tell
this refusal from other errors
> - Today the run fails as `setup_failed` with no retry, and the task
goes to board recovery. Before #19246, it was also reported as a project
authentication conflict
> - Agent verification also counts the refusal as a failed harness test,
and it parks the agent after three tries
> - This pull request gives the plugin a stable error code for a Runner
mismatch, and Core waits and retries when it gets that code
> - The benefit is that a release no longer blocks tasks or parks
agents, and a real authentication conflict keeps its own clear notice

## Linked Issues or Issue Description

Refs #19246 (it stopped reporting every precheck error as a project
authentication conflict)
Refs #19249 (the same bounded bootstrap retry for a stopped plugin
worker)
Refs #19302 (shares `environment-task-error.ts`; its diagnostic envelope
is unchanged)
Refs #19172 (related open PR: it reports an undescribable plugin Runner
as a retryable check in the same precheck)
Refs #18667 (a real `ai_connection_incompatible` conflict, which now
gets its own notice)

**What happened?**
A run started while its remote Runner was older than the app. The
environment plugin refused the task because the installed Runner did not
match the revision it expects. Core recorded a generic setup failure,
scheduled no retry, and blocked the task. Before #19246, Core reported
this as a project authentication conflict. An agent in `verifying`
counted the same refusal as a failed harness test. After three tries it
stopped verification until a person chose Retry.

**Expected behavior**
Core treats a Runner mismatch as temporary. A run retries without
blocking its task. A `verifying` agent checks again until the Runner
matches. Only a real `ai_connection_incompatible` result blocks the
task, with a notice that names the conflict.

**Steps to reproduce**
1. Use an agent on a plugin environment whose installed remote Runner
revision differs from the revision the plugin expects.
2. Wake the agent on a task, or put a new agent through verification.
3. The run fails with `setup_failed` and the task is blocked.
Verification parks after three tries.

**Paperclip version or commit**
master at 377e98f1a2.

**Deployment mode**
Authenticated deployment with a plugin environment that provides a
remote Runner.

## What Changed

- Plugin SDK: add `PluginEnvironmentTaskError` and
`ENVIRONMENT_TASK_ERROR_CODES` with one code,
`runner_revision_mismatch`. The worker RPC host sends it as structured
`environmentTask` error data with the schema
`paperclip/environment-task-error-code/v1`
(`environmentTaskErrorCodeData`, read with
`readEnvironmentTaskErrorCode`). This uses the same envelope method as
the other environment RPC errors. Core reads only the code, never the
message. The source refusal diagnostic from #19302 keeps its own schema
and API unchanged, and it takes precedence on a source `submit`.
- `executeEnvironmentTask` still replaces each worker error with a
generic error. After the #19302 diagnostic check, it now keeps only that
closed code.
- Run setup: a Runner mismatch before provider dispatch gets the error
code `runner_revision_mismatch`. Setup schedules a bounded retry with
its own reason, `runner_revision_wait`, and budget (10 attempts, 60 s
apart). The reason is in the same resource-wait lane as workspace and AI
connection waits, so a wait does not spend the failure retry budget. The
guards are the same as for `plugin_worker_unavailable`. Recovery and
legacy reconciliation treat the code like the other bootstrap-only
codes. `scheduleBoundedRetryForRun` uses the Runner wait reason and
budget for a mismatch run when a caller gives no reason, and the
recovery sweeper reports exhaustion from that budget
(`failedRunRetryBudgetSpent`). An answered interaction that hits a
mismatch also waits for the Runner, and its successor keeps the resolved
interaction continuation identity.
- Move the managed project auth classification from `heartbeat.ts` to
`classifyManagedProjectAuthFailure` in `run-preparation.ts`. Only a
found `ai_connection_incompatible` conflict becomes a configuration
failure. All other errors keep their own code.
- Recovery notices: `ai_connection_incompatible` now has its own notice
instead of the "secret/env bindings" text. An exhausted Runner wait gets
the notice "Remote Runner is out of date".
- Agent verification: the Runner probe and the managed project auth
precheck report a mismatch as a retryable check.
`assertHarnessTestPassed` maps it to the new lifecycle code
`runner_revision_mismatch`. That code never counts toward the
three-failure park. The agent checks again every 60 s and becomes ready
when the Runner matches.
- Document the error code in `doc/plugins/PLUGIN_ENVIRONMENT_TASKS.md`.

## Verification

- `cd packages/plugins/sdk && npx vitest run
tests/environment-task-error-code.test.ts
tests/environment-task-error.test.ts tests/worker-rpc-host.test.ts`: the
code crosses the real `environmentTask` RPC. A plain error with the same
message gets no code. Other methods get no envelope. The #19302
diagnostic tests still pass.
- `cd server && npx vitest run
src/services/heartbeat/run-completion.test.ts`: a fresh or wrapped
mismatch gets `runner_revision_mismatch` and the runner retry budget,
with no configuration failure. A started, untrusted (message only) or
native run keeps `setup_failed` with no retry. A real
`ai_connection_incompatible` stays `configuration_incomplete` with no
retry. Other errors, such as an unreachable execution host, are not
reported as an authentication conflict.
- `npx vitest run src/services/execution-recovery-attempt.test.ts`:
Runner waits do not change `failureRetries`, and earlier failures do not
shorten the wait.
- `npx vitest run src/services/heartbeat/retries.test.ts
src/modules/run-dispatch` (embedded Postgres): a recovery caller without
options gets the Runner wait budget and its exhaustion. An answered
interaction waits for the Runner, keeps its continuation identity and
does not spend failure retries.
- `npx vitest run src/__tests__/environment-task-runtime.test.ts`:
sanitization keeps only the code. No worker text or cause survives.
- `npx vitest run
src/modules/agent-lifecycle/__tests__/verification-retry.test.ts`
(embedded Postgres): a `verifying` agent keeps checking after five
mismatches. It never gets `retryRequired`. It becomes ready when the
next check passes.
- `npx vitest run
src/services/native-runtime/plugin-runner-probe-runner-mismatch.test.ts
src/__tests__/agent-harness-verification-remote.test.ts
src/services/recovery/stranded-notice.test.ts
src/services/recovery/service.pause-durability.test.ts
src/services/legacy-execution-recovery.test.ts`.
- `npx tsc --noEmit` passes for `packages/plugins/sdk` and `server`.
- One test in `src/__tests__/heartbeat-process-recovery.test.ts`
("redacts opaque environment-bound credentials from Sentry diagnostics")
fails on my machine. It fails the same way on the base commit without
this change.
- I did not run the full `pnpm test:run`, `pnpm -r typecheck` or `pnpm
build`. CI covers them.

## Risks

- Low to medium. The change adds one new setup failure code and one
lifecycle code. Other failures keep their current behavior.
- A Runner mismatch must come from a plugin that throws
`PluginEnvironmentTaskError`. Until an environment plugin does that,
behavior is the same as today.
- The run retry budget is 10 minutes. If a Runner update takes longer,
the task goes to board recovery with the Runner notice.
- A remote Runner run without a managed AI connection reaches the Runner
check only after native dispatch. That configuration is not supported,
so this change does not handle it. Such a run keeps the native session
retry (3 attempts, 30 s apart).
- #19172 edits the same precheck call in `agent-environment-test.ts`.
Whichever PR merges second needs a small rebase. Keep the mismatch check
before its generic retryable check.

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

## Model Used

- Claude Opus 5.5 (Anthropic), model ID `claude-opus-5-5`, used through
Claude Code with tool use (code search, shell and test runs). Reasoning
mode: standard agentic coding.

## 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 14:34:23 -08:00
DottaandPaperclip 789a71bc17 fix(runner): retry quarantined tasks in their saved workspace (#19340)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native sessions retain execution ownership and action history across
turns.
> - Task Retry must prove that old execution has stopped before it
starts another turn.
> - An attempt can inherit its predecessor's identity and fail before it
launches a provider.
> - The cleanup proof searched under that failed attempt and compared
unrelated event sequences.
> - This pull request verifies the retained owner and starts one fresh
authorized turn.
> - Saved results and uncertain action outcomes remain intact.

## Linked Issues or Issue Description

**What happened?** Task Retry accepted repeated clicks but kept the
canonical request deferred with `executionWait.reason: local_cleanup`.
It created no successor, although the retained local worker and provider
had stopped.

**Expected behavior:** After exact cleanup proof, admit one fresh turn.
Preserve the old journal and result. Keep unresolved action outcomes
unknown.

**Steps to reproduce:**
1. Finish a local native turn with retained cleanup state and a saved
result.
2. Let the next attempt inherit its identity and fail with
`native_session_cleanup_quarantined` before any launch.
3. Upgrade across a tool-contract change, then click task Retry.
4. The old code requires the failed attempt's stop receipt and journal
identity. It also requires the current tool fingerprint and equal
sequence numbers across raw and normalized events. Those requirements
cannot match the retained owner.

**Paperclip version or commit:** Reproduced on `31bf6e4482`.
**Deployment mode / installation method:** Authenticated, self-hosted
source checkout.
**Agent adapter(s) involved:** Native `paperclip_runner`, local Codex
app-server. The shared physical-stop proof also covers ACPX.
**Database mode:** Embedded PostgreSQL 18.1.
**Access context:** Board task Retry.
**Node.js version / operating system:** Incident: Node 24.20.0, Linux
ARM64. Local verification: macOS ARM64.
**Relevant logs or output:** Accepted Retry stays in
`deferred_issue_execution`; no new run or provider launch occurs.
**Relevant config:** Per-turn local native execution. The old tool
contract differs from the current contract.
**Additional context:** Related: #19311 prevents the oversized tool
result that can lead to retained cleanup. #18217 covers broader cleanup
isolation and retained-result maintenance. #19134 admits stopped Runner
sources under the current run lease. This patch fixes explicit
continuation admission on current master.

## What Changed

- Resolve the retained journal's owner under the same company, task,
agent, workspace, native session, and runner instance.
- Require terminal ownership, settled controllers, released local
leases, and exact inherited process birth. Refuse any launch evidence on
the failed attempt.
- Accept a current host's verified process absence from its exact launch
receipt when an older server lacks a stop receipt. A later launch
invalidates that observation.
- Validate normalized receipts against their own IDs, sequences, and
digests. Match ordered provider identities to the independent raw
journal.
- Keep tool compatibility at the session reuse boundary. Explicit
continuation always uses a fresh session.
- Bind the stop proof to its exact successor and canonical Retry
receipt. Reuse the proven local workspace through normal preparation.
Refuse changed bindings, closed or remote workspaces, pending
operations, and other live leases. Projectless run-ID placeholders keep
the existing fresh-workspace fallback.
- Retain journals, results, files, and uncertain action outcomes. Add
regression and opt-in live acceptance coverage.
- Make the split-handoff browser test wait for both child-completion
notices and all run settlement before teardown. Assert the final task
set stays unchanged.

## Verification

- Red: canonical Retry on the original code accepts one saved intent but
creates zero successors and records `local_cleanup`.
- Red: distinct raw and normalized event sequences reproduce that wait
after owner resolution alone. After admission was repaired, live
acceptance then exposed the empty-attempt workspace problem. A
PostgreSQL regression also reproduced that wrong workspace selection.
- Green at `6684df5e82`: **479 targeted cases**, including 26 new
PostgreSQL cases for duplicate clicks, exact process and workspace
ownership, controller/lease fences, changed state, and independent
receipt integrity. The complete 1,150-test native executor suite passed
earlier.
- `pnpm -r typecheck` and `pnpm build` pass again at final head
`432a52bea8`, including its build stamp. Token gates pass. Master now
contains the unrelated directory-fixture type correction found during
the first typecheck.
- Real authenticated HTTP Retry with Codex `gpt-6.1-sol`, high effort,
and per-turn execution passes again at final head `432a52bea8` (93.3
seconds). It preserves the first file line, appends the second once,
starts a fresh session in the existing workspace, keeps the retained
journal unchanged, commits finalization, and clears task locks.
- The strengthened split-handoff browser scenario passes **three
consecutive local runs**. Its original CI teardown and one same-head
rerun failed when cleanup cancelled an active legacy
completion-notification run and hit a PostgreSQL row-lock timeout. The
scenario now waits for the complete handoff before cleanup. [Initial
failure](https://github.com/paperclipai/paperclip/actions/runs/42106466518/job/127523693634),
[same-head
rerun](https://github.com/paperclipai/paperclip/actions/runs/42106466518/job/127531698321).
These failures remain part of the verification record.
- The local full `pnpm test:run` passed all six chat integration shards.
Its obsolete pre-rebase attempt was stopped during the general server
phase; no complete local-suite pass is claimed. Full CI, including both
required gates, passed at `6684df5e82`. At final head `432a52bea8`, all
initial CI groups passed except the unchanged status-card deadline test,
whose post-wakeup clock assertion missed its bound by 10 ms. That exact
case passed three unchanged local repetitions (16.8/27.0/18.3 seconds);
the single isolated shard rerun passed all **2,218 tests** (123 files),
with no source changes. [Initial timing
failure](https://github.com/paperclipai/paperclip/actions/runs/42114473652/job/127550722187).
- Greptile reviewed `6684df5e82` at **5/5**, with no actionable
findings. A second reviewer identified the supported projectless
placeholder case. A new real PostgreSQL regression reproduced its
workspace-binding failure before the one-line fallback correction; all
**45 focused cases** now pass, including default environment selection
and changed binding/cwd refusal. Greptile reviewed final head
`432a52bea8` at **5/5**, with no blocking findings. The second
reviewer’s thread is resolved.
- The patched Linux build verified the actual retained worker/provider
ownership and process absence in a transaction that was rolled back. No
retained state was retired or provider started during that
pre-deployment check.

## Risks

- This changes an execution admission proof. Incorrect ownership could
permit a second executor. The proof retains process, controller, lease,
scope, journal, and receipt fences, and duplicate admission is tested
through the real queue.
- Missing or conflicting evidence remains deferred. Windows, remote
execution, automatic replacement, and session reuse retain their
existing policies.
- No database migration or UI change. The original affected task still
needs deployment of the fix before recovery can be claimed.

## Model Used

- OpenAI GPT-6 through Codex, with reasoning, repository tools, code
execution, SSH diagnostics, and live provider testing. This session does
not expose the exact backend model build or context-window size. The
live test agent uses OpenAI `gpt-6.1-sol` at high effort.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-05 16:04:38 -06:00
DottaandPaperclip 15249a689d fix(server): preserve sessions across cosmetic sandbox updates (#19349)
## Thinking Path

> - Paperclip manages agent work and saves provider sessions between
turns.
> - Session and workspace compatibility include the selected environment
revision.
> - The managed sandbox reconciler also updates names, descriptions, and
stock metadata.
> - Those cosmetic updates advanced the revision even when the execution
configuration stayed the same.
> - A later turn therefore discarded an otherwise compatible saved
session.
> - This pull request keeps that revision stable for cosmetic
reconciliation and retains resets for real configuration changes.

## Linked Issues or Issue Description

Related: #18943 and #18552.

**What happened?**

A managed sandbox rename or description update made a later task turn
start a fresh provider session. The driver, provider, snapshot, and
agent configuration could all remain unchanged. Both session and
workspace freshness use the environment's `updatedAt` as its
configuration revision.

**Expected behavior**

Cosmetic managed reconciliation preserves compatible task sessions.
Actual sandbox configuration or availability changes retain the normal
replacement checks.

**Steps to reproduce**

1. Create a managed sandbox and save a task-session fingerprint for it.
2. Reconcile the same sandbox configuration with a new name,
description, or managed metadata.
3. Evaluate the saved session against that environment. Before this fix,
the decision resets the session and reports the environment category.

**Paperclip version or commit**

Reproduced on master `d3713d827f70ddba63a323e57c562384dbd648ba`.

**Deployment mode**

Managed sandbox server. The regression uses an isolated PostgreSQL
database and the actual reconciler and session-freshness resolver.

## What Changed

- Keep the existing revision for an active managed sandbox when its
execution configuration stays equal.
- Continue advancing the revision when configuration or availability
changes.
- Retain activity records and managed-stock binding updates for cosmetic
reconciliation.
- Add a regression that checks session reuse, audit history, and
rejection after a real snapshot change.
- Document the freshness contract.

## Verification

- The new PostgreSQL regression failed before the production change: it
reported `reset: true` and changed category `environment`.
- All 53 environment-service cases pass after the fix.
- All 190 session-freshness and assembled heartbeat session-preparation
cases pass.
- Full local typecheck passes. The full local Vitest run remains active,
with its local build queued afterward. Initial typecheck setup failures
were retained and resolved by repairing generated SDK links; no source
workaround was added.
- [Full current-head
CI](https://github.com/paperclipai/paperclip/actions/runs/42114984839)
passes, including the sharded test suites and build: 55 successful
checks, 3 skipped, and 1 neutral.
- Greptile and Ponytail both reviewed this commit at 5/5 with no
findings. No review threads remain unresolved.
- Deployed verification remains pending normal delivery. No pinned
release or manual rollout is required.

## Risks

- Cosmetic managed reconciliation keeps `environments.updatedAt`
unchanged, so it does not reorder that environment by this timestamp.
Activity records and managed-resource bindings still record the update.
- The comparison uses canonical configuration hashes, so JSON property
order cannot hide a real change or create false drift.
- Existing fingerprint formats and saved session records remain
compatible. Real configuration and availability changes still advance
the revision.

## Model Used

OpenAI GPT-6 through Codex desktop, with reasoning and terminal/browser
tools. The host does not expose a more specific deployment suffix or
context-window size.

## Checklist

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

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-05 16:03:30 -06:00
Nicky LeachandPaperclip 9a79384958 Relay bounded Runner source refusal diagnostics (#19302)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - An environment plugin can submit a Runner continuation from a
retained source.
> - The host can refuse that continuation when its source is unverified
or has an unresolved operation.
> - The worker and Core currently discard that bounded reason.
> - Raw provider errors can contain credentials, text or foreign
identities.
> - This pull request relays only a closed diagnostic and keeps the
existing admission and redaction rules.

## Linked Issues or Issue Description

Refs #19134 and #19157. This independent fix is based on master. A
search found no duplicate relay.

**What happened?**

A source submit can receive HTTP 409 with `SESSION_STATE_UNAVAILABLE`.
The worker drops its structured data. Core then replaces the error with
a generic message. The caller cannot distinguish the two known
diagnostic reasons safely.

**Expected behavior**

Carry only the fixed status, code and reason through SDK worker RPC and
Core. Keep all provider text and identities out of the relayed error. A
diagnostic must not authorize a retry or change state.

**Steps to reproduce**

Submit an authorized retained source through a task provider. Have the
provider throw the recognized state-unavailable error. Read the error
after worker RPC and Core dispatch.

## What Changed

- Export `PluginEnvironmentTaskErrorDiagnostic`,
`environmentTaskErrorData` and `readEnvironmentTaskErrorDiagnostic`.
- Accept only `httpStatus: 409`, `errorCode:
"SESSION_STATE_UNAVAILABLE"` and reason `"unresolved_operation"` or
`"source_state_unverified"`.
- Attach the versioned envelope only to an environment task `submit`
with `resumeFrom`. Replace its provider message with a fixed message.
- Revalidate and reconstruct the closed error in Core. Keep other
failures under the existing generic redaction.
- Document older SDK behavior and the diagnostic-only boundary. No
grant, retry, discard, lifecycle or admission rule changes.

## Verification

- Rebased head `7d02bb3e4c` includes master’s UI fixture fix from
#19342. The sole conflict was in the worker integration test imports. It
now imports both the physical-task result schema from #19300 and the
diagnostic reader and RPC error type from this PR. The 39 SDK/worker
cases and 57 Core/runtime cases passed, including PostgreSQL worker
integration. The SDK build and Runner/server TypeScript checks passed.
[Current-head
CI](https://github.com/paperclipai/paperclip/actions/runs/42111889388)
passed without retries. All 56 check-rollup entries were green,
including Snyk and Socket; [standard Greptile review
2](https://github.com/paperclipai/paperclip/pull/19302#issuecomment-6665823815)
scored this exact head **5/5**, with zero unresolved threads. The PR was
marked ready on November 5 at 21:37 UTC.

- Both new worker relay cases fail against the baseline worker and pass
with the fix.
- 46 SDK/worker cases passed, including both reasons, malformed/foreign
envelopes, extra identities, unsafe getters and non-source operations.
- 51 Core/runtime cases passed. The integration uses real SDK
serialization and dispatch with PostgreSQL through the existing
authorized retained-source submit. It verifies both reasons, malformed
RPC rejection, extra-field removal, preserved source outcome and
existing dispatch bookkeeping. A valid envelope on an ordinary submit or
status stays generic.
- Runner and SDK builds and server TypeScript check passed. An initial
Runner build lacked its eval-kernel build prerequisite; it passed after
that prerequisite was built.
- The first expanded DB fixture used a shared environment name and
failed its unique constraint. It now uses its unique fixture ID; both
complete focused files pass.
- Diff and added-line secret scans passed. [GitHub workflow
42083410387](https://github.com/paperclipai/paperclip/actions/runs/42083410387)
passed on its second attempt. All CI checks completed, and Snyk passed.
Standard Greptile scored exact head `38559ec4df8c` 5/5 with no
unresolved threads. That result applies to the previous head.
- The first GitHub attempt failed in the unchanged concurrent-budget
case in `decision-models.test.ts` (one provider call observed, two
expected). That exact case passed locally without code or assertion
changes. The workspace shard also passed 4,960 tests before the
unchanged CLI company import/export fixture exceeded its 30-second
child-server startup wait. No port-collision cause is claimed; that
fixture deliberately reserves the requested port. Both initial logs are
retained. One failed-job-only retry passed without source changes.

## Risks

Older SDKs keep the generic failure. The envelope is diagnostic evidence
only. Provider operation and call identities are never accepted.
Specific user-visible operation reporting must use Core's retained event
or receipt under its existing authority.

Master does not contain the new claimed predecessor orchestration. This
PR tests the existing retained-source submission boundary. It does not
claim the #19157 source recovery or old-journal end-to-end
qualification. Runtime pins and deployments remain separate.

## Model Used

OpenAI GPT-6.1 Sol (`gpt-6.1-sol`), high reasoning. Used repository
tools, TypeScript, SDK RPC and real PostgreSQL tests.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used with version and capability
details
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have described the issue in-PR using the bug template
- [x] I have not referenced internal or instance-local Paperclip issues
or links
- [x] My branch name describes the change
- [x] I have run the focused tests locally and they pass
- [x] I have added tests for the new behavior and rejection paths
- [x] I have updated relevant documentation
- [x] I have considered and documented risks
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no unresolved findings
- [x] I will address reviewer comments before requesting merge

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-05 13:38:55 -08:00
b929ad43de fix: keep dependency waits neutral and explain GitHub access failures (#19315)
## Thinking Path

> - Paperclip manages agent work and shows users what can move it
forward.
> - Task status and tool approval cards describe different parts of that
work.
> - A queued dependency wait has no provider attempt to retry.
> - GitHub can reject an approved action while other connection tools
still work.
> - These cases showed a false Retry banner and an unhelpful provider
error.
> - This pull request keeps dependency waits neutral and gives GitHub
access failures a direct repair path.

## Linked Issues or Issue Description

Related: #19268 preserves pending Retry preparation and current failure
causes.

**What happened?**

A queued wake cancelled by the dependency gate appeared as a failed
attempt with Retry. A GitHub MCP permission denial appeared as raw HTTP
error text after an approved repository action.

**Expected behavior**

Keep the task's dependency state. Show GitHub access failures with a
clear explanation and a link to the selected connection's Permissions
page. Preserve actual failures, provider details, and approval gates.

**Steps to reproduce**

1. Queue a wake, then leave its task dependent on unfinished work before
dispatch.
2. Open the task after the dependency gate cancels that wake.
3. Separately approve a GitHub action whose MCP result contains `403
Resource not accessible by integration`.
4. Reopen the saved tool approval.

## What Changed

- Recognize both queues' unstarted dependency-wait receipts in
cancellation provenance and task projections. No dispatch or recovery
gate changes.
- Classify GitHub's specific permission error after MCP result
normalization. Keep result validation, the raw diagnostic, and working
tools available.
- Apply the same display code to both historical GitHub error codes.
Identify the provider from company-scoped connection metadata, including
renamed apps, without rewriting stored decisions.
- Show a token-based inline access card with **Manage GitHub access**
and collapsed **Error details**.
- Give agents trusted recovery steps after an approved GitHub action
fails. Keep provider text in the untrusted result and prevent automatic
replay.
- Add regressions, a production-component Storybook case, and
execution/connector documentation.

## Verification

- Red before the fix: cancellation provenance, dependency-wait
projection, the real queue cancellation, approved MCP failure
classification, saved interaction projection, and the error card
assertions failed at the expected behavior.
- Green focused tests: queue cancellation, one ready-task executor,
approval-before-dispatch, one failed GitHub request, connection reuse,
retained provider diagnostics, saved interaction reads, task list/detail
projection and redaction.
- UI: 106 component tests passed. Token gates and Storybook catalog
checks passed.
- `node scripts/typecheck-low-memory.mjs`: passed.
- Local source instance: verified a saved dependency wait stays neutral
after reload; a saved GitHub error shows repair guidance, expands its
original error, and opens the correct connection Permissions page.
PostgreSQL and HTTP APIs were real. The GitHub denial was a controlled
MCP fixture; no account or provider permission changed.
- `pnpm build`: passed for all workspace packages, the Runner binary,
server, CLI and UI. The server build and all server typecheck layers
also passed after the review repair.
- Additional safety checks passed for realistic repository arguments,
active recovery, failed Retry preparation, and bounded provider
diagnostics.
- Review repair: 45 pure tests and 7 PostgreSQL cases passed. These
cover both historical error codes, renamed apps, and
company/provider/application identity boundaries. A second local browser
check verified the renamed app, retained error detail, and correct
Permissions destination.
- Final review regressions: the dispatcher and metadata cases failed
before the repair. Afterward, 25 focused cases and 59
projection/identity cases passed, including connection-key precedence.
The native wait and app-only GitHub source also passed a local browser
check.
- The historical read adds at most one connection query per interaction
read batch. It selects IDs and the provider key only and runs only for
candidate permission failures. It adds no provider requests.
- Full local `pnpm test:run` stopped at the chat suite's `afterAll`
cleanup timeout. All 268 assertions in that group and the three prior
chat groups passed. The failure is retained here; it is not counted as a
full-suite pass. CI passed on `e8a692e1e2` after one workspace-startup
rerun. Fresh CI is pending for the combined head. Its first workspace-a
attempt timed out in the CLI company import/export test's child-server
startup wait; 4,968 UI tests and 287 CLI tests passed in that job. The
same shard passed on the preceding commit. The workspace shard passed on
its one rerun. All other test, build and browser jobs passed. The [first
failure](https://github.com/paperclipai/paperclip/actions/runs/42093823928/job/127478869941)
is retained in the Actions history. Greptile reviewed the preceding head
at 5/5. Fresh review is pending for the combined head.

## Risks

The GitHub classifier matches the documented permission-denial phrase,
not every 403. Rate limits, missing resources and other providers retain
their existing error behavior. Historical normalization affects display
only. A dependency cancellation needs an unstarted run and a supported
queue receipt; current user gates, Retry preparation and recovery remain
authoritative. No schema migration or credential changes.

## Model Used

OpenAI GPT-6, through the Codex agent. The exact runtime model ID and
context-window size are not exposed in this session. Used reasoning,
repository tools, TypeScript, PostgreSQL, MCP fixtures and browser
verification. The parallel agent-recovery commit records Anthropic
Claude Opus 5.5 as co-author.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 20:55:34 +00:00
Nicky LeachandPaperclip 2506bcdaf4 Admit exact stopped Runner sources under the current run lease (#19134)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native execution keeps provider state and a durable controller
journal.
> - A completed process can stop before the next run attaches.
> - The current run must own recovery of that exact source.
> - An old controller must lose permission to change the consumed source
journal.
> - This pull request adds explicit source admission and protected task
dispatch.
> - The benefit is a narrow recovery boundary that preserves the
original successful result.

## Linked Issues or Issue Description

Native prerequisites #19132 and #19153 are merged into master. This PR
targets master and updates the TypeScript Runner driver contract
alongside Core and SDK admission. This change adds the SDK and Core
admission boundary. Executor selection and fresh source preparation are
a separate change. Related #17916 supplies the workspace export holds
preserved here.

## What Changed

- Plugins advertise `codexSourceRecoveryVersion: 1` for Codex App Server
and `acpxSourceRecoveryVersion: 1` for ACPX. Hosts can qualify either,
both, or neither. Core selects the matching field, because a
stopped-source check for one provider cannot establish the other
provider’s recovery guarantees. ACPX orchestration is tracked separately
in #19196. Use the existing exact source identity and `resumeFrom` under
the authenticated current run. No public recovery mode is added. Require
the claimed source, current account and prepared launch at protected
dispatch.
- Keep the first source binding pinned on the consumer lease, including
after uncertain dispatch. A newly acquired coordinator can refresh only
the same source.
- Claim one stopped successful source under the current run's acquired
native lease. Store the token-free consumer reference in existing lease
metadata. Preserve the source status and result.
- Check the exact six source identity fields, encrypted journal header,
company, task, agent, environment, driver, workspace, account selection,
and current coordinator generation. Keep workspace export and legacy
settlement holds.
- Serialize claims and checkpoint writers in the same coordinator lock
order. Reject stale source writes after consumption. Check the current
consumer before recovered source writes.
- Check source admission before and after protected worker dispatch.
Keep the request and lease owned by the current run while the native
identity remains the source.
- Allow the same owner to prepare B after a fresh source submission,
using the exact consumed-source proof instead of a generic warm-reuse
flag. Recheck that proof after dispatch.
- Preserve ordinary cached adoption and repeated preparation after
launch. The current authenticated owner can differ from the adopted PRP
identity without requesting a new source launch. Keep ordinary suspended
recovery and live task attachment checks. Extract the shared source
resolver to avoid a module cycle, and make ordinary selection honor the
same durable source consumer.

## Verification

- Current head `cac9cf188a` includes merged #19300 and the symmetric
capabilities introduced in `b7e25a64b4`. The task-owner and warm-rebind
fixtures now identify their physical task. All 29 SDK schema cases, 95
Core ownership and source-admission cases, 25 native Codex and ACPX
recovery cases, and the binary metadata check passed on this head. The
SDK and Runner builds and server typecheck passed. Both native providers
reject the other provider’s capability. GitHub CI is green and [standard
Greptile scored this commit
5/5](https://github.com/paperclipai/paperclip/pull/19134#issuecomment-6645574114)
with no open threads.

- Previous head `bda49271a4ae362a15baf3fe6d6d297e25fd2000` is directly
based on master after #19132 and #19153 landed. [GitHub workflow
42072192023](https://github.com/paperclipai/paperclip/actions/runs/42072192023)
passed on its first attempt. All check runs completed successfully or as
intentional workflow skips; Snyk passed. Standard Greptile scored this
exact head 5/5. That review had no unresolved findings.
- Runner and SDK TypeScript builds and server typecheck passed. The
focused SDK, task-owner, task-runtime, PostgreSQL source and authority
suites passed all 161 cases. The production transport-factory getter
regression and 14 native-lane selection checks also passed.
- The Linux native lane executed the new ordinary warm A→B process case.
It kept one Runner/provider process and completed two turns on the same
thread. The eager-getter mutation fails with
`native_runner_source_attachment_pending`; preserving the live getter
passes. The production factory test changes the current owner after
transport creation and checks that same options object.
- PostgreSQL cases cover both concurrent source-writer/claim orders,
exact source submission under the current caller, source-switch denial
after uncertain dispatch, same-source coordinator refresh, stale
same-boot generation, corrupted journal headers, competing consumers,
account changes and export holds.
- The protected dispatch sequence submits A, prepares B and retries B
preparation. It rejects undispatched sources, changed accounts and
foreign target bindings. Source status and accepted result remain
unchanged.
- Ordinary adopted A preparation under current B, repeated ordinary
preparation after launch and ordinary suspended continuation retain
their existing path. Absent or false claims preserve ordinary identity
denial; malformed claims fail closed.
- Earlier development attempts exposed ordinary preparation regressions
and the live-getter omission; the final regressions cover those fixes.
The first embedded PostgreSQL attempt needed missing library aliases
repaired in ignored dependencies. Those attempts are not passing
evidence.
- Historical, superseded CI heads had unchanged UI fake-timer and
Playwright health-response-lifetime failures. Their isolated cases and
scoped retries passed. The response-lifetime cause remains unproven. No
UI/test timeout was changed. The final rebased-head workflow above
passed without a retry.
- Whitespace and added-line secret/PII scans passed. Full workspace
checks ran on GitHub. No live vendor probe or deployment was performed.

## Risks

- Any driver that used the earlier Codex TypeScript option must use
`codexSourceRecoveryVersion`. The new plugin description rejects the
generic spelling. The Launcher’s existing Codex-specific HTTP header can
map to this property when the plugin adopts the new SDK. The change
requires no journal migration or ROM change.

- This is an inactive admission foundation. Executor orchestration is
separate in #19157. Native restore integrity and provider
stopped-source/account validation must be present before executor
activation. The native prerequisite uses ordinary recovery followed by
attachment. The composed executor proofs are in #19157.
- The source claim is durable. A later controller must recover the same
consumer or keep the hold. It cannot select another source after an
ambiguous launch.
- Admission adds bounded database checks around protected startup calls.
Busy issue locks fail closed; other lock waits have a one-second bound.
- No migration, snapshot format, proactive receipt, recurring worker, or
deployment is included. Full workspace checks and platform coverage run
in CI. These tests do not prove vendor history durability or a hosted
rollout.

## Model Used

OpenAI GPT-6.1 Sol (`gpt-6.1-sol`), high reasoning, through Codex with
repository tools and code execution. The runtime does not expose a
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 on the current head
- [x] Standard Greptile is 5/5 with no unresolved findings on the
current head
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-05 12:55:06 -08:00
DottaandClaude Opus 5.5 22281d4b3a feat(task-chat): approve harness-proposed plans (Codex plan mode, Claude ExitPlanMode) (#19291)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents run through the native runner. Codex and Claude Code each
have their own plan mode, and each produces a proposed-plan document.
> - Paperclip already has a planning work mode, an issue `plan`
document, and a `request_confirmation` approval gate for plans.
> - These parts were not connected. The Codex proposed-plan item was
transcript text only. The server path never put Codex into plan mode.
Claude was never put into plan mode, and its ExitPlanMode plan was
dropped. A planning run could close its task as done with no approval.
> - This pull request turns a harness's own proposed plan into the
issue's Plan document and the existing approval gate, for Codex and
Claude.
> - The benefit is that users approve or send back the agent's real plan
from the task chat. Approve resumes the agent. Request changes sends the
reason back to the agent.

## Linked Issues or Issue Description

Refs #19252

**Subsystem affected**
Native runner provider events (Codex and ACPX Claude), the runner Codex
provider (Rust), server native plan synchronization, and the task chat
UI.

**Problem or motivation**
Codex and Claude both produce a proposed-plan document in their native
plan mode. Paperclip did not use it. In live tests on the server path, a
Codex planning run was not in Codex plan mode, saved its own
`implementation-plan` document, and marked the task done without
approval. Claude planning runs were not in Claude plan mode, and the
ExitPlanMode plan text never reached Paperclip.

**Proposed solution**
Add a canonical `plan.proposed` event for a harness's proposed plan. Put
Codex and Claude into their native plan mode for planning runs. Save a
completed proposal as the issue `plan` document with the existing
`request_confirmation` gate. Agents can also propose a plan on a
standard task; the work mode stays standard.

**Alternatives considered**
Reuse `plan.updated`. Its schema is the within-turn checklist and is not
the durable Plan document, so a separate event keeps the two clear.
Stronger prompt text without native plan mode. Live tests showed that
the model kept its own document workflow.

**Roadmap alignment**
This reuses the issue plan document, planning work mode and approval
gate that already exist. It adds one canonical provider event.

## What Changed

- `packages/paperclip-runner/protocol/schemas/*`: add the
`plan.proposed` event (`paperclip.plan.proposed.v1`: `proposalId`,
`status`, `markdown` up to 60k characters, `source`). Regenerate the
schema bundle, validators and manifest.
- `packages/paperclip-runner/src/provider-events.ts`: map the Codex
`plan` item and a `<proposed_plan>` block in a completed Codex message
to `plan.proposed`. Add `claudePlanProposalEvent`.
-
`packages/paperclip-runner/runner/crates/runner-core/src/codex_provider.rs`:
send Codex's native `collaborationMode: plan` on `turn/start` for a
read-only (planning) Codex run. Before this change the server path never
sent it.
- `runner-core/src/acpx_event_payload.rs`: admit `plan.proposed` as ACPX
rich display activity.
- `drivers/acpx/codex-runtime-adapter.ts`: set Claude Code to native
plan mode for a read-only run. Capture Claude's ExitPlanMode plan from
its permission request, record it, and decline the native "Ready to
code?" prompt.
- `drivers/acpx/codex-acpx-driver.ts`, `cli/acpx-runtime-sidecar.ts`:
emit the captured proposal as a rich event. Complete that turn instead
of reporting it as cancelled.
- `backends/codex-native-backend.ts`: per-harness plan-mode
instructions, and a short opt-in hint for Claude on standard tasks.
- `server/src/services/native-runtime/native-session-executor.ts`:
synchronize a completed `plan.proposed` into the `plan` document and a
`request_confirmation` in planning and standard mode. Do not change the
issue status during the run (that revoked the run's lease). The run
finalizer turns the pending confirmation into the review wait.
- `server/src/services/heartbeat-run-summary.ts`: keep a
`<proposed_plan>` block out of the posted reply.
- UI: new `plan_proposal` provider family. The task chat shows a live
"Writing plan" card while a proposal streams and a one-line record when
it completes. Resolved plan reviews keep a receipt ("Approved Plan v1",
"Requested changes to Plan v1: reason"). Plan previews skip section
headings.
- Storybook: `Tasks and conversations/.../Task chat/Proposed plan
approval` shows the journey.
- `doc/SPEC-implementation.md`: describe harness-proposed plan approval.

## Verification

- `cargo test -p paperclip-runner-core --lib` (new:
`read_only_codex_turns_use_native_plan_collaboration_mode`)
- `cd packages/paperclip-runner && npx vitest run
src/provider-events.test.ts
src/drivers/acpx/codex-runtime-adapter.test.ts`
- `cd server && npx vitest run
src/__tests__/native-plan-proposal-sync.test.ts
src/__tests__/heartbeat-run-summary.test.ts`
- `cd ui && npx vitest run
src/components/transcript/native-run-events.test.ts
src/components/task-chat`
- Live test on a local worktree instance with real harnesses:
- Codex (`gpt-5.6-sol`), planning task: Codex asked one question, then
produced a native plan item. The task showed Plan v1 with Approve plan /
Request changes and moved to in review. Approve changed the work mode to
standard. Codex then implemented the plan and its tests passed.
- Codex, Request changes with a reason: Codex woke in planning mode and
produced Plan v2 that includes the change.
- Claude (`claude-opus-5-5`), planning task: Claude was in plan mode.
ExitPlanMode became Plan v1 and the turn completed. Approve resumed
Claude in standard mode, and Claude implemented the plan.
- Claude, standard task that asks for a plan first: Claude stopped
before code changes and asked for plan approval. The work mode stayed
standard.
- Storybook: open `Proposed plan approval` and check each stage.

## Risks

- Codex planning runs now really use Codex plan mode on the server path.
Codex can ask a clarifying question before it proposes. This matches
Codex plan mode.
- Claude planning runs now use Claude plan mode. A declined ExitPlanMode
ends the Claude turn; the runner reports it as completed. Claude's tool
row for that request records a rejection from Claude's side.
- Codex does not stream the proposed plan on the server path yet, so the
live card shows a placeholder until the plan completes.
- A `<proposed_plan>` block in any completed Codex message on a standard
task now asks for approval. This is the documented opt-in path.

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

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), via Claude Code with
tool use (shell, file edits, browser). Long context. No extended
thinking mode 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
- [ ] 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 20:53:33 +00:00
DottaandClaude Opus 5.5 ffd9591ab4 fix(recovery): preserve cancelled files after a reusable sandbox stops (#19305)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A native run writes its task files in a remote sandbox. The task
workspace records that run as its writer until the files are saved.
> - When a user stops or reassigns a run, the cancelled-workspace
preservation worker records the unsaved files as file recovery work.
> - A warm run (`keep_running`) that is not successful releases its
reusable sandbox through stop-and-retain. That release can finish before
the worker sees a terminal coordinator.
> - The worker accepted only an active lease or a released ephemeral
lease. It ignored the released reusable lease, so it never recorded the
file recovery.
> - The workspace kept the ended writer's hold. Each successor run
waited, then failed with "The workspace still has an unfinished writer
or save operation" after the one-hour cutoff.
> - This pull request accepts a released reusable lease that has a
confirmed stop-and-retain receipt as a preservation source.
> - The benefit is that the stopped run's files become recovery work,
and the task is not stranded behind a hold that nothing settles.

## Linked Issues or Issue Description

Refs #18820 (preserves files when Stop finds no live controller). Refs
#17677 (schedules cancelled workspace preservation). Refs #19285 (parks
a workspace wait behind an ended writer).

**What happened?**

A board user reassigned a task during a warm native run. Stop cancelled
the run with `issue_reassigned`, and the Stop acknowledgement was
recorded. The executor settled the coordinator to `terminal_failure`
with no owner. About eight seconds later, terminal release stopped and
retained the reusable Daytona sandbox. The lease became `released` with
`leasePolicy: reuse_by_environment`, a `stopped` termination receipt and
a stop-and-retain receipt. The task workspace stayed `dirty` with
`writerRunId` set to the cancelled run. No file recovery was recorded.
The new assignee's runs waited as `workspace_busy` for one hour. Then
each run failed `setup_failed`, and the task became blocked.

The heartbeat run row showed `nativePhase: "observed"`. That value is
stale: the checkpoint persist wrote it during teardown. The coordinator
itself was `terminal_failure`: the run's execution projection was
`recovery_needed` with no matching recovery action.

**Expected behavior**

The preservation worker records the stopped run's prepared files as file
recovery work. The lease keeps its release status and receipts. No
second stop and no new provider turn start.

**Steps to reproduce**

1. Cancel a native run with an acknowledged run-only Stop. Its remote
workspace reference is `prepared`, and the coordinator is
`terminal_failure` with no owner.
2. Release its reusable lease through stop-and-retain (`released`,
`reuse_by_environment`, confirmed stop receipt).
3. Run the cancelled-workspace preservation worker or the startup scan.
4. On master, the run has no `workspaceRestoreRecovery`, and no recovery
action exists.

## What Changed

-
`server/src/services/native-runtime/native-failed-workspace-recovery.ts`:
add one source check, `cancelledWorkspaceSource`, for cancelled
preservation. The worker check and the startup scan both use it. It
keeps the active and ephemeral cases. It adds a released reusable lease
only when the stop-and-retain receipt is confirmed
(`hasConfirmedSandboxStopAndRetain`) and `preparedRemoteWorkspaceSource`
proves the exact remote-workspace binding. This is the same proof the
restore recorder uses. Native-sync runs have no such proof and stay
ineligible. An ineligible source causes no run write or notification.
- The restore recorder (`preserveLegacyWorkspaceRestoreSources`) already
accepts this source. It checks the confirmed receipt and the exact
prepared workspace binding, and it keeps the lease status, policy and
receipts. This pull request does not change it.
- `native-cancelled-workspace-preservation.test.ts`: add the incident
shape as a regression for the worker and the startup scan, and add
safety cases.
- `doc/execution-semantics.md`: document the stopped reusable source and
the writer hold until recovery.

## Verification

From `server`:

```sh
npx vitest run src/services/native-runtime/native-cancelled-workspace-preservation.test.ts
```

- Red before the fix: the two new regression cases failed (`expected {
nativeCancellation: … } to match object { …, workspaceRestoreRecovery: …
}`). 30 other cases passed.
- Green after the fix: 36 passed.
- Review follow-up: the cases `invalid-receipt`, `expired-lease` and
`native-sync` failed on the previous head 1d461b4605. The scan or the
worker rewrote the run row. These cases pass now. Each case runs the
worker and two scans, then asserts that the run row and the lease row
are unchanged.
- Recovery-time fencing: after preservation,
`inspectWorkspaceRecoverySource` rejects with `workspace_writer_active`
while a task run is running. It rejects with
`allocation_ownership_ambiguous` when a later lease shares the
allocation.
- The regression checks: recovery recorded with the lease ID, run still
`cancelled` with `issue_reassigned`, lease row unchanged (status, policy
and receipts), one recovery action, recovery source inspection finds one
source, the worker disarms, and a repeated pass records nothing again
and creates no run or wake.
- Safety cases, all with no recovery record and unchanged run and lease
rows: a live coordinator owner, a `pending` (not acknowledged) Stop, a
lease without a stop receipt, an invalid receipt, an expired lease, a
native-sync run, and a `running` run.
- Affected suites were run locally: `native-failed-workspace-recovery`,
`native-workspace-reconstruction`, `workspace-recovery`,
`remote-workspaces.integration`, `native-environment-settlement-work`,
`server-startup-feedback-export`, `heartbeat/run-control`,
`heartbeat-remote-workspace-busy`. On a heavily loaded machine, some
cases timed out at 30 s. A rerun of those two files passed 135/135
tests; one file still reported a 30 s hook timeout. CI runs these
suites.
- `pnpm --filter @paperclipai/server typecheck` passes.
- Not done: a live staging run. The fix is not deployed.

## Risks

- Low. The new source applies only to a cancelled native run that has an
acknowledged run-only Stop, a terminal coordinator with no owner and no
accepted result, a prepared workspace reference, and a confirmed
stop-and-retain receipt for the same run, lease and allocation.
- Preservation records recovery work only. It does not stop the sandbox
again, change the lease policy, or grant execute rights. Recovery later
checks live owners and retired predecessors before it uses the sandbox.
- The writer hold on the workspace does not change. Successor runs still
wait until recovery saves the files, so unsaved files are not
overwritten. The one-hour cutoff stays.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code with tool use (shell, file edits, read-only browser reads of
staging diagnostics).

## 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-05 14:26:53 -06:00
DottaandClaude Opus 5.5 dd889e6a0b feat: show the assignee's provider usage limit in task properties (#19270)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents run on the native Runner through the Codex app-server and the
Claude ACP harness
> - Both harnesses report the provider account's rate-limit windows:
Codex sends `account/rateLimits/updated`, and Claude sends its SDK
`rate_limit_event` as `usage_update._meta["_claude/rateLimit"]`
> - Paperclip dropped both signals, so an operator could not see that an
agent was near or past its provider limit
> - This pull request normalizes both signals into one account-scoped
snapshot, keeps the latest snapshot per agent, and shows it as one
"Usage limit" line in the task properties pane
> - The benefit is that an operator sees "5h 78% · resets 15:40" (amber
above 80%, "Limited until …" when the provider refuses work, "using
credits" when a used-up window falls back to credits) next to the
assignee, and can click it for every window

## Linked Issues or Issue Description

Refs #19246
Refs #19259

**Is your feature request related to a problem?**
Codex and Claude report provider rate-limit windows during a run.
Paperclip dropped them: the Codex notification fell through the Rust
normalizer with no branch, and ACPX dropped Claude's `_meta`. Operators
could not see how close an agent was to its provider limit.

**Describe the solution you'd like**
Store the latest reported windows per agent and show one compact line in
the task properties pane, with a popover that lists every window.

**Describe alternatives you've considered**
A full gauge in the agent list and the run header. A design review chose
one property line with a details popover instead.

## What Changed

- Runner (Rust, the runnerd path): new `provider_rate_limits.rs`. Codex
`account/rateLimits/updated` becomes a `provider.notice.recorded` event
(category `provider.rate_limits`, scope `account`). `CodexProviderState`
merges sparse updates and emits a notice only when the windows change.
ACPX `usage_update` with a sanitized `rateLimit` field also emits the
notice.
- Runner (sidecar): `acpx-runtime-sidecar.ts` forwards only `status`,
`rateLimitType`, `utilization` and `resetsAt` from
`_meta["_claude/rateLimit"]`. This needs the ACPX `_meta` passthrough
from #19259. Until that merges, Claude emits nothing.
- Runner (TypeScript in-process drivers): the same mapping in
`codex-session-notifications.ts` and `codex-acpx-driver.ts` through
`drivers/provider-rate-limits.ts`.
- Shared: `provider-rate-limits.ts` holds the snapshot type, the parser,
the merge, and `summarizeProviderRateLimits` (fullest current window,
expiry by reset time, limited versus using credits).
- Server: `agent-provider-rate-limits.ts` merges each notice into
`agent_runtime_state.state_json.providerRateLimits`, scoped by company
and agent. A session reset keeps the snapshot. No migration.
- UI: `AgentRateLimitPropertyValue` adds a "Usage limit" row under the
assignee in `IssueProperties`. Transcripts treat the notice as run-log
only. A run status event refreshes the assignee's runtime state.
- Storybook: `Tasks and conversations/Task creation and lifecycle/Usage
limit property`.
- Docs: `doc/run-log-events.md` describes the notice and its storage.

## Verification

- `cd packages/paperclip-runner/runner && cargo test -p
paperclip-runner-core --lib rate_limit` and `--test
acpx_provider_events`: sparse Codex merge, dedupe, reached window,
checkpoint round trip, Claude utilization and rejection, notice shape.
- `cd packages/paperclip-runner && npx vitest run
src/drivers/provider-rate-limits.test.ts src/drivers/codex`
- `cd packages/shared && npx vitest run
src/provider-rate-limits.test.ts`: summary, expiry, limited, using
credits, parse and merge.
- `cd server && npx vitest run
src/__tests__/agent-provider-rate-limits.test.ts`: create and merge,
out-of-order snapshot, company isolation, unrelated notices, session
reset keeps the snapshot.
- `cd ui && npx vitest run src/components/transcript
src/components/issue-properties src/context/LiveUpdatesProvider`
- `pnpm -r typecheck` and `pnpm check:token-gates` pass.
- Live test on a local worktree instance: a native Codex agent on a
ChatGPT subscription login ran a real task. The runner recorded one
`provider.rate_limits` notice. The server stored it. The task properties
pane showed "7d 100% · using credits" (this account's weekly window was
used up and had credits, and the run succeeded). The transcript showed
no extra row.
- Not verified live: Claude. The local Claude CLI was not signed in, and
the Claude path also needs #19259. The Rust and sidecar paths have
tests.
- Not run locally: the full `pnpm test:run` and `pnpm build`. CI runs
them.

## Risks

- Low risk. The snapshot is display data. It does not hold or start
work. The existing provider-limit wait (#19246) still reads typed run
failures.
- Codex can send a rate-limit update often. The runner emits a notice
only when the windows change, so run-log growth stays small.
- With API-key auth, the harnesses report no windows, so no row appears.
This is the intended result.
- `CodexProviderState` gains two optional serde fields with defaults, so
older checkpoints still load.

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

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, through Claude Code
with tool use (shell, file edits, browser) 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: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 20:26:51 +00:00
Nicky LeachandPaperclip 31bf6e4482 Pin plugin Runner cleanup to its physical task (#19300)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Plugin Runner owners manage a physical task through an environment
lease.
> - The existing replacement path can assign a new physical task to that
same lease.
> - A delayed old owner can then stop the replacement or mark it
stopped.
> - Cleanup must keep the original physical task identity across RPC and
database writes.
> - This pull request pins that identity and keeps cleanup available to
the new owner.

## Linked Issues or Issue Description

Refs #19134 and #19157. This fix is based on master and does not enable
repeated stopped-source recovery. A search found no duplicate fix.

**What happened?**

The maintained `createOwnedSandbox` replacement path changes
`providerLeaseId` on one lease. An old task owner uses that mutable
lease for a later Stop. A stopped-state response already in flight can
also update the new task's flags.

**Expected behavior**

The old owner must address its original task. Its late cleanup writes
must not alter the replacement. The replacement's own owner must still
stop and complete it.

**Steps to reproduce**

1. Create a plugin task owner for a stopped task.
2. Replace that task through the existing describe/startup path on the
same lease.
3. Resume an old checkpoint or a held old status response.
4. Observe a Stop for the replacement or an incorrect stopped flag
before this fix.

## What Changed

- Require each task owner to keep the physical ID returned by its
description. Reject a rebind to a different task.
- Carry the ID through the existing internal task context after startup
ends. Reject a changed physical task before RPC dispatch.
- Match the physical task in stopped-state, completion and
credential-handoff writes.
- Hold the exact lease row through the retained-session database update
and local checkpoint save.
- Cover the maintained replacement path with real PostgreSQL tests.
Preserve positive cleanup for the replacement owner. Document this
boundary.

## Verification

- Both replacement regressions failed on unmodified master `10a7d039c8`:
delayed old-owner Stop targeted the replacement, and a held old status
response marked it stopped.
- Six focused files passed 160 tests: task authority, task runtime, warm
tasks, execution target, credential writeback and task owner.
- Four selected executor factory cases passed. They cover task-only
providers and the physical task binding after the startup context ends.
- Runner TypeScript, plugin SDK and adapter-utils builds passed. Server
TypeScript check passed. Diff and added-line secret scans passed.
- All 15 setup-probe cases passed with the actual Runner and fake
provider binaries. The first local attempt lacked cargo on PATH. A
sandboxed native attempt timed out in seven process cases; the unchanged
run with loopback permission passed all 15. A separate initial DB
integration attempt was denied a loopback listener; its
permission-correct run passed.
- Review found three missed constructors in the existing SDK/Core
lifecycle fixture. They now use the actual describe result. All four
cases in that real PostgreSQL and worker RPC file passed.
- The first GitHub attempt failed in the unchanged GitHub recovery
pagination case in `chat-channels.integration.test.ts`: its zero-request
assertion observed a configuration GET. That exact case passed locally
without code or assertion changes. The one failed-job-only retry passed;
the initial failure log is retained.
- [GitHub workflow
42082587576](https://github.com/paperclipai/paperclip/actions/runs/42082587576)
passed on its second attempt. All CI checks completed, and Snyk passed.
Standard Greptile scored exact head `fb914d98b99e` 5/5 with no
unresolved threads. The branch is mergeable and ready for review.

## Risks

The task context change is internal. It adds no plugin wire field,
schema or Runner mode. A stale owner now gets an error and cannot clean
up a replacement. A new owner can still clean up that task. The retained
checkpoint write holds one existing lease row during a bounded local
file write.

Repeated stopped-source incarnation admission, runtime pins and
deployments are outside this change.

## Model Used

OpenAI GPT-6.1 Sol (`gpt-6.1-sol`), high reasoning. Used repository
tools, TypeScript and real PostgreSQL tests.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-05 12:14:09 -08:00
Devin FoleyandClaude Opus 5.5 8aa28940bf fix(sentry): keep a live-heartbeat native capacity retry out of Sentry (#19320)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The server sends unexpected run failures to Sentry, and it keeps
known, recovered failures local
> - A native Codex "model at capacity" failure gets an automatic retry.
That failure must stay out of Sentry while its retry owns recovery
(#18295, #19051)
> - The check reads the `nativeProviderFailure` diagnostic from the
failed run's `resultJson`. On the live heartbeat path, the failed-status
write replaces `resultJson` with the adapter's copy, and that copy has
no diagnostic
> - So every live-path capacity failure with a scheduled retry still
goes to Sentry. Only the reconciliation path, which the tests cover,
kept the failure local
> - This pull request removes the diagnostic check. The scheduled retry
run is the proof
> - The benefit is that recovered capacity failures no longer create
Sentry noise on the normal live path

## Linked Issues or Issue Description

Refs #18295
Refs #19051

**What happened?**
A native Codex run failed with `native_provider_overloaded`. The
committed status decision scheduled the automatic capacity retry, and
the retry run existed before the report. The run failure still went to
Sentry. In a staging sample, every live-path capacity failure since
#19051 was reported (1 of 1).

**Expected behavior**
When the `native_provider_overloaded` retry run exists for the same
task, the failed run does not go to Sentry. The retry reports its own
outcome.

**Steps to reproduce**
1. Run a native Codex agent through the live heartbeat path
(`finalizeNativeRun` without `projectRunStatus`).
2. Let the provider return `serverOverloaded`. The status decision
schedules the capacity retry.
3. The heartbeat writes the terminal `failed` status with the adapter's
`resultJson`, and `emitTerminalAgentTaskRun` calls `reportRunFailure`
with that row.
4. The row has no `nativeProviderFailure`, so
`isScheduledNativeCapacityRetry` returns false and the event goes to
Sentry.

**Relevant logs or output**
The failed run row later shows `nativeProviderFailure:
codex/server_overloaded`, because a later reconciliation write restores
it. The row that the heartbeat write reports does not have the field.
The same overwrite also explains the missing `finalizationReasonCode`
that #19051 fixed.

## What Changed

- `server/src/services/run-failure-report.ts`:
`isScheduledNativeCapacityRetry` no longer requires
`resultJson.nativeProviderFailure`. The run must still be native and
failed, carry `native_provider_overloaded`, have a native task, have no
signal, and not be in setup. The same-task successor with
`scheduledRetryReason: native_provider_overloaded` is still required.
- The successor is enough proof. Only the committed capacity status
decision creates that successor, and only `forTerminal` in
`native-provider-failure.ts` produces `native_provider_overloaded`
together with the typed `codex`/`server_overloaded` diagnostic.
- `run-failure-report.test.ts`: a new case keeps the failure local when
the row has only the adapter's `resultJson` (no diagnostic, no
finalization fields) and the successor exists. The "diagnostic is
missing" case now reports only when no retry run exists. The case for a
non-capacity diagnostic with a capacity successor is removed, because
that state cannot occur.
- `doc/observability.md`: the native capacity paragraph now says that
the successor is the proof and that the run's own `resultJson` is not
required.

## Verification

- `pnpm --filter @paperclipai/server exec vitest run
src/services/__tests__/run-failure-report.test.ts`: 284/284 pass with
this change.
- With the `master` version of `run-failure-report.ts`, the new case
fails: 1 failed, 283 passed. So the test catches the bug.
- `native-provider-failure.test.ts`: 57/57 pass.
- `native-provider-overload.integration.test.ts` did not load in my
sandbox, because `@paperclipai/plugin-sdk` was not built. CI must run
it.

## Risks

- Low risk. The change only removes one condition from a Sentry filter.
- These failures still report: no retry run, exhausted or held retries,
other retry lanes, a successor for a different task, legacy runs,
signals, timeouts, setup failures, and unreadable retry evidence.
- The change does not affect run status, retries, recovery, or the `all`
reporting mode.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), agentic coding with tool use
(shell, repository reads, test runs).

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 12:04:33 -08:00
DottaandPaperclip 62a5aa7705 fix(runner): forward configured bindings into OpenCode (#19319)
Forward controller-selected task bindings into OpenCode shell commands. Keep OpenCode runtime configuration isolated. Redact raw and JSON-escaped binding values while preserving the non-secret environment manifest.

Fixes #19257

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-11-05 14:01:12 -06:00
Devin FoleyandPaperclip 8464a8c9ae fix(native): settle a bootstrap retry that lost its task lock (#19307)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A native run that fails before any provider work gets a bounded
same-run bootstrap retry. Restart recovery decides whether that retry
can run.
> - The retry needs the task's issue lock. A user comment or another
wake can move that lock to a new run before the retry is due.
> - Restart recovery then returned `issue_execution_lock_changed`
without a write. The same check ran on every recovery pass and never
ended. The coordinator stayed `retryable_failure`.
> - The orphaned-lease sweep waits for a terminal coordinator before it
preserves the workspace. So the sandbox lease stayed active, and every
later run for the task was cancelled as `workspace_busy`.
> - This pull request gives a lock-lost bootstrap retry the same bounded
admission that a superseded remote retry already has.
> - The benefit is that the retry settles, the board gets a
reconciliation action, and the sandbox lease is released instead of
blocking the task.

## Linked Issues or Issue Description

No public issue exists. Bug description:

**What happened?**

On a hosted instance, a native agent run failed at startup because the
provider rejected `session.open` with a rate limit. Paperclip scheduled
a same-run bootstrap retry. Seconds later, a new run for the task took
its issue lock, most likely woken by a user comment. From then on, every
restart recovery pass for the old run returned `blocked` /
`issue_execution_lock_changed` and wrote nothing. The run recorded about
1,000 recovery events over 12 hours. Its coordinator stayed
`retryable_failure` with `recoveryMode: bootstrap_retry`. The scoped
orphaned-lease sweep found a pending workspace export for the lease. It
could not preserve the workspace, because
`preserveNativeFailedWorkspace` requires `terminal_failure`. So the
lease stayed `active`, and 16 later runs for the task were cancelled as
`workspace_busy`. Three more runs on the same instance were stuck in the
same state. A board Stop on the failed run moved the coordinator to
`terminal_failure`, and the sweep then released the lease within about a
minute.

**Expected behavior**

A bootstrap retry that cannot get its task lock back settles within a
bounded number of recovery passes. The coordinator reaches
`terminal_failure`, the board gets a reconciliation action, and the
existing orphaned-lease sweep can preserve the workspace and release the
sandbox lease.

**Steps to reproduce**

1. Fail a native remote run before provider work, so its coordinator is
`retryable_failure` with `recoveryMode: bootstrap_retry`.
2. Move the task's `executionRunId` to a newer run for the same task, or
clear it.
3. Run `claimNativeRestartRecoveries` for the failed run.
4. Before this change, it returns `blocked` with no `retryAt`, and the
coordinator stays `retryable_failure` on every pass. The new tests in
`native-remote-retry-admission.test.ts` reproduce this.

**Paperclip version or commit**

Observed on a hosted deployment. Reproduced in tests on `master` at
`10a7d039c8e`.

Related: #19190 (a `session.open` model rejection no longer schedules a
doomed bootstrap retry), #19131 (preserve superseded observed
workspaces) and #19281 (release the leases of a cancelled legacy run).
None of them changes this admission path.

## What Changed

- **Restart recovery
(`server/src/services/native-runtime/native-restart-recovery.ts`)**: a
new condition, `lockLostBootstrapRetry`, matches a failed run whose
coordinator is `retryable_failure` with `recoveryMode: bootstrap_retry`
and whose issue lock is not on the run.
- A newer run for the task: the existing superseded check now covers
this case too. It defers with `native_retry_source_superseded` as
permanent, so admission is exhausted at once.
- No newer run (the lock was released): recovery defers with
`issue_execution_lock_changed` through the existing `deferRemoteRetry`.
That path persists a deadline, backs off and exhausts after the system
recovery budget.
- When admission is exhausted, the existing hold path writes
`terminal_failure` / `native_restart_recovery_blocked`, keeps the
provider evidence and creates the board reconciliation action. This PR
adds no new terminal path.
- A bootstrap retry that still holds its own lock is unchanged. The
exact-checkpoint remote retry path is unchanged.
- **Docs (`doc/execution-semantics.md`)**: the bootstrap retry section
now states the lock-lost rule.

## Verification

- New test `settles a %s bootstrap retry that lost its task lock instead
of re-checking forever` in
`server/src/services/native-runtime/native-remote-retry-admission.test.ts`,
for the `superseded` and `released` cases. The fixture is a genuine
pre-provider failure: a `session.open` rejection with no session
checkpoint, no runner checkpoint row and no provider evidence.
- On `master` without the fix, both cases fail: recovery returns
`blocked` without a deadline, and the coordinator stays
`retryable_failure`.
- With the fix, `superseded` settles on the first pass, and `released`
settles after `SYSTEM_RECOVERY_MAX_RETRIES` deferred passes. Each
deferred pass persists its `nextAttemptAt`. Both end in
`terminal_failure` with one board recovery action, and later passes open
no transaction.
- `npx vitest run
src/services/native-runtime/native-remote-retry-admission.test.ts` (from
`server/`): 92 passed.
- Related suites, each run on its own with exit code 0:
`native-ingress-bootstrap-retry`, `native-startup-retry`,
`native-scheduled-resume-lock`, `native-restart-recovery-isolation`,
`native-runner-restart-recovery`, `native-runner-restart-preparation`,
`native-runner-restart-diskless`, `native-warm-transition-restart`,
`native-controller-recovery`, `native-failed-workspace-recovery`,
`native-session-resumption`, `native-startup-cleanup-recovery`,
`heartbeat-observed-workspace-preservation`, `heartbeat/recovery`,
`runtime-selection`, `remote-workspace-migration` and
`native-session-executor` (1,149 tests). Two other suites each had one
local failure that this change does not cause:
- `heartbeat-process-recovery`: "redacts opaque environment-bound
credentials from Sentry diagnostics" fails the same way on this machine
without the change.
- `chat-channels.integration`: "settles a shared-endpoint
accepted-response backlog without retrying its own locks" failed once
under full-suite load. It passes alone, with and without the change.
- `pnpm run typecheck` in `server/`: passed.

## Risks

- Low to medium. The change only affects failed bootstrap retries whose
lock is not on the run. Before this change, such a retry could never
run, so no working path is removed.
- A released lock that comes back to the same run during the backoff
still resumes normally, because the condition reads the current lock on
each pass.
- When the retry settles, the task gets a board reconciliation action.
Before this change, the task was silently stuck behind `workspace_busy`.
- No migration and no API change.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with tool use
(shell, code search, test runs), extended thinking, and a read-only
investigation subagent for the root cause.

## 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-05 11:45:54 -08:00
Devin FoleyandPaperclip 74b11014c6 fix(server): stop a queued answer from blocking saved-work recovery (#19318)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A native run can accept its result before the workspace copy-back
finishes. If a server restart interrupts the copy-back, the run keeps an
owner receipt for the server that stopped.
> - Another server takes over only after the cloud platform proves that
the old deployment stopped. After that proof, recovery checks that the
task has not moved on, so old files never overwrite newer work.
> - That check counted any queued run on the task as newer work.
> - The user's answer to the run's question queues the assignee's next
wake. That wake cannot claim while the saved run holds the task's
execution lock, so it waits for the copy-back.
> - Recovery waited for the wake and the wake waited for recovery. The
task stayed stuck, and nothing was logged.
> - This pull request ignores a later wake for the same assignee that
has never started, and logs waits that the worker stops retrying.
> - The benefit is that answering a question during an interrupted
copy-back no longer strands the task. Every other newer-work check still
applies.

## Linked Issues or Issue Description

Refs #15824 (added platform-verified takeover and these binding checks)
and #19111 (worker concurrency and ordering for the same copy-backs).
This pull request keeps the rest of #15824's fence unchanged.

**What happened?**

On a hosted instance, a server restart interrupted a copy-back after the
worker had written its owner receipt. The run had asked the user a
question, and the user answered. The answer's wake was queued. Every
later server got a valid stop proof for the old deployment and then got
`binding_changed` from the shared binding check: the only failing
condition was the queued answer wake. The worker treats
`binding_changed` as a final wait and returns without logging. The run
stayed `running` and the wake stayed `queued` for about 19 hours, across
many restarts.

**Expected behavior**

A wake that is waiting for the saved run does not stop the saved run's
recovery. Runs that could change the workspace still prevent takeover,
and the publication check still stops a copy-back if such a run appears.

**Steps to reproduce**

1. Accept a native result and leave a copy-back owner receipt from a
stopped deployment.
2. Queue the assignee's next wake on the same task, for example by
answering the run's question.
3. Let the reconciler or the workspace worker revisit the saved result
with a verified stop proof. On `master` the takeover returns
`binding_changed` and nothing is copied or committed.

The new database tests run these steps against PostgreSQL.

**Paperclip version or commit**

`master` at `03eb49053fe`.

**Deployment mode**

Cloud-managed instances with platform stop verification. The same
binding check also guards operator owner recovery, so self-hosted
operator recovery had the same deadlock.

## What Changed

-
`server/src/services/native-runtime/native-workspace-recovery-binding.ts`:
the newer-run check ignores a run when all of these hold:
  - It is `queued` and has never started (`startedAt` is null).
- It belongs to the run's agent, and that agent is the task's assignee.
  - It was created after the saved run.
  - Its wake reason is not `source_scoped_recovery_action`.

The run claim blocks such a run while the saved run holds the issue's
execution lock (`lockIssueExecutionClaim` in `heartbeat/queue.ts`). Runs
that have started, other agents' runs, recovery-action wakes and older
queued runs still prevent takeover.
-
`server/src/services/native-runtime/native-workspace-finalization-work.ts`:
log `runId` and `reason` when the worker stops retrying a wait
(`binding_changed`, `owner_unverified`). It does not log `owner_active`.
- `doc/native-workspace-finalization-recovery.md`: describe the
waiting-wake rule in the shared binding section.
-
`server/src/services/native-runtime/native-workspace-queued-wake-recovery.test.ts`
(new): the queued-wake tests, with a small fixture of their own. They
are in a separate file so `native-workspace-finalizer.test.ts` stays
under 1,000 lines.

## Verification

- Failing first. These tests in
`native-workspace-queued-wake-recovery.test.ts` fail against the
original check and pass with this change:
  - Operator owner recovery with an answer wake queued.
  - Automatic takeover with the wake still queued.
- Automatic takeover where the wake starts during copy-back. On `master`
takeover is refused before this point; with the change, publication is
stopped.
- The real reconciler, from `retryable_failure`, with a
stopped-deployment owner and a queued answer wake. It now copies once,
commits the saved result and leaves the wake queued with its claim no
longer blocked.
- The fence is kept: three new cases (a requeued wake that already
started, a recovery-action wake, another agent's queued run) still get
`binding_changed`. They pass both before and after the change.
- Changed test: "does not reopen an old result after a newer task
execution exists" now inserts a started run instead of a never-started
queued run of the same assignee. The queued case is now the new
operator-recovery test above.
- `cd server && npx vitest run
src/services/native-runtime/native-workspace-queued-wake-recovery.test.ts
src/services/native-runtime/native-workspace-finalizer.test.ts`: 2
files, 72 passed. With the original binding file restored, the 4
positive queued-wake tests failed and the 3 fenced cases passed.
- Related suites (finalization work, platform proof, export retry,
finalizer failure, recovery actions, owner-recovery routes, status
reassessment, scoped deadline work): 8 files, 499 passed.
- After rebasing onto `03eb49053fe`: finalizer and finalization-work
suites, 97 passed.
- `cd server && npx tsc --noEmit -p tsconfig.json`: pass.
- Not verified on a live instance yet. The diagnosis comes from that
instance's run, finalization, workspace-operation and recovery-action
rows, plus a direct stop-proof request that returned a valid receipt.

## Risks

- A copy-back can now finish while the assignee's wake is queued. The
same-process retry path already does this, because it does not run the
binding check. The takeover path now matches it for wakes that cannot
start yet.
- If some path starts such a wake despite the execution lock,
`assertHeld` sees a running newer run and stops before publication. The
window between that check and the write is the same as for the other
binding conditions.
- No schema migration.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`) through Claude Code, with tool use
(shell, file edits, test runs, read-only database and log queries). The
runtime does not expose the context window or reasoning configuration.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-05 11:45:16 -08:00
DottaandPaperclip 55389554c2 fix(runner): paginate workspace discovery before journal persistence (#19311)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native agents discover reusable workspaces through
`list_workspaces`.
> - The tool returned every authorized workspace in one result.
> - A large inventory exceeded the durable command journal limit. The
result stayed pending, and session cleanup blocked the next turn.
> - This pull request limits discovery in SQL and returns explicit
continuation metadata.
> - Agents can inspect every authorized workspace without sending the
full inventory in one command.

## Linked Issues or Issue Description

Related: #15749 introduced the task workspace tools. No duplicate
pagination fix was found.

**What happened?**

A native workspace discovery call returned a multi-megabyte inventory.
Persisting its result failed with `command_payload_too_large`. The
preceding answer could still be saved, but cleanup retained the
unsettled tool call. A later turn failed before provider work with
`native_session_cleanup_quarantined`.

**Expected behavior**

Workspace discovery must return a manageable page. Agents must be able
to request the remaining authorized rows. Existing visibility checks and
journal integrity checks must stay effective.

**Steps to reproduce**

1. Create 2,000 task-selectable workspaces with ordinary repository
paths.
2. Call `list_workspaces` from an admitted native run.
3. On the old code, the real API regression returns 869,781 bytes. The
command journal allows about 510 KiB.
4. On the repaired code, follow `nextCursor` to inspect all rows. Pages
contain at most 100 rows and fit within 128 KiB.

**Paperclip version or commit**

Reproduced on source commit `7351a9d74f` before the repair.

**Deployment mode**

Isolated source checkout with real PostgreSQL and authenticated native
agent APIs. Hosted deployment and incident recovery remain separate.

## What Changed

- Validate workspace list limits and UUID cursors with a JSON Schema
pattern supported by strict validators. Return `workspaces`, `limit`,
`returnedCount`, `hasMore`, and `nextCursor`.
- Apply company, visibility, lifecycle, and cursor conditions before the
SQL limit. Order pages by workspace ID. Fetch one extra row to detect
continuation without a count query.
- Fit encoded pages within 128 KiB, including JSON escaping and Unicode.
If one summary cannot fit, return explicit string previews with
`truncatedFields`. The authorized detail API retains its full record.
- Keep existing unpaged summary callers on their array response and
recent-use order.
- Update the native tool descriptor, generated contracts, catalog
fixture, and execution documentation.
- Add regression coverage for large inventories, complete pagination,
stable pages, private and foreign rows, invalid inputs, and legacy
summary behavior. Add an opt-in two-turn live Codex acceptance check.

## Verification

- Red before repair: `task-workspace-api.test.ts -t "pages a large
authorized workspace inventory"` failed because the result was 869,781
bytes instead of below 128 KiB. The transport regression also failed at
2,284,671 bytes.
- Green after repair and review changes: 120 tests passed across the
workspace API, transport, routes, native authority, coordinator,
historical-question, and Codex vertical-slice suites. They cover
complete discovery, byte-sized pages, oversized individual summaries,
full detail retrieval, private/foreign/lifecycle exclusions, and strict
schema compatibility.
- Journal safety: two focused durable control-plane tests passed.
Oversized result rejection and lossless valid result delivery remain
effective.
- Tool catalog and reconciliation: 30 tests passed.
- `pnpm -r typecheck`: passed.
- `pnpm build`: passed again on final commit `bc10880da6`. Server
typecheck also passed after the review changes. The original affected
suites passed on the rebased first commit (31 tests), and the expanded
review regression set passed afterward (120 tests).
- Real provider acceptance passed again on final commit `bc10880da6`:
local macOS arm64, rebuilt native Runner, `gpt-6.1-sol`, high effort,
per-turn lifecycle, and 2,000 persisted workspaces. Both turns
succeeded. Persisted call inputs and results prove that the second page
uses the first result’s cursor, both calls succeed, their IDs do not
overlap, and the encoded results stay within 128 KiB. The second turn
kept the provider conversation and file. Both task locks were clear
after completion, and no recovered execution failure was recorded.
Duration: 166.92 seconds.
- Full local `pnpm test:run` stopped in the chat shard: 330 tests passed
and one Slack registration test failed with `socket hang up`. That exact
test passed in isolation without changes, and the final-head CI chat
shards passed. A broader local managed-Slack follow-up produced no
result after about eight minutes and was interrupted; it is not counted
as a pass. The original full-suite failure remains recorded.
- The first CI head failed native tool-schema compilation because strict
validators did not recognize `format: uuid`. The revised descriptor uses
a UUID pattern. The previously failing coordinator, native
vertical-slice, authority, and history suites now pass locally. The
stronger live cursor-receipt acceptance check passed. Final-head CI
completed on `bc10880da6`: 57 checks passed and five optional checks
were skipped, with no failures or pending checks. Greptile reviewed that
commit at 5/5; both prior findings are resolved.
- Dependency cost: paged discovery runs the existing visibility-scoped
summary query with an ID cursor and at most 101 returned rows. It adds
no count query, filesystem scan, provider call, or background worker.

## Risks

- Native `list_workspaces` now returns a page object instead of an
array. Individual summaries can be explicit previews when a single
record exceeds the byte budget. Its advertised descriptor and generated
contracts change together. Agents must follow `nextCursor` until null
for a complete inventory.
- Each page rechecks current visibility. Concurrent insertions,
deletions, or access changes require a fresh scan when a complete
point-in-time inventory is needed.
- No migration or journal-limit change. An existing cleanup quarantine
still requires normal recovery after deployment; this PR does not bypass
it.

## Model Used

OpenAI Codex, GPT-6 family, with reasoning, shell tools, code editing,
and test execution. The exact served model revision and context window
are not exposed in this session.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-05 13:24:52 -06:00