Commit Graph
5615 Commits
Author SHA1 Message Date
Devin FoleyandPaperclip fe85fd7ac4 Expire closed workspaces that open descendants do not use (#17464)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Git cache expiration removes closed issues' worktrees and saves
restore points for unsaved work
> - It treated a workspace as closed only when the source issue and
every descendant were done or cancelled
> - A routine that closes a parent and leaves children open for days
therefore kept every parent worktree, although the children work in
their own workspaces
> - The idle path did not help, because the children are updated every
day
> - This pull request counts only the issues that use the workspace when
it decides that a closed workspace can expire
> - The benefit is that closed parent worktrees are removed while their
children work elsewhere, and a child that later binds to the workspace
gets it back from the restore point

## Linked Issues or Issue Description

Refs #17271. Bug report described in-PR (bug report template).

**What happened?**
On one instance, an hourly routine creates a parent issue, works it in
its own worktree, marks the parent done, and leaves child issues open
(`blocked`, `in_review` or `in_progress`) for days. The children work in
their own workspaces. They are not bound to the parent's workspace. Git
cache expiration kept every parent worktree (about 300 MB each), because
the closed-tree check and the workspace reaper required all descendants
to be closed. In one project, 18 of 19 long-closed parent worktrees were
kept only by open children that did not use them. The idle path never
ran, because the children were updated daily.

**Expected behavior**
A closed issue's worktree expires while descendants are open, as long as
no open descendant uses that workspace.

**Steps to reproduce**
1. Enable git cache expiration and restore points.
2. Close an issue that has an isolated git worktree, and leave a child
issue open that works in its own workspace (or has no workspace
binding).
3. Wait past the terminal delay and run the workspace reaper: the parent
worktree stays, and the reaper counts it as `skippedNonTerminalTree`.

**Paperclip version or commit:** `master` at 410e5c6eed.
**Deployment mode:** any.
**Agent adapter(s) involved:** not adapter-specific (core).

## What Changed

- `isGitWorkspaceExpirationEligible` (closed path, not the idle path):
the "tree closed for one day" check now covers only the issues in the
tree that use the workspace. An issue uses the workspace when it is the
source issue, when its `execution_workspace_id` is the workspace, or
when its current `workspace_selection` or its
`workspace_pending_selection` selects it as the `existing` workspace.
Inheritance from a parent (`reuse_existing`) and `existing` selections
are both stored as `execution_workspace_id`, and heartbeat workspace
resolution reuses an existing workspace only through that binding or a
run's persisted native binding. An open descendant without one of these
references gets a new workspace on its next run, so it no longer keeps
the parent's worktree.
- All other checks stay the same: any open issue bound to the workspace,
live or unsettled runs of any issue in the tree or of the workspace
(including a run whose context names the workspace), finalizations,
leases, recovery actions, runtime services, shared cwd and the reopen
fence.
- Workspace reaper: when the source issue is closed, git cache
expiration applies, and no reopen is pending, the reaper routes the
workspace by the same rule. It takes the restore point due time from the
source issue and the descendants that use the workspace, also after
every descendant is closed. A recently closed child that used the
workspace still holds it for the terminal delay. The compaction delay
and the archive cooldown still use the whole tree when it is closed.
- Workspace reaper: compaction and the normal archive path keep their
behavior. While any descendant is open, the reaper does not compact the
worktree, and the expiration branch handles the workspace and continues.
The archive path still requires a closed subtree. With expiration off,
or a pending reopen, routing is unchanged.
- New exported helper `listIssuesUsingGitWorkspace` shares the SQL "uses
the workspace" predicate between eligibility and the reaper.
- Docs: `doc/recoverable-disk-cleanup.md` and the
`PAPERCLIP_GIT_RESTORE_POINT_TERMINAL_DELAY_DAYS` entry in
`doc/DEVELOPING.md`.
- The existing `preserves live descendants and pending reopening` case
now binds its open child to the workspace. An unbound open child no
longer keeps the worktree by design.

## Verification

- `pnpm exec vitest run
src/__tests__/git-restore-point-expiration.test.ts
src/__tests__/git-workspace-expiration.test.ts
src/__tests__/heartbeat-git-restore-point-resume.test.ts` in `server/`:
all pass.
- `pnpm exec vitest run src/__tests__/git-workspace-expiration
src/__tests__/git-restore-point src/__tests__/execution-workspace
src/__tests__/heartbeat-git-restore-point-resume
src/services/heartbeat/environment-selection
src/__tests__/isolated-repository` in `server/`: 15 of 17 files passed
in one parallel run. The other failures were the updated descendant case
(now fixed) and 45-second timeouts in restore point tests while 17
embedded Postgres files ran at the same time. Both files pass when run
on their own.
- `pnpm run typecheck` in `server/`: passes.
- New embedded Postgres cases with real git repositories:
- A closed parent with open `blocked`, `in_review`, `in_progress` and
`todo` children and a grandchild in their own workspaces (or with none):
the worktree expires with a restore point and the reopen restores it
exactly.
- The reaper archives it, timed from the issues that use the workspace.
A child closed one minute ago with its own workspace does not delay it.
- A recently closed child bound to the workspace holds it until its
terminal delay passes.
- When every child is closed and the most recent one used its own
workspace, the reaper does not restart the restore point delay.
- The worktree is kept when an open child is bound to it, when an open
grandchild under a child with its own workspace is bound to it, when a
child created later with `issueService.create` inherits it with
`reuse_existing`, when an open child selects it through
`workspace_selection` or `workspace_pending_selection`, when a child run
that references it is live, and when the parent was closed inside the
delay.
- The reaper never compacts the worktree while children are open, with
expiration on or off. A pending reopen keeps its current fence handling.
- After expiry, an open child that binds to the workspace reopens it
with the saved commits, index, working tree and untracked files.
- Heartbeat: a queued run for an open child that binds to the expired
workspace of a closed parent restores the workspace and runs the agent
in it with the saved state.
- Mutation checks (each run against the two restore point test files):
  - Closed-tree check restored to the whole tree: 7 tests fail.
  - `execution_workspace_id` removed from the rule: 3 fail.
  - `workspace_selection` removed from the rule: 1 fails.
  - `workspace_pending_selection` removed from the rule: 1 fails.
  - Reaper `latestTerminalAt` from the whole tree: 3 fail.
  - Reaper routing by the whole tree: 2 fail.
- Reaper restore point due time from the whole tree once it is closed: 1
fails.
  - Pending-reopen gate removed: 1 fails.
- Compaction gate (`!treeOpen`) removed: no test fails. Compaction's own
guard under the lifecycle lock still rejects open trees, so the gate
only skips work that cannot succeed.
- Not run: the full server suite and the UI suites. CI covers them.

## Risks

- Closed parent worktrees now expire while their children are open. A
child that later binds to the workspace depends on the restore point and
on the run-side reopen from #17271. The new tests cover this path. When
restore points are off, only exactly reproducible worktrees expire.
- An open child with no workspace binding no longer keeps the parent's
worktree. Heartbeat resolution never reuses a workspace without a
binding, so this does not change where the child runs.
- The rule is conservative. A workspace selected only by an issue's
current or pending selection is kept, and live runs of any issue in the
tree still block.
- The reaper runs one extra indexed query for each closed workspace when
git cache expiration applies. No migration.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), 1M context, through Claude Code
with tool use.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
canary/v2026.1024.0-canary.16
2026-10-24 12:43:57 -07:00
Devin FoleyandPaperclip 3a2c56780c fix(server): avoid execution payloads in run authority checks (#17467)
## Thinking Path

> - Paperclip manages AI agents and their tasks.
> - Each operation must use current task and user authority.
> - Chat recovery also checks the exact source-run ancestry.
> - These checks can repeatedly read full run payloads.
> - Their decisions need identity fields and two typed task keys.
> - This change selects those fields and keeps the current checks and
locks.
> - Complete execution context and output remain stored and available.

## Linked Issues or Issue Description

**What happened?**

Ancestry checks, instruction identity reads, task locks, and identity
activation
receipts transfer full execution context or full run rows for metadata
decisions.

**Expected behavior**

Select only required identity and binding fields. Keep fresh membership
and key
revocation checks, task-before-run locking, native task precedence, and
strict
validation of legacy JSON task identifiers.

**Steps to reproduce**

1. Store a run with large context, Runner profile, and output fields.
2. Read chat recovery ancestry or authorize an instruction operation.
3. Initialize or refresh its run identity.
4. Observe full context or full-row responses in these metadata paths.

**Paperclip version or commit**

Baseline: `14e1f59fbf4d9cc7875ed35ebaa9e997f6e2ea7c`.
Complementary work: Refs #17460 and Refs #17463.
Searched open and closed PRs for ancestry projections and run read
optimizations.
No duplicate was found. This PR has no dependency on #17463.

**Deployment mode**

External PostgreSQL. Tests and measurements use disposable real
PostgreSQL.

## What Changed

- Share a SQL projection of typed legacy issue/task keys with
task-binding readers.
- Select only source identity and retry links while checking chat
ancestry.
- Select current identity fields during instruction authorization.
- Narrow identity task-lock reads and activation receipts to task
binding fields.
- Preserve existing authorization, ancestry proof, revocation checks,
and lock order.

## Verification

- **72 focused tests pass** across read projections, run identity,
instruction authority, and chat admission retry.
- **49 assembled heartbeat recovery tests pass** for chat and automatic
ancestry; 359 unrelated tests were outside the filter.
- Real PostgreSQL tests include large payloads, task/company/agent
mismatch, current membership revocation, malformed JSON types, native
binding precedence, and invalid legacy identifiers.
- Real ancestry service SQL: 526,477 to 306 serialized response bytes
(99.942% smaller) with three 174,764-byte incompressible execution
fields.
- Every selected non-context value matches the baseline. Both complete
service calls return the same result. Typed binding cases are checked in
PostgreSQL.
- Five alternating warm samples after two warmups: median 2.152 ms
before, 0.812 ms after. These are local query/client timings, not hosted
request latency.
- Full build and recursive typecheck pass. The full local stable suite
is running; no full local pass is claimed. Current-head CI and review
must pass before merge.
- The first new-fixture run failed eight tests because it reused a
unique company prefix. Each fixture now has its own prefix; the focused
rerun passes all 72 tests.
- All current-head CI checks pass on
`ecc951b25d67fddc7dc2e0e3e2de861a40ee5ed0`. Greptile: **5/5, medium
impact**, no findings or unresolved threads.
- Staging effect is not measured yet.

## Risks

Missing identity fields could change an authorization or recovery
decision.
The tests exercise current authority and fail-closed behavior. The
change keeps
full execution reads intact, preserves JSON types, and does not cache
authority.
There is no migration, message deletion, timing change, or data
truncation.

## Model Used

OpenAI GPT-6 through Codex, with source inspection, code execution, and
GitHub tools.
The exact served 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-10-24 12:25:33 -07:00
DottaandPaperclip 6d46851920 test(e2e): add attended GitHub review bot journey (#17457)
## Thinking Path

> - Paperclip manages agents and the work they perform.
> - Product E2E checks verify real user paths through those agents.
> - A recorded GitHub review-bot walkthrough had no repeatable
registered case.
> - App setup, automatic triggering, publication, and task completion
need separate evidence.
> - This pull request adds an explicit attended journey to the existing
catalog and report tools.
> - Reusable setup profiles and independent assertions preserve the same
outcome across compatible targets.

## Linked Issues or Issue Description

Refs: #17262. This case remains an explicit attended attached-target
flow. It does not replace the shared deployment-target or
paid-operations framework.

**What existing behavior does this improve?**

The Product E2E catalog can now select and retain evidence for GitHub
review-bot setup and its first automatic review.

**Current behavior**

Recorded walkthroughs cannot be rerun as a fixed registered case. A
published review alone can hide a failed run or unfinished task.

**Proposed behavior**

Create and install an owned App, save automatic new-PR settings, create
a defective PR, and require correlated review delivery, an inline
finding, a consistent check, a successful run, and a Done task. Retain
failures and confirm owned-resource cleanup.

**Reason and benefit**

The case makes a useful external integration reproducible without
treating setup, unit checks, or report rendering as proof of a completed
live journey.

## What Changed

- Add two explicit attended cells for an existing configured agent in
local or Daytona execution.
- Declare hosting, account, connection, and credential prerequisites
through private setup bindings.
- Add fixed browser controls, ownership checks, independent grading, and
cleanup.
- Package declared screenshots through the existing sanitizer, manifest,
validator, and dashboard.
- Reject operations-managed execution until budgeted routing is
qualified.
- Document hosted QA credential setup and evidence limits.

## Verification

- Focused catalog, oracle, report, connection, and ownership checks: 127
tests passed.
- Inherited-environment preflight regression failed against the previous
commit and passed after the fix. Actual-run environment verification
remains required.
- Harness TypeScript check and full workspace typecheck passed. Full
build passed. The local full-test attempt was interrupted after repeated
embedded PostgreSQL startup failures; its exit code of zero does not
establish a pass; Exact-head Linux CI passed the full test, typecheck,
build, Runner, serialized, browser, and canary gates on
`abbddeade848edefc8de75f11cc9008b2cd52764`.
- A real hosted attempt published a rated review and inline finding. It
failed when its native session timed out before the task reached Done.
Cleanup passed. Full live qualification remains open.
- Credential scan found no known credential values in the authored files
or retained report corpus.

## Risks

- The full case creates external test resources and uses the configured
provider. It requires an explicit selection and current task authority.
It is excluded from default and CI matrices.
- Time and run limits are not a hard dollar cap. Missing dollar cost
stays unknown. Fixture setup does not weaken budget policy.
- Fresh personal OAuth, unattended authentication, runtime skill-read
proof, and cross-host qualification are separate claims.
- The selected live fixture needs environment/lifecycle recovery before
it can adopt a longer provider deadline.

## Model Used

OpenAI GPT-6 family through Codex, with reasoning, code execution, and
browser tools. The exact deployment 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, including 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 enhancement
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
ticket ID
- [x] I have run focused tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation
- [x] I have considered and documented risks
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open findings
- [x] I will address all Greptile and reviewer comments before merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-24 12:16:26 -07:00
Devin FoleyandPaperclip 7bd449dbb6 fix(recovery): project context evidence for latest-run checks (#17463)
## Thinking Path

> - Paperclip manages AI agents and their tasks.
> - Recovery sweeps decide whether tasks have a valid next step.
> - Those decisions inspect saved run metadata.
> - Latest-run queries also return full prompts and execution setup
data.
> - This repeats large responses when a sweep leaves a task unchanged.
> - This change projects the context evidence recovery actually reads.
> - A row-version check keeps stale evidence from changing a task.

## Linked Issues or Issue Description

**What happened?**

The latest-run recovery queries retrieve large stored contexts for each
candidate.
The decision usually consumes small recovery, message, or workspace-mode
fields.

**Expected behavior**

Keep all decision evidence while selecting a small response. Preserve
complete
stored contexts and full execution reads. Detect concurrent source
changes.

**Steps to reproduce**

1. Store a terminal run with large task Markdown, wake, workspace, and
continuation bodies.
2. Call `reconcileStrandedAssignedIssues` for its assigned task.
3. Observe the full context returned by the latest-run query on each
sweep.

**Paperclip version or commit**

Baseline: `410e5c6eedba98588a87e4104153898fb0437532`.
Related read optimization: Refs #17405. Complementary fix: Refs #17460.
These PRs have no code dependency. Searched existing PRs and issues for
run
projections, recovery reads, and database transfer. No duplicate was
found.

**Deployment mode**

External PostgreSQL. Verification uses disposable real PostgreSQL and
synthetic data.

## What Changed

- Project typed context evidence in all three latest-run recovery
queries.
- Keep only the used workspace mode and external-interaction flag from
nested setup objects.
- Preserve original JSON types, message references, retry evidence, and
existing query scopes and order.
- Read PostgreSQL `xmin` with the run. Require the same row version
during native blocked-wait repair.
- Retain the original full-context comparison for callers that supply a
full run without a row version.
- Test large payloads, malformed JSON shapes, review-message recovery,
and concurrent source edits.

## Verification

- Final-source integration checks: **450 passed** across context
projections, native finalization recovery, and the assembled heartbeat
recovery service.
- Focused issue-lock race check: **9 passed** (401 outside the filter),
including omitted-context and late-output edits and successful repair on
a fresh sweep.
- Final-source recursive typecheck and full build pass. The full local
stable suite is still running; current-head CI must also pass before
merge.
- Real-service SQL capture against the baseline: one returned row falls
from 699,724 to 573 serialized JSON bytes (99.918% smaller).
- The fixture uses four 174,764-byte incompressible execution fields.
All unaffected returned columns compare equal. Regression tests check
the retained decision evidence and unchanged stored records.
- Five alternating warm samples after two warmups: baseline median 1.479
ms, treatment 1.084 ms. Ranges overlap; no request-latency speedup is
claimed. Plans were captured outside the timing interval.
- An initial full-context hash projection cut bytes but raised the local
median to 7.457 ms. The final row-version check removes that cost.
- One integration run overlapped the hash-export rename and failed seven
tests on the removed export. A fresh final-source run passed all 450
tests. The earlier mixed-source failure remains recorded and is not
counted as a pass.
- All current-head CI checks pass on
`17add20e391c30ed0496f4b39eb9c59779efa6b4`. Greptile: **5/5, medium
impact**, no findings or unresolved threads on that head.
- Hosted transfer and product behavior remain to be verified on staging
after merge.

## Risks

The context selection must track its recovery consumers. Tests preserve
typed
flags, retry budgets, external-chat references, and malformed-input
behavior.
The row-version guard is conservative: any intervening run update defers
native
blocker repair to a fresh sweep. Concurrent omitted-context and output
edits are
covered through the real service and issue-lock wait; a fresh sweep must
still repair.
There is no migration, prompt truncation, history deletion, or
authorization cache.

## Model Used

OpenAI GPT-6 through Codex, with source inspection, code execution, and
GitHub tools.
The exact served 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-10-24 12:14:41 -07:00
Nicky Leach 60ca3a903a Keep warm sessions across project revision transitions (#17461) 2026-10-24 12:11:26 -07:00
Devin FoleyandPaperclip 14e1f59fbf fix(heartbeat): avoid large run reads during deferred-message recovery (#17460)
## Thinking Path

> - Paperclip manages AI agents and their tasks.
> - Deferred messages must survive restarts and execution cleanup.
> - Periodic recovery checks the latest run before it attempts release.
> - That check and two release checks load large execution records.
> - These checks use only admission evidence and completion identity.
> - This change selects those fields while keeping the release policy
and fresh reads.
> - Full execution records remain available for execution and detail
reads.

## Linked Issues or Issue Description

**What happened?**

Deferred-message recovery repeatedly transfers stored context, Runner
profiles,
and output excerpts to make small admission and release decisions.

**Expected behavior**

Select the fields each decision consumes. Preserve queued messages,
current
admission checks, cleanup ownership, and fresh completion reads.

**Steps to reproduce**

1. Save a terminal run with large context, profile, and output fields.
2. Leave an accepted message deferred after the issue lock is released.
3. Call `resumeQueuedRuns` and follow execution release.
4. Observe full-row reads before the admission decision.

**Paperclip version or commit**

Baseline: `410e5c6eedb`. Related read optimization: Refs #17405.
Searched open and closed PRs for run projections and stranded-queue
reads.
Searched open issues for database transfer. No matching fix was found.

**Deployment mode**

External PostgreSQL. The regression tests use disposable real
PostgreSQL.

## What Changed

- Select seven admission fields for the stranded queue's latest-run
lookup.
- Select authentication classification evidence before execution
release.
- Read completion status and typed issue identifiers after post-commit
effects.
- Keep company scoping on both release reads.
- Add large-payload and fresh-state regressions. Document the read
contract.

## Verification

- Focused PostgreSQL tests: 68 passed across queue, run control, and the
new release-read suite.
- Release reads return less than 2 KiB with three large synthetic
execution fields.
- Queue recovery passes less than 512 bytes to release, retaining the
full stored message.
- Coverage includes legacy/native owners, paused recipients, live
successors, authentication repair, and fresh completion identity after
post-commit effects.
- Root recursive typecheck and full build pass. The full local stable
suite remains in progress; no local full-suite pass is claimed.
- All current-head CI checks pass on
`c819fbb6b46bd5480a197407f06e02f245667792`. Greptile: **5/5, medium
impact**, zero comments and zero unresolved threads on that head.
- The first focused attempt found a syntax error in the new test; it was
corrected before the 68-test passing run. A redundant standalone
compiler was stopped while the required recursive check continued.
- Hosted impact is not measured yet. Staging verification follows merge.

## Risks

The projection must retain all admission evidence and preserve JSON
types.
The release policy still owns transactions, locks, cleanup, and
promotion.
There is no schema migration, polling change, message deletion, or API
change.
Configuration repair evidence is read directly, including when unrelated
result output is large.

## Model Used

OpenAI GPT-6 through Codex, with source inspection, code execution, and
GitHub tools.
The exact served 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>
canary/v2026.1024.0-canary.15
2026-10-24 12:07:45 -07:00
Nicky Leach 410e5c6eed fix(native): use current authority for warm plugin completion (#17444) canary/v2026.1024.0-canary.14 2026-10-24 10:54:40 -07:00
Nicky Leach 988fe0180b fix(heartbeat): keep source-free project policy stable across turns (#17447) 2026-10-24 10:54:28 -07:00
DottaandPaperclip 0eb2c964a7 fix(agents): allow runner qualification with agent configuration permission (#17448)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Agent owners configure the harness and computer used for that work.
> - A legacy runner upgrade must first qualify an unverified sandbox.
> - The upgrade called an instance-admin probe, so a company owner with
agent configuration permission received HTTP 403.
> - This pull request qualifies the saved computer through the agent's
existing configuration authorization.
> - Company owners can complete the upgrade while general instance
environment management stays restricted.

## Linked Issues or Issue Description

Refs #17361. Related: #17122.

**What happened?**

Upgrade runner displayed for an existing Claude agent on a Daytona
Paperclip Computer. Clicking it returned “Instance admin access
required” for a cloud company owner. The agent remained on the legacy
runner.

**Expected behavior**

A caller permitted to update the agent can qualify that agent's saved
computer and upgrade. Instance-admin access is not required for this
agent-scoped operation.

**Steps to reproduce**

1. Sign in as a cloud company owner with `agents:configure` and no
instance-admin role.
2. Open an existing legacy Claude agent on an unqualified Daytona
snapshot.
3. Select Upgrade runner.

## What Changed

- Add `POST /api/agents/{id}/qualify-runner` with the existing
`agent_config:update` guard.
- Reuse the server's effective environment resolver. The submitted
environment ID only detects stale state; it cannot select a different
computer.
- Preserve company binding checks, check binding again at lease
acquisition, and record agent-scoped audit activity.
- Return qualification status and the lease receipt without
instance-level provider metadata or raw provider errors.
- Use the new endpoint for the existing-agent upgrade. Keep the generic
environment probe restricted to instance admins.
- Disable Upgrade runner when an unsaved computer change resolves to a
different target, with a save-first explanation. Other draft settings
still accompany an upgrade.
- Add real-database authorization regression tests and update the API
contract and access maps.

## Verification

- Red: the existing UI called the instance-wide probe and failed the new
agent-scoped call assertions in eight upgrade cases.
- Green: 349 related route, provider qualification, runner selection,
and UI tests passed.
- Green: 12 authorization cases passed with real PostgreSQL, real HTTP
routes, real saved role grants, and a simulated provider boot. They
cover the cloud owner, removed grants, foreign agent/computer, inherited
targets, stale target, invalid body, archived/local target, redacted
failure, audit scope, and unchanged agent settings.
- Review regression: two cases first reproduced the enabled upgrade
button after an unsaved computer change; both now pass, along with all
168 related UI tests. Qualified/unqualified computers and restoring the
saved selection are covered.
- All 26 OpenAPI cases passed; the 12 real-database authorization cases
also passed after rebase.
- The host exhausted PostgreSQL semaphores. For the 12 cases, a
temporary local-only setup used disposable PostgreSQL 18 in Docker
instead of the embedded database launcher. The committed tests use the
normal embedded database helper. No authorization decisions were mocked.
- `pnpm -r typecheck`, `pnpm build`, `pnpm check:token-gates`, map
validation, module-boundary validation, and `git diff --check` passed.
- `pnpm test:run` was attempted but did not establish a full local pass:
the host could not start embedded PostgreSQL clusters. All current-head
CI gates passed, including the full suite in CI. The commitperclip
review workflow timed out before reporting any executed steps on its
first attempt and passed on one retry.
- Fresh staging acceptance remains pending deployment. Verify persisted
native configuration and a successful runtime test on the existing
Daytona computer after merge. Earlier live Daytona proof from #17361 is
separate evidence.

## Risks

- Qualification can start a fresh, temporary sandbox. It uses the
existing probe lifecycle and cleanup contract.
- The new path must remain scoped to the saved agent and its company.
Denial and redaction tests cover that boundary. General instance probe
authorization is unchanged.
- No migration, role change, credential change, or automatic agent
upgrade is included.

## Model Used

- OpenAI GPT-6 through Codex, with reasoning, code execution, and
browser tools. The exact deployed model variant and context-window size
were 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>
canary/v2026.1024.0-canary.13
2026-10-24 10:31:40 -07:00
DottaandPaperclip 7ae81c9fa7 fix(slack): support rich questions and complete review delivery (#17255)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Questions and review decisions must work in the conversation where a
task started.
> - Slack omitted question context and did not publish several review
kinds.
> - Some answers saved in Paperclip also lost their authorized
continuation or their final Slack reply.
> - A settled card and a successful run did not prove that the user
received the result.
> - This pull request preserves complete questions and follows the
existing answer and delivery receipts through continuation.
> - Users can read the request, submit an authorized decision, and
receive the resumed task's result in the same conversation.

## Linked Issues or Issue Description

Refs #17045 and #17218.

**What happened?**

Slack question cards could omit instructions and editable starting text.
Checkbox reviews, item verdicts, task suggestions, and connection
requests had no publication path. Complex question answers submitted in
Paperclip could fail continuation authorization. Several review
continuations completed without delivering their result to Slack.

**Expected behavior**

Show the actual question and context. Use native Slack controls when
they preserve the form contract. Otherwise provide an explicit Paperclip
review link. Persist one authorized response, resume through the
existing delivery path, settle the card, and deliver the useful result
to its originating conversation.

**Steps to reproduce**

1. Start a task in Slack and ask a text or choice question with
instructions and help text.
2. Submit native text and choice responses, then submit a complex form
through Paperclip.
3. Accept a checklist, submit item decisions, select suggested tasks, or
decline a connection request.
4. Inspect the saved answer, continuation run, original Slack card, and
final Slack reply independently.

## What Changed

- Preserve full prompts, context, descriptions, recommendations, help,
and editable starting text. Use direct buttons for short choices, inline
searchable selects for longer lists, and native checkboxes with explicit
submission for multiple choices.
- Add native custom-answer and validated-text modals, ordinary
confirmation buttons, checklist decisions, per-item verdicts with
reasons, suggested-task creation/dismissal, and connection decline. Keep
governed approvals, permission grants, and forms that cannot be
represented faithfully in the full Paperclip review surface.
- Keep Slack-specific canonical-validation feedback in its owning
platform module. Shared callbacks delegate without interpreting provider
presentation.
- Reuse the existing canonical interaction transactions, callback
receipts, publication queue, and continuation delivery. Share ordinary
confirmation handling across Telegram and Slack. Recheck current
authorization and commit response receipts atomically.
- Settle cards with the saved answer and remove stale controls. Ignore
passive Slack link callbacks so opening a task does not display a false
answer error.
- Validate exact multi-select receipt mappings during native
continuation. Recover automation-origin connection replies from their
published prompt after validating the durable connection receipt.
- Keep oversized multi-select handoffs free of unused native tokens, and
retain the complete Paperclip form for 101–128 choices plus Other. Make
the original-conversation audience explicit before submitting a Slack
modal.
- Add rollback, provider serialization, callback, continuation, and
negative authorization regressions. Update the connector playbook and
feature/access maps. No React or application UI changes are included.
- Add a durable Q01–Q11 qualification index and provider coverage
matrix. Require distinct functional, good-experience, and
best-available-control assessments; real unanswered/answering/settled
evidence; saved results, continuation and navigation proof; and retained
failures. Mark other providers unaudited rather than assuming Slack
parity.

## Verification

Candidate: `e0017d446b1016a38d65382366cd1408cce33039`. The
eleven-core-format live Slack gallery is pinned to
`c3d458b2a06b660448940fd8cf33c5906f0058d5`; subsequent boundary fixes
and the sharing notice have automated verification, not new live
screenshots.

- Real Slack UI testing covered all 11 formats: text, closed choice,
long single select, multiple choices, choices with a custom answer,
pattern-validated text, confirmation, checklist, item verdicts,
suggested tasks, and connection requests. Each has
unanswered/answering/answered screenshots as applicable, an inspected
saved result, one continuation, and an individual experience review in
the local HTML gallery.
- The dedicated private QA app/channel used signed callbacks to the
local candidate. A separately approved password-protected, read-only
preview proved connection-review, suggested-task review, and
created-task navigation. Anonymous access, writes, and credential routes
were denied. No OAuth grant was attempted.
- Real testing exposed and repaired a multi-select continuation failure
and a misleading error on link-button clicks. Failed attempts remain in
the evidence. The final navigation replay opened the exact pending
connection request and declined it in Slack without granting access.
Suggested-task creation produced the selected child with the correct
parent and description.
- Final review-fix verification: **32 form tests**, **28
provider/form-rendering tests**, **30 publication tests**, and **171
native continuation/authorization tests** passed. Earlier **3
atomic-resolution rollback**, **9 targeted
callback/publication/Telegram**, and **28 review-continuation** cases
passed. The final Slack/Teams modal subset also passed. The
automation-origin connection cases, 101/128 custom-choice cases, and
oversized multi-select continuation reproduced before their fixes and
passed afterward; failing logs remain retained. These counts describe
relevant runs, not a reliability percentage.
- CI on `b5885b85fd` exposed a test fixture that assumed every Slack
modal block was an input; the new sharing notice correctly renders a
non-input block. The fixture now selects input blocks safely. The exact
signed-modal correction/one-continuation test passed locally, together
with 14 Slack form/modal tests. The failing CI log and its specific
cause remain recorded.
- `pnpm -r typecheck` and `pnpm build` passed. Server typechecking was
repeated after the final source changes. Map and module-boundary checks
and `git diff --check` passed.
- The full `pnpm test:run` was attempted and stopped after repeated
embedded PostgreSQL semaphore exhaustion on this host. Its 1,395-test
chat suite found two stale native-control assertions; both were
corrected and re-tested in a focused run. Focused database suites used
separate disposable databases on existing local PostgreSQL. The full
suite is not claimed green; new-head CI must complete before merge.
- Final-head CI passed on `e0017d446b1016a38d65382366cd1408cce33039`:
all test/build jobs and required `ci / verify` and `ci / e2e` gates are
green
([run](https://github.com/paperclipai/paperclip/actions/runs/40115430194)).
Greptile and Ponytail both reviewed this exact head at 5/5, the security
scan passed, and all review threads are resolved.
- All three QA app callbacks were restored to staging. Both temporary
tunnels and local proxies were closed. The local endpoint was archived
and its local credential copies removed. The candidate app was stopped.

Limits: provider execution used a deterministic Claude protocol fixture,
not an LLM. This is local real-provider UX proof, not deployed staging
or broader Codie/Claude qualification. The public preview intentionally
blocked ancillary artifact routes, so its destination pages could show a
partial-history warning. The complete permission-grant flow and every
oversized/mixed-form branch were not driven manually. Earlier fixture,
test, and provider failures remain recorded.

## Risks

- New reply routes require durable origin proof. Incorrect receipt
matching could lose a reply or route it incorrectly. Tests cover
missing, forged, mismatched, and revoked authority.
- Governed forms keep their Paperclip resolution checks. This change
does not grant new approval authority to Slack.
- Existing completed review results can become deliverable through the
repaired publication path without another provider run.
- No schema, lockfile, workflow, public task-status model, or
runtime-instruction change is included. This branch is not deployed.

## Model Used

OpenAI Codex, GPT-6 family, with code execution and tool use. The exact
backend 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 linked existing issues or described the issue in-PR
following the relevant issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links
- [x] My branch name describes the change and contains no internal
Paperclip ticket id
- [x] I have run focused tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green on the new head
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
on the new head
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-24 10:29:15 -07:00
DottaandPaperclip b7506671b9 fix: preserve qualified snapshots during native setup validation (#17445)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native agent setup can validate a selected sandbox before the agent
runs.
> - A qualified snapshot binds the saved environment and resolved
runtime to an immutable provider snapshot.
> - Setup validation changes lease retention so each test starts fresh
and leaves an archived sandbox for inspection.
> - The test passed that changed config as the saved environment, which
made valid snapshot proof appear stale.
> - This pull request keeps saved qualification separate from the two
test retention settings.
> - Native setup can use the qualified snapshot and still clean up its
test lease.

## Linked Issues or Issue Description

Related: #15997, #17262.

**What happened?**

Native setup validation can return `environment_lease_acquire_failed`
for a sandbox whose saved snapshot qualification is valid. The test
changes `reuseLease` and `archiveOnRelease` before the runtime checks
the saved configuration fingerprint.

**Expected behavior**

The test must qualify the original saved environment, acquire a fresh
lease from its immutable snapshot, and preserve archive-on-release in
the lease metadata. A changed image, provider, runtime config, or stale
proof must still fail.

**Steps to reproduce**

1. Save a Daytona snapshot environment with `reuseLease: true` and
`archiveOnRelease: false`.
2. Store valid company-scoped snapshot qualification for the saved and
resolved runtime configurations.
3. Run native agent setup validation against that environment.
4. Observe that the validation retention overrides invalidate the saved
fingerprint before a provider lease is acquired.

**Paperclip version or commit**

Reproduced from master at `785d54636e1c8513ee5503d602daf6a5ee1970d6`.
The fix is based on `62f60bf158746c5807763778326b1eba4f2ed18a`.

**Deployment mode**

Any server with a qualified native Daytona sandbox environment.

## What Changed

- Pass the original saved environment to native setup lease acquisition.
- Allow only `reuseLease` and `archiveOnRelease` to differ in the run
configuration. Apply those fields after saved snapshot qualification.
- Keep both retention fields in the acquired lease metadata so provider
release still archives the test sandbox.
- Add service-to-runtime cases for valid and stale proof, immutable
snapshot selection, fresh lease retention, release behavior, and
rejected non-retention overrides.
- Extend the route regression to require the original saved
configuration.

## Verification

- The focused route regression failed before the fix because acquisition
had no `configuredEnvironment`.
- All 101 focused route, snapshot, and orchestration tests passed on the
exact rebased head.
- Full repository typecheck and build passed on exact head
`15e13e119941090e85638005de54b2d14ea52c00`.
- The local database regression could not start PostgreSQL because macOS
could not allocate system semaphores. The full `pnpm test:run` attempt
also reported database startup failures and was stopped as incomplete.
Neither attempt is counted as passing.
- Exact-head Linux CI passed all 179 environment runtime integration
tests, including the seven new regression cases. Its server shard passed
3,635 tests with 16 existing skips. All exact-head CI gates passed. The
final repository review must also complete before merge.
- No live provider or model call is part of this verification. This PR
does not deploy the fix.

## Risks

The acquisition boundary now accepts two explicit retention-only
differences. All other config fields, environment identity,
company-scoped proof, resolved runtime fingerprint, and template binding
remain subject to the existing checks. Regression cases reject changed
snapshot, image, provider, and environment variables before provider
dispatch. No schema or API contract changes.

No user-facing command or documentation contract changes.

## Model Used

OpenAI Codex, GPT-6 family, with reasoning, repository inspection, code
execution, and test tools. The session does not expose an exact backend
model identifier 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-10-24 09:59:35 -07:00
Nicky Leach 62f60bf158 Measure first deliverable text in native run traces (#17434) canary/v2026.1024.0-canary.12 2026-10-24 09:40:08 -07:00
DottaandPaperclip d1fdfced9d feat(evals): route Runner sessions through a controller gateway (#17440)
## Thinking Path

> - Paperclip manages AI agents and their work.
> - Runner Evals test provider behavior against a mock control plane.
> - A campaign budget needs to observe and admit each provider request.
> - The existing eval CLI does not select the provider-home mode needed
for a controller-assigned route.
> - A proxy URL alone therefore cannot establish controlled Codex
routing.
> - This change adds an explicit route with an isolated home and
retained identity.
> - The controller can use that route as the basis for request
accounting and admission.

## Linked Issues or Issue Description

**What happened?**

The direct Runner eval CLI passes the ambient environment to the native
transport. The transport deliberately removes an environment-only
preconfigured-home marker unless its explicit option is also set. This
prevents a controller from assigning a Codex gateway through that marker
alone.

**Expected behavior**

A controller can explicitly route a supported eval attempt through its
local proxy. The provider receives the proxy token and isolated
configuration. The artifact identifies the assigned route on success and
failure. Ordinary direct runs keep their current path.

**Steps to reproduce**

Run the new CLI tests for `--gateway-config`. They validate the
descriptor, inspect the actual transport inputs and provider sanitizers,
and check retained artifacts and owned-home cleanup. The local probe
also ran built native Codex and ACPX Claude against a fake loopback API
using only synthetic credentials.

Related request-accounting work:
https://github.com/paperclipai/paperclip/pull/17432.

## What Changed

- Add an explicit gateway descriptor bound to the requested provider,
exact model, campaign, execution, and attempt.
- Accept only the controller's IPv4 loopback URL and a separate proxy
token.
- Create a fresh provider home for native Codex and ACPX Claude. Exclude
ambient provider credentials, subscription logins, proxies, and startup
hooks.
- Retain route identity and descriptor hash in successful and failed
artifacts. Remove the owned home in finalization.
- Document the CLI contract and its limits.

The Runner eval CLI owns this behavior. It uses the existing native
preconfigured-home contract. Production routing, permissions, and
provider protocols are unchanged.

## Verification

- Initial verification passed 56 focused gateway/CLI tests and 246 CLI
and adjacent provider-isolation tests. After the admission repair, all
247 CLI and adjacent provider-isolation tests passed, with TypeScript
checks and build passing again.
- Runner TypeScript checks and build passed.
- The native daemon rebuilt from the candidate base with locked, offline
dependencies.
- Local fake-API probe: six authenticated Responses requests from native
Codex and six authenticated Messages requests from ACPX Claude reached
the assigned endpoint. Claude also made an unauthenticated hello check.
The final commit retained both route artifacts. All 27 retained files
were scanned for the synthetic proxy token and contained none. Both
temporary homes were removed, and all four recorded provider/daemon PIDs
were absent after exit.
- The fake API intentionally rejected inference. Codex retained a failed
turn; Claude retained a timeout. These are route observations, not
passing semantic evals or live provider qualification. No paid provider
calls ran.
- The first focused command used the wrong directory and discovered no
tests. It was corrected before the successful runs above.
- Full Runner validation: 3,967 tests passed, 13 skipped, and five
failed in three unchanged installation and native-settlement files. The
failures were two installation timeouts and three retained-cleanup
settlement failures. One isolated run passed all 109 applicable tests
with six skips. This shows recovery, not a proven cause for the original
five failures; those remain retained. Hosted [CI
40111220754](https://github.com/paperclipai/paperclip/actions/runs/40111220754)
passed on final commit `a5c47b64e56c1228fca718b72a81ddc3b1d185a4`,
including repository typecheck, build, tests and Runner checks. All 57
checks passed or were neutral/skipped, and the legacy status passed.
Greptile and Paperclip reviews both scored 5/5 on that commit; no
findings remain unresolved.
- Rebased onto current master
`71c04c0bfb205cc9e7f7bc20c29fd76051b44cc4`. Its added change concerns
server cleanup; Runner source is unchanged.

- An actual ACPX Codex probe failed before reaching the fake API. Review
and source inspection confirmed that Runner deliberately rejects its
preconfigured home to protect Runner state. This route is now rejected
before service creation. Both explicit and default ACPX Codex admission
regressions failed before the repair. The unsupported lane is not
presented as qualified.

## Risks

This is an explicit new CLI route. Unsupported providers fail before
service creation. Upstream route is a controller declaration, not
independent billing evidence. The local probe saw six Codex requests
where legacy usage counted one, so the controller's request ledger must
remain the budget source. Campaign allocation, proxy lifecycle, hosted
workflow wiring, dedicated OTLP relay routing, and paid qualification
are separate remaining integration steps. This PR enables none of those
schedules.

## Model Used

OpenAI Codex, GPT-6 family, with reasoning, tool use, and code
execution. The exact serving 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, including the information
available in this session
- [x] I checked ROADMAP.md; this is eval infrastructure, not a competing
core feature
- [x] I searched for duplicate PRs for this exact route change
- [x] I described the issue in-PR using the bug template labels
- [x] I have not referenced internal or instance-local issues
- [x] My branch describes the change and contains no internal issue ID
- [x] Relevant focused tests pass
- [x] I added tests for the new contract and lifecycle
- [x] I updated the relevant documentation
- [x] I described the risks and unqualified behavior
- [x] All current-head Paperclip CI gates are green
- [x] Current-head Greptile is 5/5 with no unresolved findings
- [x] I will address review findings before merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-24 09:34:10 -07:00
DottaandPaperclip 5db5734774 fix(plugins): scope operational reads to the caller's companies (#17435)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - One instance hosts many companies, and company boundaries are the
main isolation contract
> - Plugin worker logs, job runs and webhook deliveries store a
`company_id` attribution (NULL for instance scope)
> - The plugin diagnostics routes checked board org access, but they did
not filter those rows by the caller's companies
> - A board member of one company could therefore read another company's
log messages, run errors and delivery errors
> - This pull request scopes those reads to the caller's companies and
keeps the instance-wide view for instance administrators
> - The benefit is that plugin diagnostics obey the same tenant boundary
as the rest of the plugin routes

## Linked Issues or Issue Description

**What happened?**
A board user who is a member of only company A could read plugin
operational rows attributed to company B. `GET
/api/plugins/:pluginId/logs` returned log rows with `company_id` values
outside the caller's companies. `GET
/api/plugins/:pluginId/jobs/:jobId/runs` returned other companies' run
rows, including their error text and run logs. `GET
/api/plugins/:pluginId/dashboard` returned other companies' recent job
runs and webhook deliveries. The routes only required board org access
and filtered on plugin id or job id.

**Expected behavior**
A board member sees only operational rows for companies the member can
access. An instance administrator (and the local implicit board) keeps
the instance-wide view, which includes instance-scope rows where
`company_id` is NULL.

**Steps to reproduce**
1. Create two companies, A and B, on an authenticated instance.
2. Install a plugin and produce a log row, a job run and a webhook
delivery for company B.
3. Sign in as a board user who is a member of company A only.
4. Call `GET /api/plugins/:pluginId/logs`, `GET
/api/plugins/:pluginId/jobs/:jobId/runs` and `GET
/api/plugins/:pluginId/dashboard`.

Before this change, the responses included company B rows. After this
change, they do not.

**Paperclip version or commit**
Reproduced on `2e6d3f7` (`master`, 2026-10-24). The report came from a
scoped security audit of `52a66af29ddccf1fcac25f1f6efa9e9958e11fbd`.

**Deployment mode**
Self-hosted server, authenticated mode. The defect does not depend on
the deployment mode, but `local_trusted` always yields an instance-admin
actor and hides it.

## What Changed

- Added `pluginOperationalReadCompanyIds()` in
`server/src/routes/plugins.ts`. It returns `null` for instance admins
and the local implicit board, and the actor's company ids for every
other board actor.
- Scoped `GET /api/plugins/:pluginId/logs` with
`inArray(pluginLogs.companyId, companyIds)`.
- Scoped `GET /api/plugins/:pluginId/jobs/:jobId/runs` through a new
optional `companyIds` argument on `pluginJobStore.listRunsByJob()`.
- Scoped the dashboard's recent job runs through the same optional
argument on `pluginJobStore.listRunsByPlugin()`, and scoped its webhook
delivery query.
- Added
`server/src/__tests__/plugin-operational-reads-tenant-scope.test.ts`. It
starts embedded Postgres, seeds two companies plus instance-scope rows,
and asserts the single-company view and the instance-admin view for all
three routes.

## Verification

- Red first: with the test added and the route unchanged, 3 of 4 cases
failed and the response contained the company B rows.
- Green after the fix: `pnpm exec vitest run
server/src/__tests__/plugin-operational-reads-tenant-scope.test.ts`
passed (4 tests).
- Related suites: `pnpm exec vitest run
server/src/__tests__/plugin-routes-authz.test.ts
server/src/__tests__/plugin-tenant-isolation.test.ts
server/src/__tests__/plugin-install-route-security.test.ts` passed (59
tests).
- Typecheck: `pnpm --filter @paperclipai/server typecheck` passed.

## Risks

- Behavior change for non-admin board members: they no longer see
instance-scope rows (`company_id` NULL) or rows for companies they
cannot access. Instance administrators keep the full view. For a
single-company instance the visible rows are the same, except
instance-scope rows.
- No schema or migration change. No API shape change. Row-level
filtering only.
- A plugin that writes diagnostics without a `company_id` will show
fewer rows to non-admin users. That is the intended tenant boundary.

## Model Used

- Provider: OpenRouter
- Model: `deepseek/deepseek-v4.1-flash-1017` (DeepSeek V4.1 Flash)
- Harness: OpenCode, with tool use, code execution, and multi-agent
delegation
- Reasoning: enabled (thinking mode)
- Context window: 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
- [ ] 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-10-24 09:28:17 -07:00
Devin FoleyandPaperclip 785d54636e Delete re-homed isolated clones when their contents are reproducible (#17410)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Isolated task workspaces are Git worktrees on the server disk, and
the terminal workspace reaper keeps that disk usage bounded
> - #17369 moves old workspaces off their private per-issue clones onto
the project's shared clone and then deletes the private clone
> - On one instance, the first live sweep moved 12 workspaces but
deleted none of their clones (about 600 MB each), and 3 workspaces could
not move because their restore point was "too large"
> - The kept clones are not referenced by any workspace, so no other
cleanup removes them
> - This pull request replaces the clone check with a real
reproducibility check, sweeps the orphaned clones, lifts the restore
point limits for local transfers, and fixes a false
`source_identity_changed`
> - The benefit is that the disk space #17369 was meant to reclaim is
reclaimed, without deleting unique work

## Linked Issues or Issue Description

Refs #17369. Bug report described in-PR (bug report template).

**What happened?**
On one instance, with `PAPERCLIP_ISOLATED_SOURCE_REHOME_ENABLED=true`
and dry run off, the first sweep re-homed 12 workspaces, skipped 5 with
`live_or_unresolved_consumer` and 3 with `restore_point_too_large`. No
clone was removed: 11 logged `cloneKeptReason:
"clone_checkout_has_files"` and 1 logged `"source_identity_changed"`.
Each clone's own checkout was clean (`git status --porcelain
--untracked-files=all` empty). Its only history that the shared clone
lacked was remote branches and release tags that the shared clone never
fetched, which exist on the remote. An operator removed 11 clones by
hand after verifying them.

**Expected behavior**
A re-homed clone whose checkout is clean and whose local history is in
the shared clone or on the remote is deleted. A clone with real unique
work is kept. Workspaces whose changes exceed the upload limits still
move, because the transfer is local.

**Steps to reproduce**
1. Use a project repository whose `.gitignore` (or the clone's
`info/exclude`) contains `.paperclip/`, with a release tag on a commit
that is not on any branch.
2. Re-home an isolated workspace of that repository with #17369.
3. The old worktree is removed, the empty `.paperclip/worktrees`
directory stays, and `git ls-files --others --ignored --directory` lists
`.paperclip/`. The clone is kept with `clone_checkout_has_files`.
Without the ignore line, the local release tag keeps it with
`unique_clone_history`.
4. Add a file larger than 64 MiB to a workspace. The move skips with
`restore_point_too_large`.

**Paperclip version or commit:** `master` at aa0037a137 (rebased on
ee29e2850d, after #17407).
**Deployment mode:** any.
**Agent adapter(s) involved:** not adapter-specific (core).

## What Changed

- New clone reproducibility check (`cloneDisposable` in
`server/src/services/isolated-repository-rehome.ts`), used before
deleting a re-homed clone:
- `git worktree prune`, then only the clone's own worktree may remain
(`base_has_worktrees`);
- no tracked changes and no untracked files
(`clone_checkout_has_files`); the only untracked exception is
Paperclip's compaction trash;
- ignored files only inside regenerable directories (`node_modules`,
`dist`, ...) or Paperclip's compaction trash. Ignored entries are listed
with `--no-empty-directory`, and a non-regenerable ignored directory is
expanded file by file, so empty directories such as the old worktree's
`.paperclip/worktrees` parent no longer count;
- only `refs/heads`, `refs/tags` and `refs/remotes` (no stash, notes or
replacement refs: `unsupported_clone_refs`);
- every commit reachable from local branches, local tags, non-remote
reflogs, `ORIG_HEAD` and `HEAD` (so a detached checkout counts) must be
on a remote-tracking ref, present in the shared clone, or on the remote.
The remote check reuses `hasCommitsMissingUpstream` (now exported),
which fetches the remote's branches and tags into a temporary store, and
it runs only when some commit is in neither place. A local tag is
accepted when its commit is on the remote. Otherwise the clone is kept
(`unique_clone_history`), or kept with
`managed_checkout_upstream_unavailable` when the remote cannot be read;
- existing 5,000 budgets for refs, reflog entries and unique commits
(`clone_history_budget`), plus at most 1,000 ignored directories outside
regenerable ones, read in one Git call (`clone_file_budget`);
- the log entry now includes `cloneKeptPaths`: up to five paths,
worktrees, refs or commits that kept the clone.
- Cleanup also checks that the clone's `origin` still matches the
workspace `repoUrl` before deleting it.
- Old worktree ownership (#4): it is proven by `git rev-parse
--git-common-dir`, or, when that fails, by the worktree's `.git` pointer
into `<clone>/.git/worktrees/`.
- Orphan sweep (`sweepOrphanedIsolatedRepositorySources`): at the first
reaper rotation after server start (feature on), it walks
`isolated-repository-sources/<company>/<issue>/checkout`. It holds the
isolated source lock and skips clones that a workspace still needs (an
open workspace with matching `isolatedRemoteSource`, a closed one that
is not expired with its worktree removed, a project workspace or a
running service), clones with a pending move, and clones whose re-homed
workspace still has its own cleanup pending. The repository URL comes
only from the database: the issue's workspaces must record exactly one
URL and the clone's `origin` must equal it (`unproven_repository` or
`source_identity_changed` otherwise), so a rewritten `origin` is never
fetched. It then applies the same check (using the shared clone of a
workspace of the same issue and repository when one exists), renames to
trash and deletes. It logs `event:
"execution_workspace.isolated_source_orphan_cleanup"` and removes
leftover trash directories from interrupted removals. It inspects at
most eight issue directories per reaper sweep and resumes from the saved
position in the next sweep, so slow remotes cannot hold up other
cleanup. When a full walk kept any orphan, it registers a deadline one
day later in the reaper's existing deadline set. Otherwise it registers
nothing until the next server start. No new timer.
- Restore point limits for re-home (#3):
`ISOLATED_REHOME_RESTORE_POINT_LIMITS` (500,000 files, 1 GiB per file
and in total, 1.5 GiB bundle, 1,000,000 commits) replaces the upload
defaults for this local transfer. Verification is unchanged (exact
snapshot equality). In `git-restore-point.ts`, `applyRestorePoint` now
streams `git diff --binary` into `git apply` instead of buffering it
(and skips the pipe when there is no diff, so no new Git version is
needed). On a failure it kills both processes and waits for both. The
ignored-file read sizes its buffer from the tree instead of the default
limit.
- Docs: `doc/recoverable-disk-cleanup.md`.

### What I found for `source_identity_changed` (#4)

In #17369, cleanup ran `git rev-parse --git-common-dir` in the old
worktree and used `""` when the command failed. Then it compared that to
the clone's `.git` path, so any Git failure in the old worktree was
reported as `source_identity_changed`. Git fails there when the old
worktree's registration in the clone (`<clone>/.git/worktrees/<name>`)
is gone while the directory still exists. Its `.git` file then points to
a missing directory. I reproduced this by deleting the registration
after a re-home: the old code keeps the clone with
`source_identity_changed`, the new code removes the old worktree and the
clone. I could not inspect the instance, so the exact event that removed
that one registration is not confirmed. The check was a false positive
in that case, and the fix proves ownership from the `.git` pointer
instead. A worktree whose pointer leads outside the clone is still kept.

## Verification

- `pnpm exec vitest run
src/__tests__/isolated-repository-rehome.test.ts` in `server/`: 28
passed (real Git repositories, embedded Postgres). New cases:
- a clean clone whose only extra history is a remote branch and a
release tag on a commit outside any branch, both absent from the shared
clone, with `.paperclip/` ignored in the clone: deleted;
- a clone with a local branch commit that is neither upstream nor in the
shared clone: kept (`unique_clone_history`, the commit is in
`cloneKeptPaths`);
- a clone with a tracked change, then an ignored non-regenerable file
next to an ignored `node_modules`, then an untracked `build/notes.txt`:
each kept (`clone_checkout_has_files` with the path), then deleted after
cleanup;
- a clone with a second registered worktree: kept
(`base_has_worktrees`);
- a clone whose detached `HEAD` holds the only copy of a commit (reflogs
expired): kept (`unique_clone_history`);
- an old worktree whose registration was pruned: old worktree and clone
removed;
- an orphan whose `origin` was rewritten is kept with
`source_identity_changed`, and with `unproven_repository` once no
workspace records its repository;
  - 1,001 ignored directories keep a clone (`clone_file_budget`);
- orphan sweep: a batch of one returns a resume position and the next
batch finishes the walk; a dry run changes nothing. Then an orphan whose
history is in the shared clone or on the remote is deleted, and an
orphan with no workspace rows and an unpushed commit is kept, with the
next deadline one day later;
- a workspace with 65 MiB untracked and ignored files (default capture
refuses them with `restore_point_too_large`) moves with an exact
snapshot match.
- After rebasing on #17407 (nested clone migration, which reuses helpers
from the re-home module): `isolated-repository-rehome`,
`nested-clone-migration`, `execution-workspaces-service`,
`execution-workspaces-routes`, `execution-workspace-reopen`,
`execution-workspace-compaction`, `execution-workspace-policy`,
`execution-workspace-runtime-control-conflict`,
`execution-workspace-runtime-lease-route`, `git-workspace-expiration`,
`git-restore-point`, `git-restore-point-expiration`: 405 passed;
typecheck passes.
- Related suites in `server/` (before the rebase):
`git-workspace-expiration`, `git-restore-point`,
`git-restore-point-expiration`, `execution-workspace-reopen`,
`execution-workspaces-service`, `execution-workspace-compaction`,
`heartbeat-git-restore-point-resume`, `heartbeat-project-repositories`,
`git-reproducible-managed-checkouts`,
`isolated-remote-workspace-source`, with the new suite: 398 passed.
- `pnpm run typecheck` in `server/` passes.
- Mutation checks, each failing a test:
- remote proof replaced by "always unique": the unfetched-history and
orphan tests fail;
- remote proof replaced by "never unique": the local-commit and orphan
tests fail;
  - `.git` pointer fallback removed: the pruned-registration test fails;
  - re-home limits removed: the large-file test fails;
  - orphan sweep removed from the reaper: the orphan test fails;
  - worktree count check removed: the second-worktree test fails;
- regenerable-directory exception applied to untracked files: the
dirty-checkout test fails;
  - orphan batch limit removed: the orphan test fails;
- orphan URL taken from the clone again: the rewritten-origin test
fails;
  - ignored directory budget removed: the many-directory test fails;
- `HEAD` left out of the checked commits: the detached-HEAD test fails.
- Dropping only `--no-empty-directory` does not fail a test, because the
per-directory expansion also accepts an empty ignored directory. The two
mechanisms are redundant on purpose.
- Not run: the full server suite, or the change on a live instance.

## Risks

- Deleting a clone now depends on reading the remote when some local
commit is neither on a remote-tracking ref nor in the shared clone. If
the remote cannot be read, the clone is kept.
- Commits that only remote-tracking refs hold (for example a remote
branch deleted upstream after the clone fetched it) are treated as not
unique, as before. They were pushed once.
- The orphan sweep deletes clones that no workspace refers to. A closed
workspace that is expired with its worktree removed does not count as a
user, because reopening it rebuilds from the shared clone or clones
again.
- Re-home transfers can now use up to about 1.5 GiB of memory for a
single restore point payload, so a very large workspace uses more memory
during its move than before.
- The orphan sweep runs once per server start and then only while it
keeps an orphan, so a clone that becomes orphaned later waits for the
next restart.
- No migration. No schema change.

## Model Used

Claude Opus 5.5 (`claude-opus-5-5`), 1M context, through Claude Code
with tool use (shell, file editing, Git, 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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-24 09:17:28 -07:00
DottaandPaperclip 71c04c0bfb fix: stop idle environment cleanup polling (#17145)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Agent environments can leave pending teardown or unverified provider
resources.
> - Recovery must preserve those resources until ownership and workspace
safety allow cleanup.
> - Empty maintenance scans keep an idle instance and its database
active.
> - The existing rows, transaction signals, and scheduler can represent
the actual recovery obligation.
> - This pull request schedules pending cleanup and historical
verification only while work remains.
> - An empty or fully verified instance stops these scans without losing
cleanup recovery.

## Linked Issues or Issue Description

**What happened?**

The server scanned pending environment cleanup on its maintenance tick.
The run reaper also scanned it even when it recovered no runs or active
leases. Historical resource verification scanned every five minutes,
including when all history already had valid cleanup proof.

**What did you expect?**

Writes should wake cleanup. Pending rows, deferred orphan buffers, and
unverified history should retain recovery deadlines. When none remain,
these workers should make no periodic database or provider calls.

**Steps to reproduce**

Start a configured instance with no pending leases and fully verified
history. Leave it idle. Observe repeated pending-cleanup and
historical-verification reads from the maintenance paths.

Related: #17106 supplies the preceding login-cleanup migration. #13523
improves lease-reaper query indexes. #16809 addresses reusable-sandbox
recovery authority. This PR keeps those authority checks in their
existing services.

## What Changed

- Register two workers with the existing delivery coordinator and shared
scheduler. Remove their unconditional maintenance-tick calls. Run the
reaper's inline pending cleanup only after it recovers actual work.
- Add commit-aware signals to the audited lease and custom-image
writers. Existing transactions notify directly; other writers use one
small transaction helper. No tables or migrations are added.
- Derive pending cleanup deadlines from the existing row backoff and
retry or claim metadata. Retain a historical retry only while an
unverified candidate exists.
- Register unwritten orphan buffers across runtimes that share the
database handle. Buffer registration wakes recovery without a database
connection. Load the existing spool at startup and retry failed or
capacity-limited loads while needed.
- Isolate spool failures per runtime. Attempt all buffers and persisted
pending rows, then retain a retry for any failed spool.
- Document producer coverage, idle behavior, and remaining blockers.
Update the sleep inventory.
- Repair the inherited CSS-recovery E2E test: the current build no
longer emits task-page CSS. Exercise the actual lazy environment
stylesheet, preserving real network failure, recovery, and bounded
reload assertions.

## Verification

October 24 integration update: merged current master and resolved the
runtime/spool conflicts. Preserve serialized ingestion of records
appended after startup. Plugin replacement failures now register
deferred recovery with an idle hold; concurrent callers share one
persistence attempt. The producer audit includes this new writer.

- Focused cleanup selection: 270 tests now pass across five suites. The
initial run lacked a built plugin SDK and executed no tests; building
the SDK fixed setup. The new concurrent-flush assertion then reproduced
duplicate rows; a single-flight guard fixed it and all 18 plugin
integration tests passed on rerun.
- Server typecheck passed. After review fixes, all 211 tests across the
cleanup worker, real orphan spool, environment runtime, and plugin
integration suites passed. These overlap the earlier focused selection.
The rate-limit test initially expected a write after twenty idle minutes
to wait; corrected it to assert that subsequent writes cannot accelerate
the five-minute cadence.
- Review fixes preserve retries for unreadable spool
directories/records, keep historical provider checks off the startup
wait while joining them at shutdown, and restore their independent
five-minute minimum spacing. The complete [CI
matrix](https://github.com/paperclipai/paperclip/actions/runs/40108905033)
passed on final commit `79ae1589c3c7b924db4bedb337d0c7ed3d95020b`,
including all E2E shards, server/workspace tests, runner checks,
typecheck, build/public-install verification, and security checks.

- CI found a stale E2E asset matcher: `issue-detail-*.css` is no longer
emitted. Both corrected browser tests passed against a fresh production
UI build and disposable local instance. Fresh Greptile review is 5/5 on
that same final commit, with all review threads resolved.

Previous implementation verification:

- `pnpm -r typecheck` and `pnpm build` passed.
- New PostgreSQL worker suite: 12 tests passed. Covers quiet configured
state, verified history, writes, outer commit and rollback, retry
deadlines, restart, drain, deferred orphan buffers, spool failure,
pagination, provider uncertainty, shutdown, and the idle run-reaper
path.
- Worker and environment runtime suites after review fix: 183 tests
passed. A new assertion reproduced buffer starvation before the fix.
Updated transaction fault injection and isolated simulated-instance
database handles. Server typecheck also passed after the fix.
- Run-recovery suites: 58 tests passed. Pending-cleanup suite: 38 tests
passed after updating the logging fixture to seed real orphan recovery;
an idle reaper no longer invokes cleanup.
- Stop and workspace-recovery integration selection: 268 tests passed;
two live-provider tests skipped by their environment gate. This overlaps
the worker suite and is not an additional unique-test count.
- Started the full local `pnpm test:run`, then stopped that older
duplicate run after CI found the fixture mismatch. The corrected 38-test
suite passed locally. The full sharded [CI
matrix](https://github.com/paperclipai/paperclip/actions/runs/39678174479)
passed on final commit `fc0ff3c4554ca4190a8b0b81ec06ef7065b8ad2c`,
including build, typecheck, runner, server, workspace, and E2E checks.
This is not a claim that the local full run completed.

## Risks

- Missing a future producer signal could delay discovery until restart.
The producer audit is documented in `doc/idle-environment-cleanup.md`.
Independent SQL writers and another server process are outside the
existing process-local notification contract.
- Historical candidates blocked by ownership or provider uncertainty
keep a five-minute retry until they can be verified. Provider
destruction, claim fencing, Stop retention, and workspace safety remain
controlled by their existing services.
- This does not remove orphan-active discovery or legacy shutdown
settlement from run recovery. Active custom-image expiry is also still
periodic. External wake coverage does not change.
- Local tests prove these workers become quiet and recover. They do not
establish deployed instance suspension or fleet cost savings.

## Model Used

OpenAI Codex (GPT-6), with code execution and reasoning. The exact
serving model ID and context size were not exposed.

## 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-10-24 09:04:49 -07:00
Nicky Leach e5c9e60c5a Avoid session resets when reusing default task directories (#17419) canary/v2026.1024.0-canary.11 2026-10-24 08:26:57 -07:00
Nicky Leach f7716ec65b Allow exact warm-state reads during retained task retirement (#17418) 2026-10-24 08:26:48 -07:00
Nicky Leach 6b4bdc515b Fix GitHub ownership checks for warm plugin tasks (#17417) 2026-10-24 08:26:28 -07:00
DottaandPaperclip 661f33f994 fix(evals): verify stored gateway request coverage (#17432)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Product E2E campaigns retain a request ledger and send native traces
and gateway logs to Braintrust.
> - Native trace verification did not prove that each charged gateway
request had a stored log row.
> - A hosted probe showed that `x-bt-span-id` identifies the row `id`,
while the stored `span_id` differs. Propagated campaign metadata was
absent.
> - This pull request joins retained request receipts to the correct
Braintrust rows during scheduled finalization.
> - Operators can see missing request coverage without changing costs,
releasing holds, or rerunning agents.

## Linked Issues or Issue Description

Refs #17354 and #17371.

**Describe the bug**

A campaign could pass native trace and experiment indexing checks while
gateway request logs remained unverified. The runbook also claimed that
propagated metadata made a request searchable after cancellation before
response headers. The hosted gateway did not preserve that metadata in
retained evidence.

**Steps to reproduce**

Inspect a retained gateway response header and query project logs by its
`x-bt-span-id`. The matching record uses that value as `id`, not
`span_id`. Compare every model reservation with its saved response
receipt, including setup and canceled work.

**Expected behavior**

Verify each request against the stored row and report absent or
conflicting evidence. Keep trace coverage separate from final usage and
invoice reconciliation.

## What Changed

- Added a bounded, read-only gateway index verifier. It matches row IDs,
provider and model, and retains the actual span and trace IDs.
- Included setup, failed and canceled requests in coverage. Missing
receipts or row IDs, duplicate attribution, conflicting records and
evidence changes prevent verification.
- Added a separate scheduled gateway coverage gate and private receipt.
Native trace and experiment checks remain independent.
- Shared the existing bounded Braintrust query reader. Queries select
only identity fields, with at most 100 IDs per batch, three rounds and a
45-second deadline.
- Corrected the runbook's metadata guarantee and documented the
historical `gatewaySpanId` receipt field.

## Verification

- `pnpm test:e2e:runner:unit`: 4,120 passed, two existing skips; all 137
Node contracts passed.
- Final repair: 54 focused tests passed, including
scheduled-finalization coverage and all five optional identity fields.
Each conflicting-field regression failed before the fix; absent or
matching partial metadata remains supported.
- `pnpm test:e2e:runner:typecheck`: passed after rebuilding stale local
plugin SDK declarations. The initial stale-declaration failure remains
recorded in the validation logs.
- `git diff --check`: passed.
- Read-only live verification of retained evidence: all ten local Codex
requests and the one canceled drain-probe request matched stored rows.
The hosted Claude campaign matched eight of ten; its two pre-header
cancellations correctly remained incomplete.
- The first hosted lookup exceeded the 1 MiB read limit when selecting
full metadata. The final identity-only projection passed the same
lookup.
- Original journals stayed byte-identical. These checks made zero model
calls and did not change provider data or release holds.
- Full repository CI passed on
`957ad0b684b80267d230fc4fb56d4f002ef089ad`: [run
40105676630](https://github.com/paperclipai/paperclip/actions/runs/40105676630),
including typecheck, tests and build. Both current-head automated
reviews scored 5/5 with no open findings.
- The superseded initial CI run was canceled after the repair push to
release the queue; its completed results remain available. No failed job
was retried on the final commit.

## Risks

Missing gateway IDs now fail scheduled trace coverage. A failed or
canceled call may have no stored row, so this can require investigation
even if product assertions pass. Indexing can lag; bounded query retries
do not retry model work. Stored identity does not prove final billing or
gateway span nesting under native attempt traces. Private identifiers
remain in private evidence only. Recurrence and billing settings are
unchanged.

## Model Used

OpenAI Codex, GPT-6 family. The runtime does not expose a more specific
model ID or context-window size in this session. Used reasoning,
repository tools, local code execution and read-only API 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
- [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-10-24 08:23:57 -07:00
DottaandPaperclip f66d590899 fix(ui): open the new-task form immediately (#17041)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - People use New task to send work to an agent.
> - The board currently downloads the task dialog code after the first
click.
> - This shows a loading window before people can enter their request.
> - The form already loads agent and project choices after it opens.
> - This pull request loads the task dialog code with the board.
> - People can start writing immediately and keep their text as choices
arrive.

## Linked Issues or Issue Description

Related change: #16849. Commit
`fa50cab5685b9cdbb26ad9c610cc5b6c373d3ff4` added the deferred dialog and
its loading window on October 19, 2026.

**What happened?**

The first New task click shows “Loading new task…” while the browser
downloads the dialog code.

**Expected behavior**

The form opens on click. Agent and project choices load in place. Text
entered during those requests stays in the form.

**Steps to reproduce**

1. Open the board in a fresh browser session.
2. Select New task before its code is cached.
3. Observe the loading window before the form appears.

**Paperclip version or commit**

Source baseline: `8b4ab65b1c3d863d282752e83307943720a5c85c`.

**Deployment mode**

The browser code applies to local and hosted boards. Verification used a
source checkout.

## What Changed

- Import NewIssueDialog with the board. Keep the existing mount and
draft lifecycle.
- Add a first-open test that fails if the form waits for an asynchronous
render.
- Cover text entry during delayed agent and project requests and
defaults that arrive later.
- Document the behavior and its verification recipe.

## Verification

- Regression proof: the first-open test failed before the import change
and passed after it.
- All 125 tests in BoardDialogs, DeferredOverlay, NewIssueDialog and
Layout passed.
- The production UI build and UI TypeScript check passed. The typecheck
passed with `NODE_OPTIONS=--max-old-space-size=2400` after the competing
test process was stopped.
- Token gates, map validation and Git diff checks passed.
- The first UI typecheck could not resolve the unbuilt local plugin SDK.
The SDK was then built.
- Full typecheck and build attempts hit the sandbox's 4 GB memory limit
when run with the full test suite. Two later UI typecheck attempts also
hit that limit during other validation. The cgroup recorded four OOM
kills in total. The full repository test attempt and a broader UI test
attempt were stopped. No complete suite result is claimed. Serial
repository typecheck reached the missing Rust `cargo` executable. Full
CI passed on head `9dde4bc`; local limits remain recorded here.
- First CI run at `e5a99b8`: typecheck, build, all unit-test shards and
seven browser shards passed. Browser shard 2 failed at
`chat-adapters-ui-providers.spec.ts:185`, waiting for Slack setup
Success after reload. The saved page showed the working connection and
Continue button. The cause is not confirmed; this failure is retained
for follow-up. [Failed
job](https://github.com/paperclipai/paperclip/actions/runs/39522214527/job/118968542923).
- Removed the added import comment after Greptile review. Head `9dde4bc`
changes only that comment. The review thread is resolved. Fresh CI
passed on head `9dde4bc`. Greptile reviewed that exact head at 5/5 with
no open findings. All eight browser shards passed, including the earlier
Slack failure. The earlier failure remains unexplained; the only
intervening code change removed a source comment. A passing retry
establishes recovery, not its cause.
- Component tests use mocked directory requests. No deployed latency or
browser persistence claim is made.

Latest CI: [run
39523376437](https://github.com/paperclipai/paperclip/actions/runs/39523376437).
Reviewed head: `9dde4bc0caa3556e315a2fc197268b7c9879efa4`.

## Risks

- Task dialog code is part of board startup again. This adds its
dependencies to the initial code graph. No startup latency benchmark is
claimed.
- Other creation dialogs retain their existing lazy imports and loading
behavior.

## Model Used

OpenAI Codex, GPT-6, with reasoning, repository tools and code
execution. The runtime does not expose a more precise 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 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>
canary/v2026.1024.0-canary.10
2026-10-24 08:15:35 -07:00
DottaandPaperclip 87bf89756e fix: support Daytona runner upgrades and ignore obsolete Claude options (#17361)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Existing Claude and Codex agents can upgrade to Paperclip Runner.
> - A Daytona computer needs platform qualification before Runner
becomes its default.
> - The upgrade notice required that default, so it hid the action that
could start qualification.
> - Saved Claude turn limits also caused conversion to fail even though
Runner can run without them.
> - This pull request verifies the computer on click and ignores
obsolete Claude options with a warning.
> - Agents retain their model, account, environment, instructions, and
permission policy.

## Linked Issues or Issue Description

Related: #17122.

**What happened?**

A legacy Claude agent on a Daytona snapshot had no upgrade button until
its environment had platform qualification. A saved `maxTurnsPerRun:
1000` then caused conversion to return HTTP 422.

**Expected behavior**

The upgrade action verifies a supported Daytona computer and converts
the agent. Unsupported tuning options produce a visible warning and do
not block execution.

**Steps to reproduce**

1. Create a legacy Claude agent with `maxTurnsPerRun: 1000` on an active
Daytona snapshot.
2. Open Runtime before the environment has platform qualification.
3. Try to upgrade to Paperclip Runner.

**Paperclip version or commit**

After #17122, reproduced against `9c442cf48e`.

## What Changed

- Offer upgrade on probe-capable Daytona snapshots and immutable sandbox
images. Perform the public environment probe only after a click. Save
after fresh adapter discovery confirms support.
- Resolve the actual computer for explicit, instance-default, and hidden
managed environments. Preserve pending edits and reject stale probe
results.
- Ignore Claude `maxTurnsPerRun`, `chrome`, `warmHandleIdleMs`, and
`acpWarmHandleIdleMs`. Return structured warnings from create, hire, and
update. Show warnings in Runtime and after a save.
- Keep validation for commands, credentials, filesystem, network, and
permission settings.
- Retain the legacy turn limit in the existing live conversion fixture.
Update regression tests, upgrade stories, and capability maps.

## Verification

- Red: the old route returns HTTP 422 for the saved turn limit; the old
form hides the Daytona upgrade notice.
- Green: 417 focused server and form tests pass. A further 188 tests
pass after the review fixes. Qualification coverage includes failed
probes, unsupported targets, changed settings, managed defaults, unsaved
environment variables, and forced Kubernetes targets.
- `pnpm -r typecheck` and `pnpm build` pass. Token gates, map checks,
map tests, and Storybook catalog checks pass.
- Real local Claude Opus 5.5 conversion and task pass, with the
downloaded artifact checked against seeded source bytes, SHA-256, and
Git commit.
- Real browser upgrade and full Claude Opus 5.5 task on a disposable
named Daytona snapshot pass at `b81cec7d8c`. The saved agent retains its
model, high effort, credentials, timeout, and grace settings while
dropping the turn limit with a visible warning. Exactly one successful
run completed the task. The downloaded artifact matched independently
seeded source bytes, SHA-256, and Git commit; the source was unchanged,
no interactions remained pending, and the delivery link survived a
browser reload. The snapshot used current runtime assets built from
public source. The supplied Daytona account could not access the
original staging snapshot, so this verifies an equivalent local fixture
rather than that exact snapshot. Disposable test resources were cleaned
up.
- Earlier live attempts remain recorded: an image-based sandbox creation
failed, and the first named-snapshot probe returned failure with
unresolved provisional cleanup identity. Later named-snapshot probing
and the task succeeded; the initial creation failures are not attributed
to this PR or declared resolved. A separate attempt using the Oct 18
cached runtime failed the current provider-pack compatibility check;
rebuilding the runtime from matching public source removed that
mismatch. Owned resources were identified and cleaned up, with
diagnostic databases retained.
- 54 checks passed at `b81cec7d8c`, with no failed checks; Greptile and
Paperclip Review both scored 5/5, with both review threads resolved.
Master advanced during the live proof, so the branch was rebased with
both sides of additive import/test conflicts retained. On the rebased
head `2b74222b94`, 472 focused tests, UI typecheck, and UI build pass.
All required CI gates pass; Greptile and Paperclip Review both score 5/5
on this head, with no unresolved review threads. The PR is mergeable.
The serial local `pnpm test:run` was stopped after the CI test lanes
passed; that local run is not claimed as a pass.

## Risks

- An explicit upgrade can allocate a temporary probe sandbox and take
several minutes. Probe failures leave the agent on its existing runner.
- Ignored options no longer enforce a turn cap, Chrome integration, or
legacy idle-time tuning. The UI warns about each active ignored option.
- Unsafe or ambiguous execution settings still fail validation. This
change does not weaken environment qualification or runtime artifact
checks.

## Model Used

OpenAI Codex, GPT-6, with reasoning, code execution, and browser tools.
The exact deployment 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-10-24 07:59:18 -07:00
DottaandPaperclip 52a66af29d perf(evals): tighten measured campaign resource ceilings (#17381)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Product E2E checks real provider behavior under a campaign budget.
> - Its request and trace limits also determine the operating-cost
forecast.
> - The initial limits greatly exceed the measured marker and feedback
cases.
> - This pull request lowers those defaults while keeping the same
selected cases and full model context limits.
> - The benefit is a smaller bounded resource envelope with explicit
quota failures and readable historical evidence.

## Linked Issues or Issue Description

Refs: #17365
Refs: #17371

**What existing behavior does this improve?**

The resource ceilings for bounded Product E2E campaigns and their OTLP
relay.

**Current behavior**

Daily and weekly campaigns allow 60 and 120 model requests. The native
relay defaults to 256 MiB of expanded source and 16,384 records per
attempt. Both October 23 two-case qualification campaigns used ten model
requests. The largest native attempt used 6,531,801 expanded source
bytes and 2,455 records.

**Proposed behavior**

Use 30 daily and 60 weekly model requests. Default native exports to 16
MiB of expanded source and 4,096 records. Preserve the older explicit
relay maxima so prior receipts remain valid and deliberate controller
overrides still work.

**Reason and benefit**

The smaller limits leave headroom for the measured cases and reduce the
maximum source traffic in the operating forecast. They do not change the
daily or weekly dollar cap, model context ceiling, per-request
reservation, selected cases, or attempt policy.

**Breaking changes**

A new bounded attempt that exceeds the smaller default trace allowance
receives a quota error. It cannot silently omit required evidence.
Existing receipts remain readable. No paid recurrence is enabled by this
change.

## What Changed

- Set scheduled model request caps to 30 daily and 60 weekly.
- Add smaller native relay defaults while preserving the existing
explicit maximum and historical receipt validation.
- Exercise default count and expanded-source exhaustion through the HTTP
relay. Keep a regression for larger historical receipt policies.
- Document the measured basis, default capacities, and failure behavior.

## Verification

- `pnpm test:e2e:runner:unit`: 4,095 Vitest tests passed, two existing
skips; 137 Node contract tests passed.
- `pnpm test:e2e:runner:typecheck`: passed.
- `git diff --check`: passed.
- The relay test accepts two 3-MiB log payloads, rejects the third
before upstream dispatch, and retains its quota receipt.
- Read-only replay of the prior real hosted campaign preserved all five
ingestion receipts and 123,044 payload bytes exactly. Its accounting
journal remained byte-for-byte unchanged; zero provider calls.
- Full repository typecheck, tests, build and reviews are pending hosted
CI. No paid model call was made for this change. Live qualification of
the smaller defaults remains open.

## Risks

- More verbose future cases may exhaust the new defaults. Such a failure
remains visible and requires evidence before increasing a limit.
- Source-byte ceilings are not Braintrust invoice units. Operating-cost
qualification and reconciliation remain separate requirements.
- Manual controller overrides can still use the earlier larger maxima;
their cost audit must account for the limits actually recorded in the
receipt.

## Model Used

OpenAI Codex, GPT-6 family, with reasoning, code execution and tool use.
The host does not expose an exact model build identifier or context
window size for 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
- [ ] 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>
canary/v2026.1024.0-canary.9
2026-10-24 07:38:12 -07:00
515ff0f284 fix(server): report which S3 call failed in storage error events (#17424)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The server keeps attachments and other files in a storage provider.
Hosted instances use the S3 provider.
> - Sentry received `SignatureDoesNotMatch` errors from the S3 provider
on two staging hosts.
> - The events have only AWS SDK stack frames. They do not show which S3
call failed, the HTTP status, or the S3 request ID.
> - Without these facts, we cannot find the cause or make a safe fix.
> - This pull request records a small, fixed-shape failure record on the
S3 error and sends it with the Sentry event.
> - The benefit is that the next event shows the failed operation and
the provider request ID, and no private data goes to Sentry.

## Linked Issues or Issue Description

**What happened?**
The S3 storage provider threw `SignatureDoesNotMatch` four times on
staging. The Sentry events do not identify the S3 operation or the
caller.

**Expected behavior**
A storage provider failure in Sentry shows which storage call failed and
the provider's correlation fields.

**Steps to reproduce**
1. Configure the S3 storage provider against an endpoint that rejects
the request signature.
2. Read, write, check, or delete a stored file.
3. Look at the Sentry event. It has no first-party stack frame and no
operation name.

No public GitHub issue exists. I searched open and closed pull requests
for `SignatureDoesNotMatch` and storage failure diagnostics. No pull
request covers this.

## What Changed

- Add `server/src/storage/failure-diagnostics.ts`. It keeps a frozen,
fixed-shape record for each failed storage error in a `WeakMap`.
- Add `server/src/storage/s3-failure.ts`. It wraps each S3 call and
records the operation, error code, HTTP status, request ID, and SDK
attempt count. It reads only data properties. A value that does not
match its bounded format becomes `unknown` or `null`.
- Wrap `PutObject`, `GetObject`, `HeadObject`, `DeleteObject`,
`CreateMultipartUpload`, `UploadPart`, and `CompleteMultipartUpload`.
- In `captureException`, add the `storage_operation` and
`storage_error_code` tags and a `storage_failure` context to the event
of that error only. The original error and its default grouping do not
change.
- Document the new context in `doc/observability.md`.

## Verification

- `pnpm exec vitest run
src/__tests__/storage-s3-failure-diagnostics.test.ts
src/__tests__/sentry.test.ts` (in `server/`): 37 passed, 5 skipped. The
skipped tests are the existing real-SDK tests, which need an environment
flag.
- `pnpm exec vitest run src/__tests__/storage-s3-multipart.test.ts
src/__tests__/storage-local-provider.test.ts`: 8 passed.
- The new tests use a local HTTP server that returns a real S3
`SignatureDoesNotMatch` XML body with a `StringToSign` field. The tests
make sure that the record has the operation, code, status and request
ID, and that it does not contain the private string.
- I typechecked the changed source files with a scoped `tsc` project.
They have no errors. I could not run the full `pnpm -r typecheck` and
`pnpm build` in my sandbox: it has a 4 GiB memory limit and no `cargo`
for the runner binary. CI runs the full checks.

## Risks

- Low risk. The change adds a `try`/`catch` that records data and throws
the same error again. Storage behavior, not-found handling, and HTTP
responses do not change.
- Secret-sensitive routes replace the error before reporting, so those
events do not get the record. This is intended.
- The record never contains the bucket, object key, endpoint,
credentials, error message, or provider response body.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic. Used through Claude
Code with tool use (shell, file edit) and extended reasoning.

## Checklist

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

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

Co-authored-by: devinfoley <139239+devinfoley@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
canary/v2026.1024.0-canary.8
2026-10-24 05:08:56 -07:00
5e26b733b9 fix(server): finish workspace operations when the log write fails (#17415)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Before a run starts, Paperclip prepares the execution workspace (for
example `git worktree add`). Each step is a workspace operation. The
workspace operation recorder writes it as a row and as a log file on
disk.
> - When an operation fails, the recorder writes the error to the log
file, then sets the row to `failed`.
> - If that log write also fails (for example ENOSPC when the disk is
full), the recorder throws before it updates the row.
> - The row stays `running` with no `finishedAt`. The log write error
also replaces the real error of the operation.
> - This pull request makes the failure-path log write best-effort, so
the row always reaches `failed` and callers get the correct error.
> - The benefit: on a full disk, the operator sees a terminal operation
with the real failure reason. Operations do not stay "running" for ever.

## Linked Issues or Issue Description

No public issue exists. Bug description:

**What happened?**
A run failed during workspace setup on a host with a full disk (`ENOSPC:
no space left on device, write`). The stack shows the error in the
workspace operation log append inside `recordOperation`. In the failure
path, the recorder calls the log append before it updates the row.
Because the append threw again, the row stayed `running` with no
`finishedAt`. When the operation itself failed (for example a git
error), callers got the ENOSPC log error and not the git error.

**Expected behavior**
A failed workspace operation always reaches `failed` with `finishedAt`
set. Callers get the operation's own error. A log write failure must not
hide that error.

**Steps to reproduce**
1. Make the operation log append throw ENOSPC (the new tests stub
`fs.appendFile` for the log root).
2. Record an operation whose `run` throws, or whose output append
throws.
3. Before this fix: the row stays `running`, and the caller gets the
ENOSPC error and not the operation's error.

**Paperclip version or commit**
`ee29e28` (current `master`).

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

Related open PRs with disk-full handling in other places (no overlap
with this code path): #5882 (run-log rotation), #16773 (backup disk
use).

## What Changed

- `server/src/services/workspace-operations.ts`: in the
`recordOperation` failure path, the stderr log append is now best-effort
(`.catch(() => undefined)`). The stderr excerpt still records the
message, because the excerpt updates before the file write.
`logStore.finalize` was already best-effort in this path.
- `server/src/__tests__/workspace-operations-reconciliation.test.ts`:
two regression tests with log appends that fail with ENOSPC:
- The output append fails after a successful run. The row reaches
`failed` with `finishedAt` and the ENOSPC message in the stderr excerpt.
The caller gets the ENOSPC error.
- The operation itself throws. The row reaches `failed` and the caller
gets the operation's own error, not the log write error.

## Verification

- Red before the fix: both new tests failed on `master`. The row stayed
`running`, and the caller got the ENOSPC error and not the git error.
- Green after the fix: `pnpm exec vitest run
src/__tests__/workspace-operations-reconciliation.test.ts
src/__tests__/workspace-runtime-control-recovery.test.ts` passes (17
tests).
- Sibling suites that use the recorder (26 files, 945 tests): all pass
except 4 tests in `workspace-runtime.test.ts`. Those 4 tests also fail
on unchanged `master` in the same sandbox, because they need the local
pnpm and worktree CLI. This change does not cause them.
- Typecheck: `tsc --noEmit` on `workspace-operations.ts` passes. The
full server `tsc` was not run locally because the 4 GB sandbox ran out
of memory. CI runs the full typecheck.

## Risks

- Low risk. The change affects only the failure path, and only the log
file write. The success path does not change: if a log write fails after
a successful run, the operation still fails with the write error.
- The log file for a failed operation can lose its last stderr line on a
full disk. The row's stderr excerpt still has the message.

## 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: Devin Foley <139239+devinfoley@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
canary/v2026.1024.0-canary.7 nightly/v2026.1024.0-nightly.0
2026-10-24 00:30:06 -07:00
ee29e2850d fix: keep GitHub guest provenance from failing tool discovery (#17340)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents reach their tools through the MCP tool gateway. Every run
starts with `tools/list`.
> - Tool discovery runs a GitHub guest sponsor probe
(`githubGuestBotConnectionForSession`) before it lists any connection.
> - The probe walked the run's identity chain before it knew whether the
task had a guest sponsored GitHub message at all.
> - When agent A works a task and the task is reassigned to agent B, B's
run chain continues A's run. The chain walk refuses with a plain
`HttpError`. The gateway returns 403 for `tools/list` with no audit
record. B loses every tool, built-in and connected, even when the
company has no GitHub endpoint.
> - This pull request makes the probe look up guest sponsored GitHub
messages for the task and agent first. When there are none, the probe
answers `null` and does not consult provenance.
> - The benefit is that a reassigned or continued task keeps its tools,
while a guest sponsored task still refuses a broken chain.

## Linked Issues or Issue Description

Fixes #17307

**What happened?**

`tools/list` on the MCP gateway returned `403 {"error":"Chat tools
cannot be inherited by another task"}` for every run whose identity
chain contained a run of another agent or another task. No audit event
was recorded. The agent received no tools at all, including unrelated
MCP connections. The company had no GitHub chat endpoint.

**Expected behavior**

A run on a task with no GitHub guest sponsor lists its tools. The guest
probe only decides whether the run must be limited to the sponsoring
bot's connection. When the task has no guest sponsored GitHub message,
the answer is "no sponsor" and discovery continues.

**Steps to reproduce**

1. Let agent A run on a task. The run gets an accepted identity context.
2. Reassign the task to agent B and let B run. B's identity context has
A's context as parent.
3. Call `tools/list` for B's run. Before this change the gateway returns
403 with the message above. The same happens when the parent context
belongs to a run on another task (see #17307).

**Call path**

`handleMcpGatewayProtocol` → `listToolsForNamedGateway` →
`listToolsForContext` → `buildToolsForContext` →
`connectionToolsForContext` → `githubGuestBotConnectionForSession` →
`chatRunSourceMessageId`, which throws `forbidden(...)`. The error is an
`HttpError`, not a `ToolGatewayHttpError`, so the route sends it as a
bare 403 and the discovery audit never runs.

**Paperclip version or commit**

`d69631b01b9c658f80bf8bed389863dce82643e1` (image `sha-d69631b`),
Docker, authenticated mode with PostgreSQL.

## What Changed

- `server/src/services/chat-github-tools.ts`:
`githubGuestBotConnectionForSession` first selects the inbound GitHub
message links on the task whose delivery carries `githubAuthority.guest
= true`, for endpoints assigned to the session agent. When there are
none it returns `null` without reading the run or its provenance chain.
When there are some, it verifies provenance as before and returns the
connection of the link that matches the verified source message.
- `server/src/services/chat-channels/run-origin.ts` is unchanged. The
cross-agent and cross-task refusals stay in place for Slack authority,
GitHub reviews and response comments, and for guest sponsored tasks.
- New test `server/src/services/chat-github-tools.test.ts` (embedded
PostgreSQL, same pattern as `run-origin.test.ts`):
- a run whose chain continues another agent, or another task, with no
guest message → `null` (fails before this change with the 403 above,
passes after);
- a guest sponsored task whose chain continues another agent → still
rejects with status 403;
- a run sponsored by a GitHub guest → returns the bot connection; the
same run with `guest: false` → `null`.

## Verification

- `pnpm --filter @paperclipai/server exec vitest run
src/services/chat-github-tools.test.ts`: 2 failed / 3 passed before the
fix, 5 passed after.
- `pnpm --filter @paperclipai/server exec vitest run
src/services/chat-channels/run-origin.test.ts
src/services/chat-github-tools.test.ts`: all passed.
- `pnpm --filter @paperclipai/plugin-sdk ensure-build-deps && pnpm
--filter @paperclipai/server exec tsc --noEmit`: clean.
- Manual: with a patched image, a run on a task that was reassigned
between two agents receives its full tool list through `tools/list` and
a `discovery` audit event is written. Before the patch the same kind of
run received the 403 and no audit event.

## Risks

- Low risk. The probe's result is unchanged for every session where it
returned a value before. The only behavior change is for sessions that
previously threw: when the task has no guest sponsored GitHub message,
they now get `null`, which is the value the old code could only have
returned for them anyway, because the final match also required a guest
link on this task for this agent.
- One extra query per discovery on guest sponsored tasks. On every other
session the run and provenance queries are skipped.

## Model Used

- Claude (Anthropic), Claude Code agent session; model id
`claude-fable-5-1`, extended thinking, tool use (shell, git, vitest with
embedded PostgreSQL).

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

https://claude.ai/code/session_01LoF4rrmx35JJqbhEpaLQ9Q

---------

Co-authored-by: Ingvar Petrov <ip@protrener.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
canary/v2026.1024.0-canary.6
2026-10-23 20:06:16 -07:00
Devin FoleyandPaperclip 1a79522a4e perf: narrow run-to-issue authorization reads (#17405)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Run pages show links to issues associated with a run.
> - The issue-link route checks the run's company and issue visibility
before it reads those links.
> - That check needs four binding fields, but the route reads the full
run.
> - Full runs can include large prompt, output, and result fields.
> - This pull request uses the existing narrow authorization read.
> - The route transfers less database data and keeps its permission
checks and response contract.

## Linked Issues or Issue Description

**Problem or motivation**

`GET /api/heartbeat-runs/:runId/issues` calls `heartbeat.getRun` for a
permission check. Its consumers only need the run ID, company ID, scope
kind, and issue ID. The full read also fetches context, output excerpts,
result data, and runtime metadata. Repeated link reads pay for that
unused data.

**Proposed solution**

Use `getRunReadAccess`, the existing four-field read from #17348. Keep
authentication, company access, company-scope authorization, run issue
visibility, and linked-issue filtering in their current owners. Keep the
existing empty response for missing and inaccessible runs.

**Alternatives considered**

A new query contract is unnecessary because the existing read provides
all required fields. A permission cache could become stale. Changing the
full run reader could affect consumers that need run details.

**Roadmap alignment**

This is a focused improvement to an existing route. It adds no feature,
route, provider, cache, or background work. Searches for run-issue
links, narrow run authorization, and `getRunReadAccess` found no open
duplicate. Related work: #17348.

## What Changed

- Change the issue-link route to use the existing narrow run read.
- Reject full run reads in the route tests. Cover readable company and
issue scopes, denied issue access, missing bindings, anonymous requests,
and the missing/cross-company empty response.
- Add a developer note about the read contract.

## Verification

- `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/activity-routes.test.ts
src/services/heartbeat/run-state.test.ts
src/__tests__/agent-live-run-routes.test.ts`: 126 tests passed.
- `pnpm build`: passed.
- `pnpm -r typecheck`: passed.
- The route tests use the real run visibility helper with only the four
binding fields. The full reader throws if a route calls it. The existing
run-state suite checks the exact four-column selection and covers
database wiring with disposable Postgres.
- `git diff --check`: passed. Reviewed the committed diff for secrets
and private identifiers before push.
- Apex review: 5/5 on exact head
`888a4ce9ea88bb2b284eb5e28fafc01b6f48b386`, completed successfully, with
zero review threads.
- Full current-head CI passed: 54 successful checks and two expected
skips. [Workflow attempt
2](https://github.com/paperclipai/paperclip/actions/runs/40051399577/attempts/2).
[Initial serialized shard
failure](https://github.com/paperclipai/paperclip/actions/runs/40051399577/job/120744710081):
the first company-history case in unchanged
`agent-live-run-routes.test.ts` hit its 15-second timeout; the shard had
391 passing tests and one failure. That file, its agent route, the
heartbeat state reader, and the service export are unchanged from base.
The five company-history cases pass on unchanged base
`9fb28d06ffebbb615f413765078aeeb7c4291311` (9.12s suite) and this head
(8.29s suite). They also passed in the 126-test focused run. The timeout
was not reproduced locally; its cause is not established. Only the
failed shard was retried on the same head. It passed in attempt 2. This
establishes recovery; the initial timeout remains in this record and its
cause is unproven.
- The prior local full-suite failures and baseline triage remain
documented in #17384. No new local full-suite pass is claimed here. This
change uses focused local tests and the full current-head CI gate.
- No hosted user journey, staging deployment, production deployment, or
billed savings is claimed for this follow-up.

## Risks

- Run visibility depends on the canonical `scopeKind` and `issueId`. The
existing helper returns those same fields. It does not use prompt or
result data for access decisions.
- Linked issues still pass the existing SQL visibility filter. Missing
and cross-company runs still return `200 []`. Anonymous requests still
fail before the run read.
- The database query count stays the same. The change reduces data
selected for this authorization read.
- No schema or API response changes.

## Model Used

OpenAI Codex, GPT-6 family. The exact deployment model ID and context
window are not exposed in this session. Used reasoning, repository
tools, and code execution.

## Checklist

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

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-23 19:59:21 -07:00
Devin FoleyandPaperclip 70de7d49e9 Migrate standalone nested clones onto the shared project clone (#17407)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Each task runs in a git worktree. Extra project repositories sit in
the task under `.paperclip-repositories/`.
> - #17382 made new nested checkouts git worktrees of one shared clone
per project repository. Existing standalone nested clones stayed as full
clones.
> - Long-lived tasks keep those clones for a long time. Many tasks hold
the same large repository, so most of the disk is duplicate `.git` data.
> - #17369 added a locked and verified move of isolated workspaces onto
the shared clone, with a reaper sweep behind a setting.
> - This pull request adds the same kind of locked and verified
conversion for standalone nested clones in open tasks, and runs it from
the same reaper path behind a new setting.
> - The benefit is that the duplicate clones go away without losing any
state or local history, and the disk stops filling.

## Linked Issues or Issue Description

Refs #17382 #17369. Bug report described in-PR (bug report template).

**What happened?**
On one instance, 44 open task worktrees in one project each hold a
standalone nested clone of the same large repository. Together they use
about 25 GB. About 16 GB of that is duplicate `.git` data. Another
project has 33 smaller clones. The clones are removed only when their
tasks expire, and many tasks stay open for a long time. The disk keeps
filling.

**Expected behavior**
Existing standalone nested clones in open tasks become worktrees of the
project's shared clone, with the same state and local history, and the
standalone clones are deleted.

**Steps to reproduce**
1. Prepare a task with a nested project repository before #17382, or
without a shared clone, so that `.paperclip-repositories/<name>` is a
standalone clone.
2. Create the project's shared clone, as #17382 does for new tasks.
3. Observe that the old checkout stays a full clone until the task
expires.

**Paperclip version or commit:** `master` at 8549ecaf9a.
**Deployment mode:** any.
**Agent adapter(s) involved:** not adapter-specific (core).

## What Changed

- New `server/src/services/nested-clone-migration.ts`. It converts one
standalone nested clone in an open execution workspace into a worktree
of the shared clone:
- It holds the source lock and workspace lifecycle lock (as git cache
expiration does), the per-shared-clone lock from #17382, and the
expiration lock files (`index.lock`, `HEAD.lock`, current branch ref
lock) in the clone.
- It requires the #17369 no-live-consumer eligibility (no live runs,
leased finalizations, unreleased leases, running services, or reopen
flag). It checks again before the swap.
- It identifies the repository by managed checkout name, project
workspace rows and the origin URL, and it requires a verified shared
clone.
- It brings local history into the shared clone under the checkout's own
`paperclip/<id>/` prefix. The current branch becomes
`paperclip/<id>/<branch>`, tracking `origin/<branch>`. Unpushed local
branches become `paperclip/<id>/<branch>`. Unpushed tags become
`paperclip/<id>/tags/<tag>`. A restore point carries the commits, the
`HEAD` reflog and `ORIG_HEAD`.
- It stages the worktree at `<name>.clone-migrating`, applies the
restore point, copies the `HEAD` reflog and `ORIG_HEAD`, and moves
regenerable dirs, untracked `.env` files and
`.claude/settings.local.json` by rename. Old and new must then match
exactly (snapshot, history, branch, created refs, moved items).
- Swap: it renames the clone to `<name>.clone-standalone`, runs `git
worktree move` and `git worktree repair`, updates a saved restoration
recipe that lists the checkout, and then renames the old clone to trash
and deletes it.
- A `nestedCloneMigrationPending.<name>` metadata record makes recovery
possible. A crash before the swap rolls back (moves items back, removes
the staged worktree and the created branches). A crash after the swap
rolls forward.
- Skip reasons (nothing changes): `live_or_unresolved_consumer`,
`unknown_managed_checkout`, `local_source_checkout`, `foreign_origin`,
`no_shared_base`, `shared_base_repo_mismatch`,
`unsupported_base_git_config`, `nested_clone_has_stash` (stashes are
skipped, because `refs/stash` would be shared by every worktree),
`nested_clone_has_worktrees`, `nested_clone_has_hooks`,
`unsupported_local_git_config`, `unsupported_local_git_state`,
`unsupported_info_exclude`, `unsupported_nested_clone_refs`,
`unsupported_index_flags`, `unique_branch_reflog_commits`,
`nested_clone_ref_conflict`, `snapshot_mismatch`, `history_mismatch`,
restore point limits, and `nested_migration_recovery_conflict`.
- Reaper: `sweepTerminalWorkspaces` runs the conversion for open
workspaces when `PAPERCLIP_NESTED_CLONE_MIGRATION_ENABLED=true` (default
off). `PAPERCLIP_NESTED_CLONE_MIGRATION_DRY_RUN=true` only reports. Each
attempt logs `event: "execution_workspace.nested_clone_migration"` with
outcome, reason, branch, candidate bytes and reclaimed bytes. There is
no new timer. A retry deadline is registered on the existing reaper
deadlines only for a pending record or a transient skip (live consumer,
busy lock, changed row). A workspace is attempted again at most hourly.
A workspace without standalone clones costs one directory listing.
- `isolated-repository-rehome.ts`: I exported the shared pieces
(eligibility, locks, safe base config check, transfer plan, history
boundary, snapshot and history keys, item moves, rollback and
verification) instead of copying them. Re-home behavior does not change.
- `nested-repository-worktrees.ts`: I exported the stale registration
cleanup and added the `moveNestedRepositoryWorktree` (move and repair)
and `trackNestedWorktreeUpstream` helpers.
- Docs: `doc/recoverable-disk-cleanup.md` (new section about the
conversion) and `doc/DEVELOPING.md` (settings).

## Verification

- `cd server && pnpm exec vitest run
src/__tests__/nested-clone-migration.test.ts`: 19 passed. It uses
embedded Postgres and real git repositories, and covers:
- Conversion with an unpushed commit, a second branch with unpushed
commits, a lightweight tag and an annotated tag, staged and unstaged
edits, a deleted file, an untracked file with mode 0600, an ignored
file, `node_modules`, `.env` and the settings file. The result is a
worktree of the shared clone (`--git-common-dir`), with the same HEAD,
status, staged content and reflog, the owned branch tracking
`origin/main`, and the history under the task's prefix. No other branch
in the shared clone changes. The old clone is gone and reclaimed bytes
are reported.
- Skips with nothing changed: stash, live run, foreign origin, unknown
checkout, no shared clone, local excludes, and a change between capture
and verification (hook).
- Two tasks convert concurrently against one shared clone. The test
holds the shared-clone lock and shows that neither conversion advances.
Then they run one at a time (peak 1), and each keeps its own branches.
- Crash recovery at `afterAside`, `afterMove` and `afterFinalize` (roll
forward), and before the swap (roll back, then convert). A dry run
reports the pending record and changes nothing.
- Expiration and restore of a converted checkout work afterwards.
Unpushed branches in a converted checkout block expiration
(`unpushed_managed_checkout_branches`).
- Reaper: disabled by default; dry run changes nothing and registers no
deadline; a live run registers a retry deadline that survives a second
sweep; enabled conversion leaves no deadline.
- Commits that only the clone's `origin` remote-tracking refs hold are
kept as `paperclip/<id>/remotes/origin/<branch>`.
- A pending conversion is finished after the workspace is archived, and
forgotten (keeping unpushed branches) when the worktree is gone.
- A filter planted in the shared clone right before the worktree add is
refused (`unsupported_base_git_config`) and never runs.
- Only checkouts named after configured project repositories are
considered; at most 8 per sweep, pending first, rotating (unit test).
- Related suites (run before review fixes; the nested, re-home and
execution workspace service suites were rerun after them): `pnpm exec
vitest run` on execution-workspaces-routes,
execution-workspaces-service, git-reproducible-managed-checkouts,
git-restore-point (all 5 files), git-workspace-contents,
git-workspace-expiration, git-workspace-expiration-eligibility,
heartbeat-git-restore-point-resume, heartbeat-project-repositories,
isolated-repository-rehome, nested-clone-migration: 15 files, 393 tests
passed.
- `cd server && pnpm run typecheck` (`tsc --noEmit`): passes.
- Mutation checks. Each mutation makes the targeted test fail, and I
reverted each one:
  - Remove both eligibility checks: the live run test fails.
- Remove the snapshot comparison: the capture/verify change test fails.
  - Disable roll forward after the swap: the recovery tests fail.
- Drop the `paperclip/<id>/` prefix from transferred branches: the
conversion and concurrency tests fail.
  - Remove the shared-clone lock: the concurrency test fails.
- Drop remote-tracking refs from the plan, the deadline restore during
backoff, archived recovery, or the config check before the worktree add:
each targeted test fails.
- Not run: the full server suite and a run on a live instance.

## Risks

- The setting is off by default. Enable a dry run first and read the
`execution_workspace.nested_clone_migration` logs.
- A clone that a run writes to between verification and the swap rename
can lose those writes. This window is the same as in #17369. Locks, the
eligibility check and the git lock files make it small.
- A crash in the short time between the rename aside and `git worktree
move` can collide with a run that recreates `<name>`. In that case the
conversion stops with `nested_migration_recovery_conflict` and keeps
both copies. Expiration keeps such a workspace because of the `.clone-`
names.
- Stashes, annotated tag messages (the tagged commit is kept as a
branch), custom hooks, custom local config and per-branch reflog-only
history are not converted. Clones with them are skipped with a named
reason. Only unpushed branches and tags get a name in the shared clone.
A pushed local branch name is not kept, but its commits stay on the
remote.
- The conversion fetches the shared clone first. A failed fetch is
ignored, and then the restore point can be larger.
- Pushing from a converted checkout needs an explicit destination, as
for all #17382 nested worktrees.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), 1M context, through Claude Code
with tool use (shell, file editing, 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
- [ ] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
canary/v2026.1024.0-canary.5
2026-10-23 19:52:07 -07:00
Nicky Leach aa0037a137 Support Codex warm plugin task reuse and recovery (#17390) canary/v2026.1024.0-canary.4 2026-10-23 19:14:14 -07:00
9fb28d06ff docs(release): stable notes for the 2026.1023.0-beta.0 soak (v2026.1026.0, since v2026.1023.0) (#17388)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The release process promotes a soaked beta to stable. The stable
GitHub Release body comes from `releases/beta/v<beta-version>.md` on
`master`
> - The `draft_stable_notes` job pushed a commit-log skeleton to this
branch when `2026.1023.0-beta.0` published (workflow run 40013393296)
> - The skeleton is grouped commit subjects. It is not in release-notes
voice and it does not tell a self-hoster what to do before an upgrade
> - Stable `v2026.1023.0` was published from `2026.1012.1-beta.0` while
this draft was open, so the previous stable is now `v2026.1023.0`, not
`v2026.1012.0`. The notes must describe only what changed after it
> - This pull request replaces the skeleton with curated notes. Each
claim traces to a pull request in `v2026.1023.0..d69631b0`
> - The benefit is that the earliest stable promotion (v2026.1026.0)
finds finished notes on `master` on time, and readers of the
`v2026.1023.0` changelog are not told about the same features twice

## Linked Issues or Issue Description

**Issue type**

Missing content

**Where is the issue?**

`releases/beta/v2026.1023.0-beta.0.md` is the stable-notes file for the
`2026.1023.0-beta.0` soak. It exists only on this machine-owned branch
and only as the auto-generated skeleton.

**What's wrong?**

The stable promotion reads this file from `master` and publishes it as
the GitHub Release body. Until this branch merges, the stable preflight
has no notes to resolve. The skeleton lists 715 raw commit subjects with
nested PR summaries, 200 of which already shipped in `v2026.1023.0`. It
does not call out the changes that need operator attention before the
upgrade.

**Suggested fix**

Merge the curated notes so the notes invariant holds for the
v2026.1026.0 promotion. Correct the `> Released:` date in a follow-up if
the promotion date slips.

## What Changed

- Replaced the skeleton in `releases/beta/v2026.1023.0-beta.0.md` with
curated stable notes for v2026.1026.0 in the same layout as
`releases/v2026.1023.0.md`: overview, Breaking Changes, Highlights,
Fixes, Improvements, Upgrade Guide, Contributors
- The notes are delta-framed against `releases/v2026.1023.0.md`.
Features that stable already describes (private tasks, model gateways,
Slack self-setup, the two-screen GitHub review bot, the Costs ledger,
MCP assistant edits, the Operator preset change, OpenAI Dot, the primary
agent, decision models, agent identity keys) are not repeated. Bullets
that mixed shipped and new work were split so only the new work remains
- Breaking Changes lists the removal of the Pipelines experiment
(#16834), Sentry run-failure reporting off by default (#16300), the
bounded run history list (#16286), and the `--yes` flag on
`instructions-file:delete` (#11504)
- Highlights cover the Paperclip Runner as the default for new agents,
GitHub Copilot as a native harness, project repositories as skill
sources, prompt injection prevention, OTLP observability, the unified
Google Workspace connection, self-cleaning workspaces with restore
points, bulk roster edits, Grok Bot and Muse, Speko, and the Slack,
GitHub bot, Apps catalog, and Audit commentary follow-ups
- Upgrade Guide enumerates migrations `0334` through `0374` with
one-phrase effects, including the constraint rewrites in `0340` and
`0361` and the index in `0360`, the new environment variables with
defaults, the new default-off experimental settings, the
`commentary:view` grant and its startup re-seeding, the Docker image
entrypoint change, and the API changes
- Release and CI internals, smoke specs, canary tooling, Paperclip Cloud
work, dependency bumps, and test-only changes are left out

## Verification

- Beta publish is complete: npm dist-tag `beta` is `2026.1023.0-beta.0`,
tag `beta/v2026.1023.0-beta.0` points at
`d69631b01b9c658f80bf8bed389863dce82643e1`, and the `publish_beta`,
`smoke_beta`, and `draft_stable_notes` jobs in run 40013393296 all
succeeded
- Stable `v2026.1023.0` (tag at
`1b4c803816e82b0265c8422e1a804327a7ed8c54`, the `2026.1012.1-beta.0`
source commit) is an ancestor of `d69631b0`; `git rev-list --count
v2026.1023.0..d69631b01b9c658f80bf8bed389863dce82643e1` returns 515,
which matches the overview and Contributors section
- Contributors are the 26 unique GitHub logins of the commit authors in
the range, read from the GitHub compare API with `[bot]` accounts
excluded. The 21 external handles are thanked by name
- `./scripts/release.sh stable --date 2026-10-26 --print-version`
returns `2026.1026.0`
- Every `#NNNN` link in the file was checked: none is a commit subject
in `v2026.1012.0..v2026.1023.0`, and each is either a squash-merge
subject in `v2026.1023.0..d69631b0` or a pull request whose
`merge_commit_sha` lies in that range (#14180, #16356, #16401, #16403,
#16976)
- Migration numbers, setting keys, env var names, defaults, and harness
pins were checked in the code at the source commit and at
`v2026.1023.0`, not in commit subjects
- The file contains no internal ticket ids or instance-local links
- The compare view for this branch shows one changed file and no source
changes

## Risks

- Low risk: a single markdown file, no source changes. If the promotion
date slips past 2026-10-26, the `> Released:` line and H1 need a
one-line update before the stable dispatch. The beta-keyed filename
makes that re-date harmless
- The notes assume the next stable is promoted from this beta. If a
later beta is promoted instead, its own notes file supersedes this one

## Model Used

- Claude (Anthropic), model ID `claude-fable-5-1` (Claude Fable 5.1),
extended thinking enabled, tool use via 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: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: devinfoley <139239+devinfoley@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Bender (Fable) <bender-fable@users.noreply.github.com>
canary/v2026.1024.0-canary.3
2026-10-23 18:35:05 -07:00
Devin FoleyandPaperclip 0c3b5ce273 perf: filter sandbox provider lookups in Postgres (#17395)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Sandbox execution selects a provider from installed plugin
manifests.
> - Driver-key lookup reads every plugin row before it finds a matching
declaration.
> - Repeated lookups transfer manifests for unrelated plugins.
> - The registry owns this database read and can filter candidates
before returning rows.
> - This pull request adds that filter and keeps the existing provider
selection checks.
> - The returned data now grows with matching providers instead of the
full catalog.

## Linked Issues or Issue Description

**Problem or motivation**

`resolvePluginSandboxProviderDriverByKey` calls the unfiltered registry
catalog on each lookup. The caller only needs plugins with a
sandbox-provider declaration for one driver key. Unrelated plugin
manifests still cross the database connection and are decoded in the
server.

**Proposed solution**

Add an optional sandbox-provider driver-key filter to
`pluginRegistryService().list`. Match the key and kind on the same JSON
declaration in Postgres. Pass the key from the existing resolver. Keep
full matching rows, install order, status checks and worker checks.

**Alternatives considered**

A catalog cache would need an invalidation contract for plugin updates
and independent writers. This change keeps fresh database reads.
Returning only one SQL row could skip a later eligible provider when an
earlier worker is unavailable. The filter therefore keeps all matching
candidates.

**Roadmap alignment**

This is a small performance fix to existing plugin execution. It adds no
product feature or provider. Searches for sandbox/driver lookup and
plugin catalog/filter work found no duplicate PR or issue.

## What Changed

- Add an optional JSONB containment filter to the existing registry list
read.
- Use it in sandbox-provider lookup by driver key. The unfiltered
catalog call keeps its existing behavior.
- Add real-Postgres regression coverage and a short developer note.

## Verification

- Four focused provider-selection/recovery/seam suites passed: 27 tests.
- `pnpm build`: passed.
- `pnpm -r typecheck`: passed.
- The new Postgres tests seed large unrelated manifests and verify the
actual resolver SQL filters rows in the database. They cover key/kind
matching within one declaration, unfiltered reads, install order,
disabled and unavailable workers, live status/declaration changes,
unknown keys and bound unusual input.
- The filter keeps the existing SQL call count. It does not add a
network call, cache, timer, or background worker.
- `git diff --check`: passed. Reviewed the staged diff for secrets and
private account data before push.
- Full PR CI passed on `6f97c984a931416b6347e9885d840557c54d8aab`: 54
successful checks and two expected skips. [Workflow
evidence](https://github.com/paperclipai/paperclip/actions/runs/40041294925).
- Apex review: 5/5 on that exact head, completed cleanly, with no
comments or unresolved threads. Merge state is clean.
- The prior local full run on parent work is documented in #17384. It
did not pass because of test setup, host Git-default assumptions and
transient failures. Those results were preserved and triaged against
unchanged code. This change uses focused local checks and full
current-head CI; no new local full-suite pass is claimed.
- No hosted user journey, live provider throughput, production
deployment or billed savings is claimed.

## Risks

- JSONB containment checks the stored manifest before returning rows. It
still scans candidate metadata in the database; no index or SQL-call
reduction is claimed.
- Both key and kind must match the same declaration. The regression
fixture includes a plugin with those values on different declarations.
- Selection still uses the existing install-order and live
readiness/worker checks. Matching disabled records remain visible to
callers that do not require a running worker.
- No schema, API response, provider protocol, file-copy behavior or
scheduling changes.

## Model Used

OpenAI Codex, GPT-6 family. The exact deployment model ID and context
window are not exposed in this session. Used reasoning, repository tools
and code execution.

## Checklist

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

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-23 18:31:36 -07:00
Devin FoleyandPaperclip 8549ecaf9a Re-home isolated repository workspaces onto the shared project clone (#17369)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Each isolated task workspace is a Git worktree on the server disk,
and the terminal workspace reaper and git cache expiration keep that
disk usage bounded
> - For some days, workspace preparation gave each isolated
`git_worktree` workspace its own full clone under
`isolated-repository-sources/`, with the worktree inside it. #16963
removed that creation path, but the existing clones stay, and restoring
an expired workspace still recreates its private clone
> - On one instance, 30 such clones use about 23 GB: 12.1 GB of `.git`
data, 5.2 GB of unused default-branch checkouts and 5.75 GB of real
worktrees. 28 of them back open workspaces, and the disk is nearly full
> - This pull request moves each such open workspace onto the project's
shared managed clone with its exact state, deletes the private clone,
and makes restore use the shared clone
> - The benefit is that disk use drops to one shared clone per project
repository, with no lost work and no change for workspaces that cannot
move safely

## Linked Issues or Issue Description

Refs #16963. Bug report described in-PR (bug report template).

**What happened?**
On one instance, 30 per-issue clones under
`isolated-repository-sources/<companyId>/<issueId>/checkout` use about
23 GB. 28 of them back open workspaces. No process moves these
workspaces to the shared project clone that new workspaces use, and git
cache expiration does not remove a clone while its workspace is open.
Reopening an expired workspace whose recipe has `sourceKind:
"isolated_remote"` clones the full repository again. Separately, a
restore point with a non-default file mode cannot be restored: the
reference read back from jsonb has reordered `modes` keys, so
`loadGitRestorePoint` fails with `restore_point_manifest_mismatch`.

**Expected behavior**
An open workspace with a private clone moves to the shared project clone
with the same branch, HEAD, index, worktree, untracked and ignored
files, and file modes. The private clone is then deleted. Reopening an
expired workspace uses the shared clone when one exists. A restore point
with non-default file modes restores.

**Steps to reproduce**
1. Create an isolated `git_worktree` workspace with
`materializeIsolatedRemoteWorkspaceSource` and record
`isolatedRemoteSource` metadata, as preparation did before #16963.
2. Keep its issue open. The clone stays, because no sweep moves or
removes it.
3. Expire the workspace with a restore point that has a file with mode
`0600`, then reopen it. The private clone is created again, and the
restore fails with `restore_point_manifest_mismatch`.

**Paperclip version or commit:** `master` at ec2e0d6407.
**Deployment mode:** any.
**Agent adapter(s) involved:** not adapter-specific (core).

## What Changed

- New `server/src/services/isolated-repository-rehome.ts` with
`rehomeIsolatedRepositoryWorkspace`. It holds the isolated source lock,
the `execution_workspace_lifecycle:<id>` advisory lock and
expiration-style Git lock files (`index.lock`, `HEAD.lock` and the
branch ref lock in the clone).
- Eligibility uses the safety checks of git cache expiration that matter
while a workspace stays open: no live or unfinished relevant runs, no
unsettled native finalizations, no unreleased environment leases, no
running runtime services, no pending reopen, and no other workspace on
the same path. It does not require a closed or idle issue tree. The
check runs again inside the update transaction.
- The target base is the project's existing shared managed clone for the
same `repoUrl` (the paths `ensureManagedProjectWorkspace` uses, verified
with the same origin and storage checks as restoration).
- The move captures a restore point of the old worktree. The upstream
boundary is commits that both repositories have, so the bundle is small.
It imports the bundle into the shared clone, creates
`refs/heads/<branch>` or fast-forwards it from an ancestor, and adds the
worktree at `<base>/.paperclip/worktrees/<branch>`. Then it applies the
restore point. It copies the `HEAD` reflog and `ORIG_HEAD`, copies the
branch's upstream config when it creates the branch, and moves
regenerable directories, untracked `.env`/`.env.*` files,
`.claude/settings.local.json` and nested `.paperclip-repositories/*`
checkouts by rename. Standalone nested clones move as is. Nested
checkouts that are worktrees of their own shared clone (#17382) move
too, followed by `git worktree repair` in that clone so the registration
follows the new path, and their `paperclip/<hash>/` branch is renamed to
the new path's prefix so ownership checks still pass. Entries inside a
moved nested checkout (for example its `node_modules`) move with it.
Rollback and interrupted-move recovery repair them again after moving
them back, and verification checks that each one still resolves to the
same shared clone.
- Verification: the restore point snapshot (HEAD, index, worktree,
ignored files, modes) of the new worktree must equal the old one before
and after the move. The reflog and `ORIG_HEAD` must match, and every
moved item must exist. Only then does one transaction set
`cwd`/`providerRef`, drop `isolatedRemoteSource` and
`gitCacheExpiration`, rewrite a saved active restoration recipe to
`managed_project`, and record `isolatedRepositoryRehome: { at, from, to,
cleanedAt }`. A failure before that removes the new worktree, moves
items back, restores the branch, and leaves the old worktree untouched.
- Safety of the checkout in the shared clone: the move skips with
`unsupported_base_git_config` when the shared clone's local config sets
filter drivers, includes, attribute files, worktree config or command
settings, or when it has `info/attributes`. It checks before planning
and again right before `git worktree add`.
- Crash safety before the update: the move records
`isolatedRepositoryRehomePending` (base, target, branch, head, previous
branch tip, planned moves) before the first change to the shared clone.
The next attempt undoes an interrupted move under the same locks: it
moves items back, removes the new worktree only when it is proven to be
this move's (a linked worktree of the shared clone on the issue branch
that no workspace row references), and restores the branch. A live
failure removes the target only after this attempt created it. When an
item exists in both places, rollback stops with
`rehome_rollback_conflict` and keeps the target and the record, so no
copy is lost.
- Cleanup after the update: remove the old worktree (rename to trash,
then delete, then `worktree prune`). Delete the clone (rename to trash,
then delete) only when no other workspace, project workspace or running
service uses it, it has no other worktree, its checkout has no changes
or non-regenerable files, its refs are only branches, tags and
remote-tracking refs, and every local commit is on a remote-tracking ref
or present in the shared clone. Cleanup reads at most 5,000 refs, reflog
entries and unique commits (`clone_history_budget`). `cleanedAt` records
completion. A crash after the update leaves a pending record that the
next pass finishes, also after the workspace is archived: the reaper
scan includes archived rows with unfinished re-home cleanup when the
feature is on.
- Reaper integration in `sweepTerminalWorkspaces`: behind
`PAPERCLIP_ISOLATED_SOURCE_REHOME_ENABLED` (default `false`) and
`PAPERCLIP_ISOLATED_SOURCE_REHOME_DRY_RUN`, only when git cache
expiration is enabled. It logs `event:
"execution_workspace.isolated_source_rehome"` with outcome, reason,
candidate bytes and reclaimed bytes. A skipped workspace gets a retry
deadline one hour later in the reaper's existing deadline set. No new
timer.
- Restore: `reopenClosedIsolatedExecutionWorkspaceForIssue` calls
`rehomeExpiredIsolatedRestoration` first. For an expired workspace with
an `isolated_remote` recipe, it uses the shared clone when one exists
and the branch there is absent or at the saved commit. It keeps the old
behavior for recipes with worktree-backed nested checkouts on branches,
because those branch names are bound to the old path. It also requires
that the shared clone has every base commit (prerequisite) of the saved
restore point bundle. It rewrites the row and recipe to the shared clone
and the standard worktree path, then the existing restore runs
(`ensureGitRestorePointCommit`, `ensureSavedGitCommit`, apply). Without
a shared clone, the old behavior stays.
- Fix: restore point `modes` comparison in
`git-restore-point-storage.ts` compares by path and mode, so jsonb key
order does not matter.
- Small exports: `allocatedBytes`, `expirationScope`,
`managedSourcePaths`, `verifiedGitCacheSource`.
- Docs: new "Isolated repository sources" section in
`doc/recoverable-disk-cleanup.md`, new settings in `doc/DEVELOPING.md`.

Deviations from the requested design:
- The sweep does not create a missing shared clone. When none exists, it
skips with `no_shared_base`, because a new network clone in a cleanup
path can use more disk. Restore follows the same rule and keeps the old
behavior.
- History moves through the restore point bundle, not a separate fetch
from the clone. The bundle contains HEAD and every reflog/`ORIG_HEAD`
commit that the shared clone does not have. The move then checks that
all of them are present.
- The standard location is `<base>/.paperclip/worktrees/<branch>`. A
worktree outside the clone's standard location is skipped
(`nonstandard_worktree_location`).
- Lock files use a token derived from the workspace id, so a crashed
attempt's locks are recovered without a database marker.
- A clone that another open workspace still uses is kept and marked
`cloneRetained: "base_has_consumers"`. That workspace's own move removes
it later.

Skip reasons: `live_or_unresolved_consumer`,
`unsupported_base_git_config`, `rehome_rollback_conflict`,
`nested_branch_conflict`, `invalid_rehome_record`,
`interrupted_rehome_pending` (dry run only), `lifecycle_busy`,
`source_busy`, `workspace_changed`, `no_shared_base`,
`shared_base_repo_mismatch`, `missing_worktree`,
`nonstandard_worktree_location`, `source_identity_changed`,
`branch_identity_changed`, `unproven_worktree_admin`,
`unsupported_worktree_git_state`, `unique_worktree_refs`,
`unknown_worktree_git_logs`, `unsupported_local_git_state`,
`unique_git_refs`, `unsupported_index_flags`, `unsupported_branch_name`,
`invalid_restoration_recipe`, `unsupported_restoration_recipe`,
`target_worktree_exists`, `base_branch_checked_out`,
`base_branch_diverged`, `untransferable_ignored_files`,
`untransferable_managed_checkouts`, `worktree_git_busy`, `head_changed`,
`history_not_transferred`, `transfer_conflict`, `snapshot_mismatch`,
`history_mismatch`, `transfer_incomplete`, and restore point reasons
such as `untracked_nested_repository`, `submodules_unsupported`,
`filtered_paths`, `restore_point_too_large`. Clone kept reasons:
`base_has_consumers`, `base_has_live_services`, `base_has_worktrees`,
`clone_checkout_has_files`, `unsupported_clone_refs`,
`unique_clone_history`, `clone_history_budget`.

## Verification

- `pnpm exec vitest run
src/__tests__/isolated-repository-rehome.test.ts` in `server/`: 18
passed. It uses real Git repositories and embedded Postgres. It covers:
- a move with an unpushed commit, staged and unstaged edits, a deletion,
an untracked file with mode `0600`, an ignored file, `node_modules`,
`.env`, `.claude/settings.local.json` and a nested
`.paperclip-repositories` checkout. Snapshots, branch, HEAD, `git
status` and reflog match, the row points to the new path without
`isolatedRemoteSource`, and the old worktree and clone are gone;
- skips with nothing changed on disk or in the database: live run,
branch in the shared clone at an unrelated commit, no shared clone, an
untracked nested repository, and a concurrent change between capture and
verify (hook). The new worktree is removed and the old one keeps its
moved items;
  - the clone is kept while another workspace references it;
- a worktree-backed nested checkout moves with the task worktree, stays
registered in its shared clone at the new path (not prunable), and keeps
its changes, with its branch renamed to the new path's prefix. On
rollback it returns to the old path with its registration repaired and
its branch name restored. A standalone nested checkout moves together
with its own ignored `node_modules`;
- a file recreated in the old worktree before verification stops the
rollback and keeps both copies, then the move completes once the
conflict is gone;
- a hostile `filter.*.smudge` in the shared clone's config makes the
move skip without running it;
- a clone with more than 5,000 refs is kept, and is deleted once the
refs are gone;
- a move interrupted before the database update is reported (not undone)
by a dry run, then undone and completed on the next attempt;
- a target that another party creates right before `git worktree add` is
never deleted;
- a crash after the database update; after the row is archived, a later
reaper sweep removes the old worktree and clone;
- restore keeps the private-clone fallback when the shared clone lacks
the restore point's base commits;
- disabled by default (both flags needed), dry run changes nothing, and
the retry deadline is registered;
- reopening an expired `isolated_remote` workspace rebuilds it in the
shared clone with the restore point applied and no clone created.
- After the round 4 fixes, the related suites
(`git-workspace-expiration`, `git-restore-point-expiration`,
`execution-workspace-reopen`, `execution-workspaces-service`,
`heartbeat-git-restore-point-resume`, `heartbeat-project-repositories`,
`isolated-remote-workspace-source`) with the new suite: 326 passed.
- After rebasing on #17382, the related suites (adding
`heartbeat-project-repositories`) with the new suite: 374 passed.
- After the first review fixes, the related suites
(`git-workspace-expiration`, `git-restore-point`,
`git-restore-point-expiration`, `execution-workspace-reopen`,
`execution-workspaces-service`, `execution-workspace-compaction`,
`heartbeat-git-restore-point-resume`,
`isolated-remote-workspace-source`) with the new suite: 301 passed.
- Related suites in `server/` (first commit): `pnpm exec vitest run` on
`git-workspace-expiration`, `git-workspace-expiration-eligibility`,
`git-restore-point`, `git-restore-point-expiration`,
`git-restore-point-expiration-config`, `git-restore-point-ledger`,
`git-restore-point-ledger-config`, `execution-workspace-reopen`,
`execution-workspaces-service`, `execution-workspace-compaction`,
`heartbeat-git-restore-point-resume`,
`isolated-remote-workspace-source`,
`isolated-remote-workspace-source-lock`, `git-workspace-base-storage`,
`git-reproducible-managed-checkouts`, `git-expiration-auth-fallback` and
`workspace-runtime`: 550 passed, 2 failed. The 2 failures are in
`workspace-runtime.test.ts` (base ref resolution) and also fail on
unmodified `master` on the same machine.
- `pnpm run typecheck` in `server/` passes (after `pnpm -r --filter
"./packages/**" build`).
- Mutation checks. Each reverted guard fails one test in the new suite:
  - eligibility always true: "keeps a workspace with a live run" fails;
- snapshot verification removed: "removes the new worktree and keeps the
old one when the workspace changes during the move" fails;
- clone reference check removed: "keeps the clone while another
workspace still references it" fails;
- restore re-home removed from reopen: "restores an expired isolated
workspace into the shared project clone" fails;
  - old `JSON.stringify` modes comparison: the same restore test fails;
  - interrupted-move recovery removed: the interrupted-move test fails;
- target existence check before `git worktree add` replaced by assumed
ownership: the foreign-target test fails;
- bundle prerequisite check removed: the stale shared clone restore test
fails;
  - archived-row cleanup scan removed: the crash test fails;
- dry run check before interrupted-move recovery removed: the
interrupted-move test fails;
- `git worktree repair` after the forward move removed: both nested
worktree tests fail. The repair after moving back removed: the rollback
test fails;
- nested branch rename forward or back removed: the matching nested test
fails;
  - rollback conflict check removed: the reappearing-file test fails;
  - covered-entry filter removed: the full re-home test fails;
  - shared clone config check removed: the hostile filter test fails;
  - ref budget removed: the many-ref test fails.
- Not run: the full server suite, and the move on a live instance.

## Risks

- Workspace paths change for re-homed workspaces. Agents that resume an
old session can remember the old path. The run's working directory comes
from the updated row, but text in a resumed conversation can still name
the old path.
- The feature is off by default. It runs only with
`PAPERCLIP_ISOLATED_SOURCE_REHOME_ENABLED=true` and git cache expiration
enabled. Dry run is available. The restore change is not behind the
flag: reopening an expired private-clone workspace now uses the shared
clone when one exists.
- A run can start between the last eligibility check and the database
update. The update transaction checks eligibility again, and the old
worktree is deleted only after another check. This is the same window
that git cache expiration accepts.
- Imported restore point objects stay in the shared clone as unreachable
objects after a failed attempt until Git garbage collection removes
them. Base expiration keeps a base with unreachable objects, so this
only delays base expiration.
- A workspace that cannot move (for example `no_shared_base`) is retried
each hour while the feature is on. Each retry runs a few Git commands
for that workspace.
- No migration. No schema change.

## Model Used

Claude Opus 5.5 (`claude-opus-5-5`), 1M context, through Claude Code
with tool use (shell, file editing, Git, 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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-23 18:25:26 -07:00
Devin FoleyandPaperclip 724fa6144d fix(opencode-local): preserve the agent during sandbox tool OOMs (#17398)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The Legacy OpenCode adapter runs an agent and its shell tools in the
same sandbox.
> - A large build can exceed the sandbox memory limit and trigger a
kernel OOM kill.
> - The kernel can kill OpenCode before its tool, which loses the active
turn.
> - OpenCode provides a shell environment hook that can change only the
tool subprocess.
> - This pull request uses that hook to prefer Bash tool processes as
OOM victims.
> - The benefit is that OpenCode can report a failed tool and continue
the task.

## Linked Issues or Issue Description

**What happened?**

An OpenCode sandbox run ended with exit 137 after a TypeScript process
had also been killed. Its cgroup limit was 4 GiB. It had completed
useful tool work and had not reached its execution deadline. The
original sandbox was deleted, so its historical OOM counters are
unavailable. Memory pressure is the leading explanation, not a proven
historical kernel diagnosis.

**Expected behavior**

When a shell tool exhausts sandbox memory, the kernel should prefer that
tool over OpenCode where possible. The adapter must preserve configured
shell startup, model, permissions and resource limits.

**Steps to reproduce**

Use a disposable 4 GiB Linux sandbox with OpenCode. Retain a 1,200 MiB
buffer in the agent and run a Bash tool with fourteen 250 MiB workers.
Compare the same workload before and after installing the generated
hook. Record the cgroup memory events, agent exit and final response.

Refs: #16844. That PR covers the same protection for Claude. This PR
adds the OpenCode-owned hook. Refs: #17254 for the separate shutdown
recovery path. This PR does not change retry policy.

## What Changed

- Generate a dependency-free OpenCode `shell.env` plugin and a Bash
startup file in the isolated runtime config.
- Stage those assets only for sandbox execution. Resolve the startup
file relative to the copied plugin.
- Set child `oom_score_adj` to `1000` on Linux. Keep the parent
environment unchanged and honor existing `BASH_ENV` values, including an
empty value.
- Keep command execution available if the optional plugin cannot be
installed or the process priority cannot be changed.
- Materialize linked plugin directories inside the isolated config
before writing. Keep the operator directory and its plugins unchanged.
- Cover relocation, shell output and exit status, operator settings,
local execution, remote staging and failed installation. Update the
adapter documentation and verification recipe.

## Verification

- Red before the fix: the new runtime regression failed because the
sandbox shell plugin was absent. The existing 15 runtime config tests
passed. The same regression passes with the fix.
- Apex first found a host-write risk through linked plugins directories
(4/5). A new regression reproduced the host directory modification, then
passed after the installer materialized the copied link. The finding is
resolved. Apex reviewed current head
`df82a8f51725a197c0ea3ebeb942488509f32b34` at 5/5 with no actionable
issues remaining.
- The full OpenCode adapter suite passes: 87 source tests. The adapter
typecheck and map checks pass.
- Real sandbox acceptance used the plugin generated from
`df82a8f51725a197c0ea3ebeb942488509f32b34` and OpenCode 1.18.34 in a
disposable 4 GiB Daytona sandbox. A controlled local model endpoint
issued an actual Bash tool call; no real model credentials were used.
The tool reported priority `1000`; the parent reported `0`.
- Controlled OOM comparison: without the guard, OpenCode was killed by
SIGKILL after 8.95 seconds and the cgroup recorded one OOM kill. With
the guard, five workers were killed, OpenCode exited `0` after 21.87
seconds, and its final response was `fixture-complete`. The cgroup
counter increased by five. Both attempts used the same allocation and
workload. The fixture started with a linked operator plugins directory.
The installer preserved that directory, staged a real plugins directory,
and passed both the process-priority probe and OOM comparison. The
fixture allocation was deleted afterward.
- The first live probe was not accepted because the harness could not
parse the provider response framing. After correcting the decoder, the
unchanged plugin passed the probe and the controlled OOM comparison.
Both initial disposable allocations were deleted. After the review fix,
a third allocation repeated the probe and OOM comparison with a linked
plugins directory. That allocation was also deleted. The initial
comparison had the same agent-killed/agent-survived result (8.42 seconds
without the guard; 19.42 seconds with it).
- Repository `pnpm -r typecheck` and `pnpm build` pass. The full CI
matrix passes on `df82a8f51725a197c0ea3ebeb942488509f32b34`, including
tests, browser shards, Runner checks, build, policy and Apex. [CI
run](https://github.com/paperclipai/paperclip/actions/runs/40043947375).
- The first full local test attempt had 15 embedded PostgreSQL startup
failures. Direct `initdb --version` and `postgres --version` probes
showed missing bundled library symlinks and SIGABRT. Running the bundled
symlink hydration script restored both executables. All 15 affected
cases pass after that setup repair. No product code changed for it.
- Two existing workspace tests assumed that new Git repositories have no
`main` branch. They failed under the host default branch setting of
`main`, then passed with a per-command `init.defaultBranch=master`
setting. The global setting was preserved. The full local test command
was restarted with hydrated libraries and that controlled fixture
default. It had no reported assertion failures when it was stopped after
the complete CI matrix passed. It did not complete locally; full-suite
verification comes from CI. The first failed invocation was also
stopped.
- The original tenant task has not been retried or recovered by this PR.
Its board API credential is expired. The original hosted workflow and
the separate shutdown recovery remain outside this acceptance result.

## Risks

- This is a preference, not a memory reservation. It cannot guarantee
that OpenCode survives every OOM condition, including memory growth
inside OpenCode itself.
- The guard applies to non-interactive Bash tools. Other shells ignore
`BASH_ENV`. Existing operator startup settings take precedence.
- Older OpenCode releases without the shell environment hook will not
apply the preference. No model, permission, deadline, memory quota or
retry limit changes.
- No schema change or migration.

## Model Used

OpenAI GPT-6 through Codex, with repository tools and code execution.
The exact deployment variant and context window are not exposed by 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>
canary/v2026.1024.0-canary.2
2026-10-23 17:49:00 -07:00
Nicky Leach a978ecadef Honor negated attachment requests in task completion (#17399) 2026-10-23 17:38:50 -07:00
72a5fecb58 docs(release): canonicalize stable notes for v2026.1023.0 (#17400)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The release process promotes a soaked beta to stable. The stable run
publishes the curated notes from `releases/beta/v<beta-version>.md` on
`master` as the GitHub Release body
> - After the publish, the notes must move to
`releases/v<stable-version>.md` on `master`. That path is the canonical
location that the release announcements and the next release notes link
to
> - Stable `v2026.1023.0` was published on 2026-10-23 from beta
`2026.1012.1-beta.0`. The `canonicalize_stable_notes` job in run
40030701194 was skipped, so the workflow did not push this branch
> - This pull request does by hand what that job does: it renames the
beta notes file to the stable path. It changes no content
> - The benefit is that `releases/v2026.1023.0.md` exists on `master`
and is byte-identical to the published GitHub Release body

## Linked Issues or Issue Description

Refs #17373
Refs #16053

**Issue type**

Missing content

**Where is the issue?**

`releases/` on `master`. The stable notes for `v2026.1023.0` are only at
the beta path `releases/beta/v2026.1012.1-beta.0.md`. The canonical path
`releases/v2026.1023.0.md` does not exist.

**What's wrong?**

The GitHub Release
https://github.com/paperclipai/paperclip/releases/tag/v2026.1023.0 is
published, but the `canonicalize_stable_notes` job of the release run
was skipped. The release announcements link to
`releases/v2026.1023.0.md` on `master`, and that link returns 404 until
this rename is merged.

**Suggested fix**

Rename the beta notes file to the stable path with no content change.
This is the same step the workflow job performs.

## What Changed

- `releases/beta/v2026.1012.1-beta.0.md` is renamed to
`releases/v2026.1023.0.md`. Git reports a 100% rename
- No line of the file changes. The blob is
`170e36dc971bc8f3a59fb574fc8cd19d366fc2d4`, the same blob the release
run pinned at `master` commit `e0d90339` when it created the GitHub
Release
- This pull request is a pure rename. It does not change the release
workflow

## Verification

- `git diff --stat -M100%
master...release-notes/v2026.1023.0-canonicalize` shows one renamed file
and no insertions or deletions
- `git rev-parse
release-notes/v2026.1023.0-canonicalize:releases/v2026.1023.0.md` prints
`170e36dc971bc8f3a59fb574fc8cd19d366fc2d4`
- `git rev-parse
e0d90339bfbde08ba804c1e527b781ac139075c7:releases/beta/v2026.1012.1-beta.0.md`
prints the same blob
- `gh api repos/paperclipai/paperclip/releases/tags/v2026.1023.0 --jq
.body | diff - releases/v2026.1023.0.md` reports only the one trailing
newline that `jq` adds to its output. The release body and the file have
the same bytes
- After merge,
https://github.com/paperclipai/paperclip/blob/master/releases/v2026.1023.0.md
returns 200 and the old beta path returns 404

## Risks

- Low risk: one markdown file moves and no content changes
- The release workflow's `canonicalize_stable_notes` job was skipped in
run 40030701194 although `publish_stable` succeeded and the notes mode
was `master_beta`. The job's `if` has no status function, so GitHub
applies an implicit `success()` that evaluates to false when an upstream
job in the chain was skipped (`release_runner_binaries` was skipped in
this run). A one-line `!cancelled() &&` guard, as `publish_stable`
already uses, fixes it. That fix is not in this pull request so this
stays a pure rename

## Model Used

- Claude (Anthropic), model ID `claude-fable-5-1` (Claude Fable 5.1),
extended thinking enabled, tool use via 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: devinfoley <139239+devinfoley@users.noreply.github.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-23 17:33:39 -07:00
Nicky Leach 100477e887 Avoid live workspace inspection in task context reads (#17396) canary/v2026.1024.0-canary.1 2026-10-23 17:05:06 -07:00
cc127f5516 fix(runner): drop unusable musl builds and duplicate Copilot from provider packs (#17387)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The runner release builds a Linux "provider pack". The pack is a
self-contained runtime with Node, Claude Code, Codex, and OpenCode. The
Daytona runner image and SSH hosts use it.
> - The release artifacts for this pack are large: 1,182 MB for
linux-x64 and 543 MB for linux-arm64.
> - Most of that size is necessary. But about 20–30% of it is code that
the pack can never run.
> - pnpm 9 installs all libc variants of optional platform packages.
Thus the pack includes the musl (Alpine) builds of Claude Code and
OpenCode. The pack runs only its copied glibc Node, so these builds are
never used.
> - On linux-x64, the npm Copilot package is also a byte-identical copy
of the verified Copilot provider asset (162 MB).
> - This pull request removes these unused bytes from the pack at build
time.
> - The benefit is smaller release artifacts and Docker images, and
faster uploads and downloads.

## Linked Issues or Issue Description

**What happened?**
The runner release workflow uploads `runner-provider-packs-linux-x64`
(1,182 MB) and `runner-provider-packs-linux-arm64` (543 MB). The darwin
daemon artifacts are about 10 MB. The darwin artifacts are a different
artifact type, so the comparison is not direct. But inspection of the
extracted Linux packs found real waste:
- `@anthropic-ai/claude-agent-sdk-linux-*-musl` (about 225 MB
uncompressed)
- `opencode-linux-*-musl` and `opencode-linux-x64-baseline-musl` (about
180 MB each)
- on linux-x64,
`node_modules/.pnpm/@github+copilot-linux-x64@1.0.88/.../copilot`, which
has the same SHA-256 (`0059754c…`) as
`provider-assets/copilot/linux-x64/copilot`

**Expected behavior**
The pack contains only code that its copied Node and its launch shims
can run.

**Paperclip version or commit**
`master` at `e0d9033`. The inspected release run built commit `837add0`.

**Steps to reproduce**
1. Install `@agentclientprotocol/claude-agent-acp@0.73.0` and
`opencode-ai@1.18.34` with pnpm 9.15.4 on a glibc Linux host.
2. List `node_modules/.pnpm`.
3. See the `*-musl` entries. Their `package.json` declares `"libc":
["musl"]`.

## What Changed

- `provider-pack-runtime-tree.mjs`: add `providerPackLibc()`. It reports
the libc of the Node that the pack copies (`glibc` or `musl` on Linux,
none on other platforms).
- `provider-pack-runtime-tree.mjs`: add
`removeForeignLibcPackagesFromProviderPack()`. It removes each pnpm
virtual-store entry whose own `package.json` declares a `libc` list
without the pack libc. Packages without a `libc` field stay (for example
`@openai/codex-linux-*`, which holds Codex's static musl binary).
- `provider-pack-runtime-tree.mjs`: add `removePnpmStoreEntries()`. It
removes store entries and every symlink that resolves into them, so no
dangling link stays in the pack. It refuses names outside
`node_modules/.pnpm`.
- `provider-pack-executable-shims.mjs`: when
`provider-assets/copilot/<platform>/copilot` has the same bytes as the
npm Copilot executable, the `copilot-<platform>` command now points at
the asset and the npm copy is removed. If the bytes are different, the
old behavior stays.
- `build-provider-pack.mjs`: run the libc prune immediately after `pnpm
deploy`. Log the removed entries.
- No file bound by `provider-pack.json` changes. Thus the manifest
digest and the qualification and digest pins do not change.

## Verification

- `node --test
packages/paperclip-runner/scripts/provider-pack-runtime-tree.test.mjs
packages/paperclip-runner/scripts/provider-pack-executable-shims.test.mjs
packages/paperclip-runner/scripts/candidate-provider-pack.test.mjs
packages/paperclip-runner/scripts/release-provider-pack.test.mjs`: 40/40
pass.
- The new Copilot test fails with the old
`provider-pack-executable-shims.mjs` and passes with the new file.
- A real pnpm 9.15.4 tree with the pack's provider dependencies, on
linux-x64 (glibc):
- The prune removed `@anthropic-ai+claude-agent-sdk-linux-x64-musl`,
`opencode-linux-x64-musl`, and `opencode-linux-x64-baseline-musl`. The
Copilot dedupe removed the npm Copilot entry.
  - `node_modules` changed from 1.9 GB to 1.1 GB.
  - `find node_modules -xtype l` found no dangling links.
- The claude-agent-acp SDK still resolves
`@anthropic-ai/claude-agent-sdk-linux-x64/claude`.
  - `opencode --version` prints `1.18.34`.
- The rewritten `copilot-linux-x64` prints `GitHub Copilot CLI 1.0.88.`
- Earlier measurement: we pruned the same paths from the extracted pack
artifacts and compressed them again with `tar -czf`. linux-x64 went from
1131 MB to 876 MB. linux-arm64 went from 519 MB to 360 MB.
- Not run locally: a full `build-provider-pack.mjs` build. This sandbox
has 10 GB of disk, which is not sufficient for a full workspace install
and a 3 GB pack. The `release-verify.yml` provider-pack step and the
Daytona image `RUN claude/codex/acpx --version` checks run the full
build.
- `test/acpx-codex-package-contract.test.mjs` has 1 failure in this
sandbox, and the failure is the same on `master`. The cause is that this
checkout has no `node_modules`.

## Risks

- Low risk. All pack builds run on glibc hosts (Debian bookworm images
and Ubuntu CI). On a musl build host, the prune removes the glibc
packages, and the existing Claude executable resolution fails at build
time. Before this change, that build gave a pack with a Claude binary
that could not run.
- The `copilot-linux-x64` command now runs the binary from
`provider-assets/copilot/linux-x64/`. The bytes are identical. Runtime
admission already uses this asset, through the descriptor-pinned lease.
- The pack content changes but the manifest digest does not. The remote
verifier checks only the bound artifacts, `dist`, and provider asset
trees. None of these change.
- Not in this pull request: Pi's bundled Node is byte-identical to the
pack Node (121 MB, about 43 MB compressed). A fix for it changes the
qualified Pi closure digest. Also, the `release.yml` publish jobs
download the full packs, but they read only `provider-pack.json`. These
are possible follow-ups.

## Model Used

- Claude Opus 5.5 (Anthropic), model ID `claude-opus-5-5`, used through
Claude Code with tool use (shell, file edit) and code execution in a
sandbox.

## 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 (no
doc change is necessary: build behavior and commands do not change)
- [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: devinfoley <139239+devinfoley@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
canary/v2026.1024.0-canary.0
2026-10-23 16:41:14 -07:00
Devin FoleyandPaperclip 5a7e740d82 fix: validate managed AI accounts before verification allocation (#17384)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A saved agent must pass execution configuration checks before it can
run.
> - Managed AI account selection belongs to the existing company-scoped
account service.
> - Verification allocated its execution environment before it checked
that account.
> - A missing default or revoked account could create a sandbox on each
retry.
> - This pull request checks account selection before allocation and
keeps the final check.
> - Invalid account settings now fail before they cause avoidable
provider work.

## Linked Issues or Issue Description

**What happened?**

Saved-agent lifecycle verification allocates a test execution target
before `withManagedAiProbe` selects the managed AI account. An
unavailable account can cause sandbox creation and cleanup on every
retry.

**Expected behavior**

Check account compatibility, responsible-user access, default selection,
grant health and agent eligibility before target allocation. Check
current account access again before the real probe.

**Steps to reproduce**

1. Configure a saved OpenCode agent with an OpenRouter personal-default
binding.
2. Leave that user's OpenRouter default empty, or revoke the selected
grant.
3. Let lifecycle verification use a sandbox execution environment.
4. Before this change, verification can allocate the target and then
reject the account. After this change, account selection rejects the
configuration before allocation.

**Paperclip version or commit**

Source baseline `b5bf86ea09042a864b60910f80c8fdf7e64e9ef0`.

**Deployment mode**

Server with Postgres and a remote execution environment. Tests use
disposable local Postgres and a controlled target allocation boundary.

Searched public PRs and issues for verification, connection preflight,
probes and lifecycle work. No duplicate fix found. Related public issue
#16128 concerns credential probing and reauthorization. This change does
not alter reauthorization or credential refresh.

## What Changed

- Call the existing managed account selector with the resolved adapter
and model before allocating a verification execution target.
- Keep router resolution, secret handling, final account selection and
target release with their existing owners.
- Add six Postgres tests and document the saved-agent ordering contract.

## Verification

- Focused verification suites: 30 tests passed across two files.
- `pnpm build`: passed.
- `pnpm -r typecheck`: passed.
- Full PR CI passed on `ddc0879d46b60310c533e6d3c4b8acdd178d979c`: 54
successful checks and two expected skips. [Workflow
evidence](https://github.com/paperclipai/paperclip/actions/runs/40033261909).
This includes typecheck, tests, build and browser checks.
- Apex review: 5/5 on that exact head, clean check, no comments or
unresolved threads. Merge state is clean.
- Additional local `pnpm test:run` is still running. The complete local
suite is not yet a pass claim.
- The first focused run passed the missing-default case but failed five
setup hooks because fixtures reused a unique company prefix. Each
fixture now has a distinct prefix. The complete focused rerun passed.
- Tests use real account selection, memberships, grants, personal
defaults, encrypted fixture credentials and managed runtime preparation.
Only execution-target allocation and the provider probe are controlled
boundaries. No live provider request is required.
- Tests prove that missing defaults, revoked grants and incompatible
models do not allocate a target. A valid account still reaches the
managed probe. Revocation during allocation still fails the final
account check and releases the target. Probe failure also releases it.
- `git diff --check`: passed. Reviewed the diff for secrets and private
account data before push.
- No hosted login or production deployment is claimed for this
follow-up.

## Risks

- Adds one read-only account selection to each managed saved-agent
verification attempt. The selector can read company accounts and grants.
It makes no provider request and creates no execution target.
- Final selection stays fresh. A successful preflight does not reserve
an account or cache authorization.
- Missing configuration still follows the existing durable lifecycle
retry schedule. This change prevents needless allocation; it does not
remove those database retries.
- No schema, scheduler, provider-account choice, capacity or public API
changes.

## Model Used

OpenAI Codex, GPT-6 family. The exact deployment model ID and context
window are not exposed in this session. Used reasoning, repository tools
and code execution.

## Checklist

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

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-23 16:31:18 -07:00
Devin FoleyandPaperclip ec2e0d6407 Share one clone across nested project repository checkouts (#17382)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A project can attach extra repositories, and every task worktree
gets a managed checkout of each one under
`.paperclip-repositories/<name>`
> - Each of these nested checkouts is a full clone. The clone uses
`--dissociate`, so no objects are shared between tasks
> - On one instance, 44 task worktrees held nested checkouts of the same
repository: 24 GB of 27 GB of worktrees. Each new task added about 0.5
GB, and the disk filled twice in one day
> - Main task worktrees already avoid this, because they are git
worktrees of one managed project clone
> - This pull request makes nested checkouts git worktrees of one shared
clone per project and repository, and adapts expiration, restore points,
restore and cleanup to them without weaker safety checks
> - The benefit is that repository objects are stored once, and a new
task adds only its working files

## Linked Issues or Issue Description

No public issue exists. This is the in-PR bug report.

**What happened?**
Every task in a project with extra repositories got a full, independent
clone of each extra repository under `.paperclip-repositories/`. On one
instance this used 24 GB of 27 GB of task worktrees (16 GB of it `.git`
data) and filled the disk twice in one day.

**Expected behavior**
Nested checkouts of the same repository share one object store, like
main task worktrees share the managed project clone. Disk use per new
task is only its working files.

**Steps to reproduce**
1. Create a project with a primary repository and a second repository
that has a repository URL and no local folder.
2. Run several tasks in isolated git worktrees in that project.
3. Look at `<task worktree>/.paperclip-repositories/<name>/.git`: each
one is a full directory with its own copy of every object.

**Paperclip version or commit:** `master` at ff835a3fc6.
**Deployment mode:** any.
**Agent adapter(s) involved:** not adapter-specific (core).

Related: #15662 (SSH restores skip nested worktrees) touches a different
path; this PR does not change adapter sync.

## What Changed

- New `server/src/services/nested-repository-worktrees.ts`. It finds the
shared clone (the managed project checkout for the repository URL, from
`ensureManagedProjectWorkspace`), adds nested worktrees, restores them,
removes their registrations, and serializes all of these operations and
fetches per shared clone with an in-process lock.
- `prepareProjectRepositoryWorkspaces` takes a `sharedBase` (company and
project). For a repository without a configured local folder, it fetches
the shared clone and adds a worktree at the same
`.paperclip-repositories/<name>-<key>` path. Each worktree gets its own
branch `paperclip/<id>/<branch>` that tracks `origin/<branch>` (`<id>`
derives from the worktree path), or a detached HEAD for a tag or commit
ref. An existing worktree must belong to the project's shared clone.
Configured local-folder copies and existing standalone clones are
unchanged. Moving a worktree to `detached-repositories` repairs its
registration. Heartbeat preparation and fresh-start reopening pass
`sharedBase`.
- `assertReproducibleManagedCheckout` and `provenManagedCheckoutHistory`
accept worktree-backed checkouts. They verify the worktree link, its
admin directory, the shared clone identity and origin, and that HEAD is
detached or on a branch under the worktree's own prefix. History checks
scope to HEAD, the per-worktree HEAD reflog, ORIG_HEAD and the owned
branches with their reflogs. Other tasks' branches are not inspected.
Stashes, unsupported refs and custom config in the shared clone still
block. The upstream proof reads objects from the common git directory.
- Restoration contents record `worktree: { base, admin }` for a
worktree-backed checkout. Validation checks its shape and the owned
branch name, and every use checks the base against the project's
expected clone paths.
- Expiration locks for a worktree-backed checkout sit in the shared
clone (`.git/worktrees/<admin>/index.lock`, `HEAD.lock`, and the owned
branch ref lock). `gitCacheExpirationLockLocations` is now async.
- `finishGitCacheTrash` removes the expired worktree's registration and
deletes its owned branch only at the recorded commit.
- `restoreGitWorkspaceContents` recreates a worktree-backed checkout
from the shared clone, and clones the shared clone again if it is gone.
It imports the restore point into the shared clone, adds the worktree on
the saved branch and commit, writes reflog-only commits back into the
new HEAD reflog, and applies the restore point.
- `cleanupExecutionWorkspaceArtifacts` (terminal reaper archival, close
and delete) collects the task's nested worktrees first. After the task
directory is gone, it removes their registrations and deletes owned
branches only when every commit is on a remote-tracking ref. An unpushed
branch stays in the shared clone.
- Review follow-ups. Owned branches always track origin, including names
with dots, and the config check accepts their tracking keys. A ref held
by a retained detached checkout gets `paperclip/<id>/r<n>/<branch>`.
Restore derives the upstream from the saved source ref. Each new
worktree fetches with `--prune`, refreshes `origin/HEAD` when no ref is
configured, and strips `refs/heads/` from branch refs. Git commands on
the shared clone run with hooks disabled. The worktree `gitdir` pointer
is read through a bounded, non-following, non-blocking handle, and more
than 64 owned branches stop the proof before reflog reads.
- Docs: new "Nested project repository worktrees" section in
`doc/recoverable-disk-cleanup.md`, and updates in `doc/DEVELOPING.md`.

## Verification

- `pnpm -r --filter "./packages/**" build`, then `pnpm run typecheck` in
`server/`: passes.
- `pnpm exec vitest run` in `server/` for `git-workspace-expiration`,
`git-reproducible-managed-checkouts`, `git-workspace-contents`,
`git-restore-point*`, `heartbeat-project-repositories`,
`execution-workspaces-service`, `execution-workspaces-routes`,
`execution-workspace-compaction`, `workspace-compaction`,
`heartbeat-git-restore-point-resume`, `managed-repository-checkout`,
`task-workspace-repositories*`, `native-workspace-sync*`,
`heartbeat-managed-clone-credentials`,
`services/heartbeat/workspace-preparation`,
`project-repositories-persistence`, `git-workspace-base-storage`,
`agent-directory-probe`, `workspace-runtime`,
`workspace-preparation-history` and `native-completion-feedback`: 866
passed, 2 failed. The 2 failures are in `workspace-runtime.test.ts`
(`rejects explicit main on a master-only remote...` and `does not
diagnose a missing remote branch...`). They fail the same way on
unmodified `master` on this machine.
- New tests (embedded Postgres, real git repositories) in
`git-workspace-expiration.test.ts`, section "nested checkouts as
worktrees of a shared base":
- Two tasks get worktrees of one shared clone. `--git-common-dir` points
to it, each task has a distinct `paperclip/<id>/main` branch, upstream
is `origin/main` and the remote is the repository URL.
- Concurrent preparation of three tasks succeeds, and preparation waits
while the shared clone lock is held.
- A legacy standalone clone is still adopted, and no shared clone is
created for it.
- Expiring a clean nested worktree removes only its registration and
branch. The sibling worktree and branch stay. Reopening recreates the
worktree from the shared clone on the same branch and commit, with its
local settings.
  - Reopening clones a deleted shared clone again.
- Unpushed commits and uncommitted files block plain expiration, are
saved in a restore point and are restored exactly.
- Reflog-only history is saved and written back into the restored HEAD
reflog, and it survives `gc --prune=now` of the shared clone.
- Stashes, a checked-out branch outside the worktree's prefix, and
unpushed owned branches or owned reflog entries outside HEAD history
block expiration.
- Another task's unpushed branch does not block, and it stays after
expiration.
- A worktree of a foreign clone at the nested path is rejected by
preparation and by the prover.
- Expiration locks in the shared clone are recovered after a process
exit.
- Deleting a task removes only its nested registrations, keeps its
unpushed branch and deletes the sibling's pushed branch when the sibling
is deleted.
- Follow-up tests: dotted branch tracking, a ref switched away and back
with a retained checkout, a changed remote default branch, a renumbered
branch restored next to a same-named remote branch, a fully qualified
branch ref after the remote advances, a branch deleted upstream, planted
`post-checkout` and `reference-transaction` hooks, more than 64 owned
branches, and a FIFO, oversized or symlinked `gitdir` pointer. Each fix
was mutation-checked.
- `heartbeat-project-repositories.test.ts`: a repository-URL-only
sibling is now a worktree of the shared clone on an owned branch, and
its detached copy is still a valid checkout after its repository leaves
the project. In `workspace-preparation.test.ts`, the publication guard
now runs before any worktree exists.
- Mutation checks. I reverted one guard at a time and confirmed that a
test fails: the owned-HEAD check, the owned-branch check in restore
point mode, the owned-reflog check, the shared-clone identity check, the
pushed-only branch deletion on cleanup, the cleanup release hook, the
registration removal on expiration, the reflog-only history write-back
on restore, and the shared clone lock. The owned-branch-tip check alone
is also covered by the owned-reflog check, so removing only one of the
two keeps the tests green.
- Not run: the full server test suite, the UI and e2e suites, and a live
instance migration.

## Risks

- Behavior change for agents: a nested checkout of a URL-only repository
is now on `paperclip/<id>/<branch>`, not `<branch>`. `git pull` and `git
status` work as before. A plain `git push` now fails because the branch
names differ. Agents must push with an explicit destination, for example
`git push origin HEAD:<branch>`. Branches in the shared clone are
visible to all tasks of the project, like main task worktree branches.
- Agent-created branches outside the worktree's prefix, stashes, and
local config live in the shared clone. Stashes and custom config block
expiration of every worktree of that clone (fail-closed). Other branches
stay in the shared clone after a task is deleted.
- The shared clone is the managed project checkout for that repository
URL. Fetches into it update its remote-tracking refs. A concurrent
restore point capture of a sibling can then roll back and retry on the
next sweep.
- The base lock is per process. Across processes, git's own ref and
index locks apply.
- No migration in this PR. Existing standalone nested clones keep their
disk use until their tasks are removed or expired. A safe migration
helper is a follow-up. Task repositories (`task-repo-*`) and configured
local-folder copies stay standalone clones.
- Restoration recipes now can carry `contents.checkouts[].worktree`. An
older server rejects such a recipe as invalid (fail-closed) and does not
restore without it.

## Model Used

Claude Opus 5.5 (`claude-opus-5-5`), 1M context, through Claude Code
with tool use (shell, file editing, 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
- [ ] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
canary/v2026.1023.0-canary.18
2026-10-23 16:05:13 -07:00
DottaandPaperclip bf223596ab fix(runner): bootstrap fresh sessions in replacement sandboxes (#17320)
Reuse the untouched-session proof and atomic directory claim when a sandbox
replacement carries a provider session that has never run. Retain verified
restore for saved backups and all prior or ambiguous session activity.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-23 15:35:24 -07:00
e0d90339bf fix(runner): make task-capable plugin environments Runner-only (#17380)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - An environment plugin can run Paperclip Runner tasks through a
`driver: "plugin"` environment.
> - Core lets only the `paperclip_runner` adapter use a plugin
environment.
> - A Claude agent cannot get onto the Runner there. Runner selection
needs a reported platform for Claude, and a plugin target reports none.
> - The adapter Test route also checks the driver against the requested
harness before it resolves the runner.
> - This pull request makes task-capable plugin environments Runner-only
and checks the Test driver after runner selection.
> - The benefit is that Claude and Codex agents can use a Runner task
environment, and the UI cannot produce a legacy agent there.

## Linked Issues or Issue Description

Refs #17029. Refs #16978.

**What happened?**

A Claude Code agent with a plugin environment that runs Runner tasks
fails the connection test. The error is: `Environment driver "plugin" is
not allowed here. Allowed drivers: local, ssh, sandbox`. The Runner
selector in Advanced shows Paperclip Runner as unavailable, so the user
cannot fix the agent in the form.

**Expected behavior**

A Claude or Codex agent on that environment uses Paperclip Runner. The
connection test passes the driver check. Legacy is not offered, because
a legacy adapter cannot run on a plugin environment.

**Steps to reproduce**

1. Install an environment plugin whose driver declares `supportsTasks:
true`.
2. Create a plugin environment for that driver.
3. Open an agent with the Claude Code harness and select that
environment.
4. Click Test. The test fails with the driver error above.

**Agent adapter(s) involved**

Claude Code, Codex (through Paperclip Runner).

**Additional context**

Two gates caused the failure:

- `agentRunnerAvailability` requires a platform for `claude_local`. A
plugin target is `{ driver: "plugin" }`, so Claude resolves to Legacy.
- The Test route ran `assertAdapterTestEnvironmentForCompany` with the
URL adapter type before `resolveNewAgentRunnerForCompany`. The plugin
driver is allowed only for `paperclip_runner`, so the check failed
before selection could change the adapter.

## What Changed

- Runner target resolution marks a plugin environment as `runnerTasks`
when its plugin manifest declares `supportsTasks` for the driver key and
has the `environment.drivers.register` capability. The check reads the
manifest only. Run admission still verifies a ready plugin and a live
worker.
- `agentRunnerAvailability` returns Paperclip Runner as the only runner
for a `runnerTasks` target. It skips the platform check, because the
plugin provisions the host and launches its own pinned Runner. Harnesses
that the Runner does not support keep the Legacy result.
- The adapter Test route runs the company-binding check first, as
before. It runs the driver check after runner selection, with the
resolved adapter type. The driver check still runs before secret
resolution, leases, and the adapter probe.
- A saved adapter that cannot run on the requested environment no longer
keeps its saved execution in the Test route. It re-resolves its runner.
- `assertAdapterTestEnvironmentBinding` is a new helper that holds the
binding half of the guard. `assertAdapterTestEnvironmentForCompany`
keeps its behavior and calls it.
- The agent form switches a Legacy runner choice to Paperclip Runner
when the selected environment offers no Legacy runner. The test and the
save then send the Runner choice.
- `docs/specs/agent-config-ui.md` records the Runner-only rule and the
Run test order.

## Verification

- `pnpm --filter @paperclipai/server exec tsc --noEmit` passes.
- `pnpm --filter @paperclipai/ui exec tsc -b` passes.
- Server suites pass: `agent-runner-selection.test.ts`,
`agent-test-environment-routes.test.ts`,
`agent-permissions-routes.test.ts`,
`environment-selection-route-guards.test.ts`,
`agent-harness-verification.test.ts`,
`plugin-runner-task.integration.test.ts`, `adapter-routes.test.ts`,
`adapter-auth-signal-routes.test.ts`,
`agent-adapter-validation-routes.test.ts`,
`agent-hire-ai-connections.test.ts`.
- UI suites pass: `AgentConfigForm.render.test.tsx` and
`NewAgent.test.tsx`.
- New tests cover these cases:
- A task-capable plugin target offers only Paperclip Runner for Codex
and Claude.
  - Claude saves as `paperclip_runner` with ACPX on that target.
- A driver without `supportsTasks`, a different driver key, a missing
capability, an unknown plugin, or a malformed config keeps the existing
plugin target.
  - The Test route checks the driver against the resolved adapter.
- The Test route still rejects a plugin environment for a legacy
adapter, before any secret resolution.
- A saved adapter re-resolves only when it cannot run on the requested
environment.
- The form switches a saved or new Legacy choice to Paperclip Runner on
a Runner-only environment.
- I ran the new route and form tests against the old code. They fail
without the change.
- The full server `typecheck` script did not complete locally. Its
Runner vendor step needs `cargo`, which this machine does not have. CI
runs the full script.
- I did not run a live plugin environment end to end.

## Risks

- Behavior change: Codex on a task-capable plugin environment now offers
only Paperclip Runner. Before, the selector also showed Legacy, but Core
rejected a legacy save on that environment. No working setup loses an
option.
- The `runnerTasks` decision reads the plugin manifest. If the plugin is
down, the agent still selects the Runner, and the run fails at admission
with the existing plugin error.
- The adapter auth-signal route still checks the driver with the
requested harness. This PR does not change that route.
- An explicit Legacy save through the API on a Runner-only environment
still fails with the existing driver error. The UI no longer offers that
choice.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`) wrote the change and the tests in
Claude Code, with shell, file editing, and local test runs.
- Claude Fable 5.1 (`claude-fable-5-1`) did the earlier diagnosis in the
same session.
- The session 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
- [ ] 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: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 15:21:09 -07:00
Devin FoleyandPaperclip b5bf86ea09 Clear process identity when settled native runs stop (#17374)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Each heartbeat run stores the process identity of its runner in
`heartbeat_runs.process_pid`, `process_group_id` and
`process_started_at`
> - Only cancellations (`release_run_resources`), restart recovery and
heartbeat recovery cleared these fields
> - Failed native runs take the `schedule_retry` path, so they kept a
PID after the run finished, for weeks
> - Liveness checks for old runs then depend on PID-reuse defenses, and
code that reads a set PID as "running" is misled
> - This pull request releases the process identity of settled native
runs when the process is confirmed gone, and backfills old sandbox rows
> - The benefit is that a finished run no longer looks like it owns a
live process, while live, warm and retrying processes keep their
identity

## Linked Issues or Issue Description

Refs #17358. Bug report described in-PR (bug report template).

**What happened?**
On one instance, 16 of 23 old native workspace sync folders belonged to
failed or cancelled runs that finished more than a week earlier and
still had `process_pid` set. The PID is written when the run's process
starts (for remote targets it is the sandbox PID). The status arbiter
clears it only through `release_run_resources`, which only cancellations
emit. Failed native runs take `schedule_retry`, and other terminal paths
(finalization reconciler, restart recovery terminal failures, watchdog
timeouts) do not clear it either.

**Expected behavior**
When a run is terminal and its process is confirmed gone (or no longer
owned), the process identity is cleared. A process that may still be
alive keeps its identity.

**Steps to reproduce**
1. Run a native task whose provider turn fails, so the arbiter decides
`run_failed_partial_evidence_preserved` with a `schedule_retry` effect.
2. Wait for the run to finish and the runner to exit.
3. Read the run row: `status = 'failed'` and `process_pid` /
`process_group_id` / `process_started_at` are still set.

**Paperclip version or commit:** `master` at 75fcb0b3fc.
**Deployment mode:** any.
**Agent adapter(s) involved:** native runtime (`paperclip_runner`);
local and sandbox targets.

## What Changed

- `releaseSettledNativeRunProcess` (in `native-local-process-stop.ts`)
clears `process_pid`, `process_group_id` and `process_started_at` of a
native run under a row lock, only when all of these are true:
- the status is `failed`, `cancelled`, `timed_out` or `interrupted`, and
`finished_at` is set;
- the finalization coordinator is absent, `committed` or
`terminal_failure` (a `retryable_failure` same-run retry keeps the
existing heartbeat recovery path; unfinished finalization keeps its
identity);
- the run has a session checkpoint or provider evidence, so native
session resume never mistakes it for an unused bootstrap;
- local target: every lease is released without failed cleanup, the
latest `native.process_identity_recorded` event names this host and the
same PID/group, and the PID and group probe absent. The release writes
the existing `native.local_process_stopped` receipt in the same
transaction, so `hasNativeLocalProcessStop`,
`readNativeLocalProcessStop`, explicit continuation and acknowledged
Stop keep their proof;
- sandbox target: every lease has a remote termination receipt (sandbox
PIDs are never probed on this host).
- Succeeded runs keep their identity: a warm runner can serve the next
turn, and chat retry matches warm predecessors by PID.
- The heartbeat executor calls the release after lease and scratch
cleanup at a terminal boundary (not when a same-run resume, workspace
finalization or held ownership is pending).
- Background work stays idle (see
`rules/sleep-aware-background-work.mdc`):
- Trigger: the executor settlement calls `settleNativeRunProcess`. If
the release is not ready yet (process still alive, local lease not
released, remote termination receipt missing), the run goes into an
in-memory pending set.
- Startup: the first `reapOrphanedRuns` pass after boot runs
`discoverSettledNativeRunProcesses` once, in pages of 50, over the
durable rows. This reconstructs work after a restart and covers terminal
transitions outside an executor (reconciler, restart recovery, watchdog,
timeouts).
- Retry: later passes call `retryPendingSettledNativeRunProcesses` only
for pending runs. It makes no database call when the set is empty. A run
leaves the set when it is released, becomes ineligible, or reaches 20
attempts.
  - Runs that this process still executes are skipped.
- Stop receipts now also record `processStartedAt`.
`withReleasedNativeRunProcess` restores a released identity from the
latest stop receipt. Retained session cleanup
(`reconcileRetainedNativeSessionCleanup`) and warm transition restart
checks use it. Quiescent runnerd state recovery reads the stop receipt
when the prior run's identity was released. Without a receipt, all three
still treat the identity as unproven.
- Migration `0377_clear_settled_native_run_process_identity` backfills
sandbox runs (without bumping `updated_at`) with the same rules: native,
settled status, `finished_at` older than 1 day, settled coordinator
phase, checkpoint or provider evidence, and at least one lease where
every lease has a valid remote termination receipt. The snapshot is
carried forward as `0377_snapshot.json`.
- Not changed: legacy (non-native) runs. Case handoff release, scratch
cleanup and conversation continuation read the terminal row's local
PID/group as process-tree absence evidence, so clearing them needs
separate work. `release_run_resources` cancellation behavior and
`native_run_finalizations.controller_*` fields (they track the server
controller, not the run process) are unchanged.

### Migration margin

The 1-day margin is defense in depth. The real proof is the remote
termination receipt: the provider confirmed that the sandbox stopped or
was destroyed. One day is far longer than lease release, sandbox stop,
restart recovery takeover and the finalization reconciler sweep, so no
in-flight teardown or same-run recovery can still read these rows. Local
rows are not backfilled in SQL: only the host that recorded the process
can verify that it stopped and write the stop receipt. The runtime sweep
releases them on that host.

## Verification

- `pnpm exec vitest run
src/__tests__/native-settled-process-release.test.ts` (new, 20 tests):
release with a stop receipt and restore of the identity from it;
`cancelled`/`timed_out`/`interrupted`; live process kept, then released
after exit; kept for succeeded, `retryable_failure`,
`workspace_finalizing`, unreleased local lease, another host, no
evidence, unconfirmed sandbox, mixed leases; sandbox release with
receipts; startup discovery paging and skip; pending retry; retry cap;
no database access while nothing is pending; `reapOrphanedRuns`
discovery, then retry after the process exits; executor release for a
failed native run and no release for a succeeded run.
- `native-provider-failure.integration.test.ts`: a real `schedule_retry`
decision (`run_failed_partial_evidence_preserved`) leaves the failed run
with its PID after finalization, and the release then clears it and the
stop receipt reads back.
- `native-session-executor.test.ts`: quiescent recovery and retained
session cleanup accept a released identity with a stop receipt and
reject one without a receipt.
- `packages/db` `client.test.ts` migration rule: the backfill does not
set `updated_at`.
- `native-finalization-migration.test.ts`: migration 0377 clears only
old settled native sandbox rows with confirmed termination (15 fixtures,
including recent, succeeded, running, legacy, unsettled phases, no
evidence, unconfirmed or local leases, no lease).
- Related suites, all passing: `pnpm exec vitest run` on
heartbeat-process-recovery, native-workspace-sync-retention(-config),
native-finalization-{migration,settled-candidates,recovery},
chat-failed-run-retry-boundary, acknowledged-native-stop,
explicit-native-continuation, conversation-continuation,
native-session-resume, native-run-finalizer-telemetry,
native-session-executor, native-safe-replacement,
native-finalization-reconciler-timing, native-restart-recovery,
native-finalization-recovery-state, run-lifecycle,
agent-instruction/directory working copies, heartbeat-case-handoff (1920
tests across 23 files).
- `pnpm run typecheck` in `server/`: passes. `pnpm run check:migrations`
and the snapshot drift test in `packages/db`: pass.
- Mutation checks (each reverted alone; a test failed every time):
executor call, startup discovery, pending retry in `reapOrphanedRuns`,
retry cap, pending-set cleanup on retry, start time in the stop receipt,
retained cleanup receipt fallback, coordinator phase guard, succeeded
exclusion, liveness probe, host check, remote receipt check,
provider-evidence guard, local lease check, quiescent recovery receipt
fallback, migration margin, migration NULL-safe receipt check, migration
phase guard. The NULL-safe check came from a real bug the migration test
found. The warm transition restart call site has no direct test: that
path has no existing fixture. It uses the same tested helper.
- Not run: the full server suite and e2e tests.

## Risks

- Behavior change: more native runs now have a null `process_pid` after
they settle. Every native reader was checked: explicit continuation,
acknowledged Stop and agent stop use the stop receipt for null
identities; cancelled-startup detection also requires no process events,
so a released run is not mistaken for a cancelled startup; native
session resume selection is protected by the evidence guard; chat retry
reads its warm-predecessor PIDs from succeeded runs, which are not
released.
- A recycled local PID keeps a row unreleased (conservative), and
sandbox runs from providers without termination receipts keep their
identity.
- Background cost: one paged discovery at startup. After that, only
pending runs are checked, at most 20 times each. With nothing pending
there is no recurring query.
- A transition outside an executor that happens after startup, with no
executor settlement, is released at the next startup discovery.
- Migration: a data-only `UPDATE` on `heartbeat_runs` with a selective
`WHERE`; it passes the migration safety check. It is safe to re-run.

## Model Used

Claude Opus 5.5 (`claude-opus-5-5`), 1M context, through Claude Code
with tool use.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
canary/v2026.1023.0-canary.17
2026-10-23 15:09:43 -07:00
Devin FoleyandPaperclip 4660b22f6b perf: narrow run bindings during event ingestion (#17378)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Agent runs append native events and retry-exhaustion receipts.
> - These writes lock the run row and verify its company and agent
bindings.
> - The binding check fetched the full run, including prompts,
configuration, and output.
> - This pull request reads only the two fields that the check uses.
> - The benefit is less database transfer while the same row lock
protects sequence allocation and replay checks.

## Linked Issues or Issue Description

Related: #17355 and #17348 reduce other run-read payloads. GitHub
searches for event-read and narrow-run PRs found no duplicate of this
event-ingestion change.

**What existing behavior does this improve?**

The run binding read during native event and retry-exhaustion ingestion.

**Current behavior**

The transaction uses a full-row `SELECT ... FOR UPDATE` to check only
`companyId` and `agentId`. Large run context, runner configuration, and
output cross the database connection for each such check.

**Proposed behavior**

Select only the company and agent IDs. Keep the same run ID predicate,
row lock, transaction, sequence allocator, replay cursor, and binding
checks.

**Reason and benefit**

The check does not use the other fields. Narrowing the query removes
that repeated transfer without a cache or an authorization change.

**Breaking changes**

None. No API response, schema, event, or scheduling changes.

## What Changed

- Narrow the existing run-row read inside `appendHeartbeatRunEvent`. The
event service continues to own its transaction and binding validation.
- Exercise native and retry-exhaustion writes and replays with large
stored payloads. Record the executed SQL to prove the lock reads only
the two binding fields.
- Extend missing-run and company/agent mismatch coverage to both
transaction paths. Keep the existing concurrent-writer and rollback
tests.
- Document the read boundary.

## Verification

- Focused PostgreSQL event append and sequencing suites: 9 tests passed.
They include concurrent writers, replay conflicts, rollback, cursor
checks, large payloads, and company/agent/missing-run denials.
- The first new retry-exhaustion fixture failed its duplicate assertion
because it omitted the exhaustion fields from the event payload. The
existing producer contract stores those fields in the payload. The
fixture now does the same; the rerun passed. No production retry or
replay logic changed.
- `pnpm build`: passed.
- `pnpm -r typecheck`: passed.
- Greptile Apex: 5/5 on `76a6d3ee813ae47919b1ec8e8b88e01e58d5655d`,
clean check and no review threads. The [full PR
workflow](https://github.com/paperclipai/paperclip/actions/runs/40028758665)
passed on that exact head: 54 checks passed and two conditional checks
were skipped. The branch is conflict-free.
- The full local all-in-one suite is not repeated for this follow-up.
Full exact-head CI will supply the broader suite coverage.
- No hosted deployment or bill reduction is claimed for this follow-up.

## Risks

- Low scope: only the selected columns change. The `FOR UPDATE` lock,
transaction boundaries, sequence reservation, duplicate checks, and
failure handling remain in place.
- Native replay and legacy event writers must still serialize on the
same row. Existing real PostgreSQL concurrency tests cover that
boundary.
- No migration, provider change, new timer, or operator configuration is
required.

## Model Used

OpenAI Codex (GPT-6 family), with code execution and repository tools.
The runtime does not expose the 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
#` 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 suites 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 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-10-23 15:09:29 -07:00
ff835a3fc6 docs(release): set the v2026.1023.0 header and contributor count before publish (#17373)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The release process promotes a soaked beta to stable. The stable
workflow reads `releases/beta/v<beta-version>.md` from `master` at
dispatch time and publishes that revision as the GitHub Release body
> - The curated notes for `2026.1012.1-beta.0` (#16053) were written on
2026-10-13. They guessed the stable version `v2026.1015.0` and the date
`2026-10-15`. That version did not ship
> - The promotion is now set for 2026-10-23. `./scripts/release.sh
stable --print-version --date 2026-10-23` on `master` returns
`2026.1023.0`
> - Nothing merged after the stable dispatch reaches the release page,
so the header must be correct on `master` before the dispatch
> - This pull request sets the header, the date, the intro version, and
the contributor count. It changes no other text
> - The benefit is that the public release page for v2026.1023.0 shows
the correct version, date, and credits

## Linked Issues or Issue Description

Refs #16053

**Issue type**

Incorrect content

**Where is the issue?**

`releases/beta/v2026.1012.1-beta.0.md` on `master`: the H1, the `>
Released:` line, the first sentence of the overview, and the first
sentence of the Contributors section.

**What's wrong?**

The file names `v2026.1015.0` and `2026-10-15`. The stable that promotes
this beta is `v2026.1023.0` on `2026-10-23`. The Contributors line says
6 contributors. `git shortlog -sn
v2026.1012.0..1b4c803816e82b0265c8422e1a804327a7ed8c54` lists 8 authors.
With `github-actions[bot]` excluded, as in `releases/v2026.1012.0.md`,
the count is 7 (Dotta, Devin Foley, dependabot[bot], Nicky Leach,
Michael Nguyen, Kilian, lorenzozane).

**Suggested fix**

Replace the four stale values. Keep every other line unchanged so the
file still matches the audited content.

## What Changed

- `# Paperclip v2026.1015.0` becomes `# Paperclip v2026.1023.0`
- `> Released: 2026-10-15` becomes `> Released: 2026-10-23`
- The overview sentence now reads `Paperclip v2026.1023.0 carries 200
commits, promoted from 2026.1012.1-beta.0`
- The Contributors sentence now reads `This release has 200 commits from
7 contributors`
- No other line changes. The credited handles `@lorenzozanee` and
`@lusoris` stay as they were

## Verification

- `git diff master..release-notes/v2026.1023.0-prepublish --stat` shows
one file, 4 insertions, 4 deletions
- `grep -n '1015\|2026-10-15' releases/beta/v2026.1012.1-beta.0.md`
returns nothing on this branch
- `./scripts/release.sh stable --print-version --date 2026-10-23` on
`master` prints `2026.1023.0`
- `git rev-list --count
v2026.1012.0..1b4c803816e82b0265c8422e1a804327a7ed8c54` prints 200. The
GitHub compare API for the same range also reports 200
- `git shortlog -sn
v2026.1012.0..1b4c803816e82b0265c8422e1a804327a7ed8c54` lists 8 authors.
7 remain after `github-actions[bot]` is removed, which is the rule
`releases/v2026.1012.0.md` used
- All 129 `#NNNN` links in the file resolve to pull requests inside the
range. The 71 in-range pull requests that the file does not cite are CI,
tests, lockfile refreshes, heartbeat refactors, dependency bumps,
release housekeeping, or Paperclip Cloud work (#15405, #15566, #15691,
#15722)
- Migrations `0306` to `0333` are the 28 `.sql` files added in the
range. The three environment variables in the Upgrade Guide and their
defaults match `server/src/services/authorization.ts` and
`server/src/index.ts` at the source commit

## Risks

- Low risk: a single markdown file, four lines. If the UTC day changes
before the dispatch, the dispatch keeps `stable_date=2026-10-23` so the
header and the tag agree

## Model Used

- Claude (Anthropic), model ID `claude-fable-5-1` (Claude Fable 5.1),
extended thinking enabled, tool use via 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: devinfoley <139239+devinfoley@users.noreply.github.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-23 14:57:47 -07:00
Nicky LeachandPaperclip a3e8fa568a Reduce repeated work during plugin Runner admission (#17377)
## Thinking Path

> - Paperclip manages agents and their execution authority.
> - A plugin can host Runner tasks on a persistent execution host.
> - Admission checks must observe current lifecycle state and
credentials.
> - The current runtime reader also resolves files, skills, and
connections for each check.
> - Some providers also repeat the same project authentication check
during submit.
> - This pull request adds a compact fresh reader and an explicit
submit-time validation contract.
> - Providers can remove repeated work while Core keeps lease and scope
checks.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

Plugin Runner startup before task submission.

**Current behavior**

One measured startup made 30 full runtime reads during bootstrap. These
reads took 5.759 seconds. Each guard can resolve instructions, skills,
and connection assignments even when it only needs current identity and
lifecycle state. Providers that already enforce project authentication
in submit must also perform a separate Core preflight.

**Proposed behavior**

Use a lifecycle-owned runtime identity reader for guards. Keep the full
runtime reader for synchronization inputs. A trusted provider can
declare `projectAuthCheckAtSubmit: true` in its prepared result. Core
pins the exact configuration, provider, and project set on the lease.
The provider must reject authentication conflicts before credential
installation or task start. Providers that omit the flag retain the
existing preflight.

**Reason and benefit**

Estimated savings with a paired consumer: 4–5 seconds from compact
runtime checks, plus roughly 0.5–0.9 seconds from removing the duplicate
authentication operation. These estimates overlap and are not a measured
deployed result. Refs #17367 for the separate run-log HEAD optimization;
this PR does not duplicate it.

**Breaking changes**

No migration. The SDK adds a host method and an optional prepared-result
field. Consumers that use them require this Core host version. Older
providers retain their existing flow.

## What Changed

- Add `agents.getRuntimeIdentity` across SDK, RPC, host service, and
test fixtures.
- Reuse the existing company, plugin ownership, readiness, and
capability checks. Read lifecycle state and protected instance secrets
fresh on every call.
- Skip instruction, organization, skill, and connection hydration in the
compact reader.
- Allow explicit submit-time authentication enforcement, with prepared
scope pinned and checked again at dispatch.
- Emit fixed preparation, project-auth, submit, and connection timing
phases.

## Verification

- 72 targeted SDK capability, plugin task-owner, and environment-task
mediator tests passed.
- 33 real embedded-Postgres lifecycle and secret tests passed, including
pause, content revision, company isolation, plugin disablement, and
secret rotation.
- SDK and server TypeScript checks passed. Local Runner/eval TypeScript
outputs were built to satisfy server type resolution.
- Local Postgres required missing library aliases in ignored
dependencies; no product source was changed for that repair.
- Full repository suites are delegated to GitHub CI. No live rollout or
speedup claim.

## Risks

The submit-time flag delegates enforcement to a trusted provider. It is
not proof that prepare already checked authentication. The provider must
check before VM credential installation or task admission. Core still
checks exact prepared scope, active lease, run identity, credential
type, and dispatch fencing. Existing providers keep the separate check.
Roll back a paired consumer before rolling back this host.

## Model Used

OpenAI GPT-6 (Codex), with code execution and repository tools. Exact
deployment build and context-window size are not exposed.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have described the issue in-PR following the relevant issue
template
- [x] I have not referenced internal/instance-local Paperclip issues or
links
- [x] My branch name describes the change and contains no internal
ticket id
- [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-10-23 14:54:25 -07:00
3c4050e3ad fix(connections): keep Cloud refresh reauthorization diagnostics (#17362)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Managed connections (Gmail, Google Workspace, GitHub, Asana, Slack
manager) get and refresh their OAuth tokens through the Paperclip Cloud
connector broker.
> - When a provider refuses a refresh grant (the user revoked it, it
expired, it was already rotated, or it no longer matches the profile),
the broker sent `502 PROVIDER_OPERATION_FAILED`. That is the same answer
it sends for a provider outage.
> - Paperclip then kept the grant `active`. Each health check, catalog
refresh, or catalog read failed again with a 502, and the server
reported each failure as a crash. The user did not get a reconnect
prompt.
> - The broker now sends `409 PROVIDER_REAUTHORIZATION_REQUIRED` for a
dead grant. The connector already maps any 409 to
`REAUTHORIZATION_REQUIRED`. Thus the grant becomes
`needs_reauthorization` and the caller gets a 422 reconnect response.
> - This pull request adds the new reason to the diagnostic allowlist,
pins the producer contract, and tests the reconnect path from end to
end.
> - The benefit is that a dead managed grant asks the user to reconnect.
It is not reported as a server crash. Real provider outages keep their
502 response.

## Linked Issues or Issue Description

**What happened?**
A managed Paperclip Cloud connection refresh failed with `Paperclip
Cloud connector rejected the request (operation=refresh, status=502,
reason=PROVIDER_OPERATION_FAILED)`. The connection health-check and
catalog routes wrap that error as an HTTP 502. The error handler reports
every status of 500 or more, so the failure went to error monitoring.
The grant stayed `active`, so the same failure can occur again on each
use.

**Expected behavior**
If the provider refuses the refresh grant, Paperclip marks the grant
`needs_reauthorization` and shows the reconnect path. Only a real
provider or Cloud outage gives a 502.

**Steps to reproduce**
1. Connect a managed Gmail connection through Paperclip Cloud.
2. Revoke the app access in the Google account, or let the refresh grant
expire.
3. Wait until the access token needs a refresh. Then run a connection
health check.
4. Before the broker change, the health check returns 502 and the server
reports it. The grant stays `active`.

**Additional context**
The broker-side classification ships separately in Paperclip Cloud. This
pull request is safe before or after that change, because the connector
already maps any broker 409 to `REAUTHORIZATION_REQUIRED`. Without this
pull request, the 409 still works, but diagnostics show
`reason=UNKNOWN_BROKER_ERROR`.

## What Changed

- `server/src/services/paperclip-cloud-connector.ts`: Add
`PROVIDER_REAUTHORIZATION_REQUIRED` to the allowlist of broker reasons
that errors can keep.
-
`server/src/services/fixtures/cloud-connector-refresh-reauthorization.json`:
Add the producer HTTP contract (status 409 and the exact JSON error
body).
- `server/src/services/paperclip-cloud-connector.test.ts`: Test that a
refresh with this contract gives `REAUTHORIZATION_REQUIRED`, status 409,
the retained reason, and no refresh-token text.
- `server/src/__tests__/tool-access-service.test.ts`: Test that
`refreshOAuthGrantCredentials` changes a reauthorization refresh failure
into a 422 `oauth_reauthorization_required` response and sets the grant
to `needs_reauthorization`.
- `doc/connections/GMAIL.md`: Add a troubleshooting row that tells the
two refresh reasons apart.

## Verification

- `cd server && npx vitest run
src/services/paperclip-cloud-connector.test.ts`: 48 passed. Without the
allowlist change, the new test fails (`reason=UNKNOWN_BROKER_ERROR`).
- `cd server && npx vitest run src/__tests__/tool-access-service.test.ts
-t "managed grant for reconnection|orders managed reconnect"`: 2 passed,
against the embedded test database.
- I did not run the full `pnpm -r typecheck`, `pnpm test:run`, and `pnpm
build` locally. The sandbox has a 4 GB memory limit, and the server
`tsc` process was stopped because it used too much memory. CI runs these
checks.

## Risks

- Low risk. The status mapping does not change: a 409 already meant
`REAUTHORIZATION_REQUIRED`. This change only keeps one more fixed reason
code in the error message.
- The reason allowlist accepts fixed protocol codes only. The broker
message, request data, and tokens stay out of errors and logs.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, in Claude Code agent
mode with tool use (shell, file edit, code search, and a sub-agent for
the code-path trace).

## Checklist

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

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

---------

Co-authored-by: devinfoley <139239+devinfoley@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 14:39:22 -07:00
DottaandPaperclip eadc560712 fix(evals): retain usage during bounded gateway shutdown (#17371)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Product E2E tests use a local gateway to record model usage and
reserve costs.
> - Test cleanup can disconnect a background request before its provider
sends final usage.
> - Immediate abort then leaves the request cost unknown and its full
reservation held.
> - This pull request lets an admitted response finish within a short
drain period.
> - The benefit is better accounting without a new request or a retry.

## Linked Issues or Issue Description

Refs #17365 and #17354.

**What happened?**

Two hosted cases passed, but cleanup canceled background requests before
the gateway received their usage. Both requests kept their full holds. A
local HTTP regression reproduces this loss with OpenAI and Anthropic
responses.

**Expected behavior**

Retain final usage when an already-dispatched response finishes within a
bounded cleanup period. Keep the client outcome canceled. Preserve the
hold when final usage is still unknown.

**Steps to reproduce**

1. Send a request through the eval gateway.
2. Disconnect the client before the provider returns final usage.
3. Close the gateway and let the provider response arrive.
4. Before this fix, the gateway aborts immediately and loses the final
receipt.

**Paperclip version or commit**

Reproduced against master `b6e690044a821faab52d5466db36c4330c754a2d`.

## What Changed

- Drain an already-dispatched response for at most five seconds after a
disconnect. Keep the original provider timeout and campaign deadline.
- Stop forwarding response data to a disconnected client. Stop admission
as soon as shutdown begins.
- Retain `clientDisconnected` on the private receipt and keep the
outcome canceled, even when final usage arrives.
- Make repeated shutdown calls share completion.
- Add HTTP and SSE regression tests. Document the drain and its
accounting limits.

## Verification

- Red: five regression cases failed against the previous gateway
implementation. Four further cases reproduced late cancellation during
review.
- `pnpm test:e2e:runner:unit`: 4,072 Vitest tests and 137 Node contract
tests passed. Two existing Vitest tests were skipped.
- `pnpm exec tsc --noEmit -p tests/runner-e2e/tsconfig.json`: passed.
- `git diff --check`: passed.
- Regressions cover both providers, an interrupted SSE stream, grace
expiry, a shorter provider deadline, no dispatch during interrupted
identity resolution, no new admission, repeated shutdown, client-only
disconnects, and disconnects during each final receipt/ledger write.
- Full repository typecheck, tests, and build passed in [CI run
40026846417](https://github.com/paperclipai/paperclip/actions/runs/40026846417)
on `40ef13bd4d5df6b2ca0a9666fb485adf2a565c28`. Greptile and Paperclip
reviews are both 5/5. No paid model test was run for this change. Live
qualification remains open.

## Risks

- Cleanup can take up to five more seconds. The admitted request can
continue generating during that period, inside its existing reservation
and deadlines.
- An abort does not prove that the provider stopped billing. Missing
usage keeps the unknown hold.
- This change does not establish invoice settlement or rewrite
historical receipts. It changes only the eval gateway.

## Model Used

OpenAI Codex, GPT-6 family, with reasoning, code execution, and tool
use. The host does not expose an exact model build identifier or context
window size for 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-10-23 14:36:41 -07:00
Nicky Leach 2e38649341 perf(run-logs): skip durable log restore for a proven-new run (#17367) 2026-10-23 14:30:06 -07:00
DottaandPaperclip a967d479bd fix(e2e): retain verified onboarding artifact evidence (#17370)
Retain the bounded verified onboarding download and independent oracle in
private normalized results before cleanup. Preserve the receipt when later
readback or trace checks fail without marking the journey as passed.

Refs #17262. Refs #17253.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-23 14:23:08 -07:00
75fcb0b3fc fix(sentry): keep typed ACP provider access failures local (#17366)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The server sends unexpected run failures to Sentry through
`reportRunFailure`
> - A Claude ACP run can fail because the provider rejects the stored
credential ("Invalid API key"). The ACP agent then sends a typed
`access` terminal failure.
> - The Claude adapter already classifies this failure as
`claude_auth_required`. The run is blocked, and the responsible user
gets a reconnect request.
> - Sentry also records each of these runs as "ACP agent reported a
terminal access failure.". This duplicates a signal that the user
already gets, and it hides real defects.
> - This pull request adds one narrow exclusion for this proven provider
auth state, next to the existing quota and Codex capacity exclusions.
> - The benefit is less Sentry noise, and all other access-shaped
failures stay visible.

## Linked Issues or Issue Description

Refs #14886 (related: it routes ACP access failures in recovery, not in
Sentry reporting).

**What happened?**
Staging Sentry recorded a run failure "ACP agent reported a terminal
access failure." for 8 events. All events had
`agent_adapter=claude_local`, `error_code=claude_auth_required`, and a
typed provider access failure ("Invalid API key · Fix external API
key"). The failures stopped when the provider accepted the credential
again.

**Expected behavior**
A provider auth failure that the adapter types and classifies is a user
action state. The product already shows it to the user. It must not go
to Sentry in `unexpected` mode.

**Steps to reproduce**
1. Configure a `claude_local` agent with an ACP runtime and a credential
that the provider rejects.
2. Start a run.
3. The ACP agent ends the turn with `category: "access"`. The run fails
with `claude_auth_required`.
4. See a new Sentry run-failure event.

**Agent adapter(s) involved**
Claude Code (`claude_local`, ACP lane).

## What Changed

- `server/src/services/run-failure-report.ts`: add
`isAcpProviderAccessFailure`. It skips a report only when all of these
are true: the reporting mode is `unexpected`, the run status is
`failed`, the error code is `claude_auth_required` or
`acpx_auth_required`, `resultJson.terminalSessionFailure.category` is
`access`, the phase is not `setup`, and the run has no signal.
- `server/src/services/__tests__/run-failure-report.test.ts`: add
regression tests for the staging shape, eight near-miss shapes that must
still report, and the setup and `all` mode paths.

## Verification

- Red: without the predicate, the two new "keeps ... local" tests fail
(`2 failed | 266 passed`).
- Green: `cd server && pnpm exec vitest run
src/services/__tests__/run-failure-report.test.ts
src/services/__tests__/run-failure-diagnostics.test.ts` gives `383
passed`.
- `pnpm exec tsc --noEmit -p server` shows no errors in the changed
files.

## Risks

- Low risk. The change only removes Sentry events. It does not change
run status, recovery, or user notices.
- A real defect that the ACP agent wrongly types as `access` with a
Claude auth code is not reported in `unexpected` mode. Set
`PAPERCLIP_SENTRY_RUN_FAILURES=all` to see these events again.
- Codex and other ACP adapters do not map `access` to an auth code, so
their access failures still report.

> 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`), run through Claude Code
with tool use (shell, file edit, Sentry and GitHub connectors).

## 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: Devin Foley <139239+devinfoley@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
canary/v2026.1023.0-canary.16
2026-10-23 14:07:19 -07:00