Commit Graph
1322 Commits
Author SHA1 Message Date
DottaandPaperclip 28a197cdfd fix(server): preserve terminal workspace recovery cursor precision
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-24 15:27:55 -07:00
DottaandPaperclip a1a7bfd915 Merge verified decision paging repairs into next retention branch
Co-Authored-By: Paperclip <noreply@paperclip.ing>

* codex/sleep-14-decision-retention:
  fix(server): retain database precision in effect recovery cursor
  fix(server): preserve expiry paging timestamp precision
2026-10-24 15:24:56 -07:00
DottaandPaperclip 2c5babcabc Merge decision paging repairs into retention stack
Co-Authored-By: Paperclip <noreply@paperclip.ing>

* codex/sleep-13-grok-dispatch:
  fix(server): retain database precision in effect recovery cursor
  fix(server): preserve expiry paging timestamp precision
2026-10-24 15:24:26 -07:00
DottaandPaperclip 31bcd19888 Merge decision paging repairs through dispatch stack
Co-Authored-By: Paperclip <noreply@paperclip.ing>

* codex/sleep-12-decision-target-invalidation:
  fix(server): retain database precision in effect recovery cursor
  fix(server): preserve expiry paging timestamp precision
2026-10-24 15:24:26 -07:00
DottaandPaperclip a263b7e69a Merge decision recovery paging repairs
Co-Authored-By: Paperclip <noreply@paperclip.ing>

* codex/sleep-11-decision-effect-recovery:
  fix(server): retain database precision in effect recovery cursor
  fix(server): preserve expiry paging timestamp precision
2026-10-24 15:24:03 -07:00
DottaandPaperclip cebcd9c427 fix(server): retain database precision in effect recovery cursor
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-24 15:24:01 -07:00
DottaandPaperclip cbf7306989 Merge expiry paging repair into effect recovery
Co-Authored-By: Paperclip <noreply@paperclip.ing>

* codex/sleep-10-decision-expiry:
  fix(server): preserve expiry paging timestamp precision
2026-10-24 15:11:57 -07:00
DottaandPaperclip e89c682ba5 fix(server): preserve expiry paging timestamp precision
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-24 15:11:54 -07:00
DottaandPaperclip 2aa32647bf docs: record retention review repair and remaining cursor audits
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-24 14:54:48 -07:00
DottaandPaperclip a29ce265b0 fix(server): preserve database precision in retention recovery cursors
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-24 14:53:21 -07:00
DottaandPaperclip 2f334c678b docs: record approval retention local verification
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-24 14:43:35 -07:00
DottaandPaperclip ea3dd40cdd fix(server): schedule approval retention from durable source deadlines
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-24 14:43:07 -07:00
DottaandPaperclip f240381fd8 docs: record live review and CI evidence for sleep stack
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-24 14:10:38 -07:00
DottaandPaperclip b20a7f6741 docs: audit attention retention sources before scheduling migration
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-24 14:08:32 -07:00
DottaandPaperclip 706beb01f2 test(server): verify Grok dispatch lifecycle and quiet history
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-24 14:04:03 -07:00
DottaandPaperclip 1bbfc69590 fix(server): retain full target scans with scoped transaction hints
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-24 14:00:17 -07:00
DottaandPaperclip 5062efb368 fix(server): invalidate decision targets from committed changes
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-24 13:47:32 -07:00
DottaandPaperclip e7db8ce543 docs: audit decision target invalidation producers
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-24 13:37:37 -07:00
DottaandPaperclip a5b5994c1d fix(server): recover decision effects from saved claim deadlines
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-24 13:32:27 -07:00
DottaandPaperclip 67bf465e3b docs: record decision sleep PRs and effect recovery audit
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-24 13:21:18 -07:00
DottaandPaperclip 63b58cc26f test(server): bound and qualify decision expiry lifecycle
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-24 13:18:55 -07:00
DottaandPaperclip 5f96892a58 fix(server): serialize decision outcomes and bound continuation shutdown
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-24 13:14:07 -07:00
DottaandPaperclip 66bbef521e fix(server): wake decision continuations from committed changes
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-24 13:03:17 -07:00
DottaandPaperclip 7ea1fbd8e1 docs: record eighth sleep PR and decision continuation audit
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-24 12:46:24 -07:00
DottaandPaperclip 692e3ee02c fix(server): wake archive notification delivery from committed changes
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-24 12:22:58 -07:00
DottaandPaperclip 0956883a84 fix(server): wake tool-review card repair from committed changes
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-24 12:14:33 -07:00
DottaandPaperclip db2420ef1a docs: record sixth sleep PR and projection follow-up
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-24 10:55:36 -07:00
DottaandPaperclip 85003a7ac4 docs: record interrupted-tool recovery contracts and verification
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-24 10:54:03 -07:00
DottaandPaperclip 7a1cf03e62 test(server): require settled outcomes for approved Test-tab calls
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-24 10:49:28 -07:00
DottaandPaperclip 1cdb26a68b fix(server): separate tool outcome persistence from provider errors
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-24 10:47:01 -07:00
DottaandPaperclip 5dcd193d35 fix(server): recover approved tool actions from committed changes
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-24 10:38:39 -07:00
DottaandPaperclip 38c4e28135 docs: audit approved tool recovery ownership for next sleep slice
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-24 10:20:17 -07:00
DottaandPaperclip ce6a260a83 test(server): qualify pending review expiry lifecycle
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-24 10:16:29 -07:00
DottaandPaperclip 36e1c9f23e Merge branch 'codex/sleep-03-gateway-credential-expiry' into codex/sleep-04-tool-review-expiry
* codex/sleep-03-gateway-credential-expiry:
  perf(server): bound gateway expiry deadline discovery

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-24 10:09:27 -07:00
DottaandPaperclip c5c53fc43f perf(server): bound gateway expiry deadline discovery
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-24 10:07:58 -07:00
DottaandPaperclip 77d6a3bdc2 docs: record stack evidence and separate tool-review obligations
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-24 09:57:00 -07:00
DottaandPaperclip 75332b9641 fix(server): schedule gateway credential expiry from committed changes
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-24 09:54:24 -07:00
DottaandPaperclip fac808eabc fix(server): drive secret-proposal expiry from committed changes
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-24 09:41:03 -07:00
DottaandPaperclip a592550336 fix(server): schedule custom-image setup expiry from committed changes
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-24 09:33:06 -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) 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
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>
2026-10-24 05:08:56 -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>
2026-10-23 19:52:07 -07:00
Nicky Leach aa0037a137 Support Codex warm plugin task reuse and recovery (#17390) 2026-10-23 19:14:14 -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