Commit Graph
3 Commits
Author SHA1 Message Date
Devin FoleyandPaperclip 188bb14ba0 fix: evict stale workspace seed generations instead of freezing (#17357)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs in remote sandboxes upload the workspace as seed
archives, and a per-company cache keeps recent seeds so a later run with
the same workspace state can reuse them
> - The cache only ever added seeds; at its limits it stopped accepting
new ones and never removed old ones
> - On one instance it had filled to its 8 GiB cap with week-old seeds
and froze, so it held disk without helping new runs
> - This pull request expires seeds unused for 7 days and evicts the
least recently used seeds when a new one needs room
> - The benefit is a cache that stays useful within the same limits and
gives back space held by stale seeds

## Linked Issues or Issue Description

Bug report described in-PR (bug report template).

**What happened?**
On one instance, the company's seed cache
(`native-workspace-sync/generations/<company>/`) sat at its 8 GiB cap:
209 generations across 169 workspaces. 107 generations (8.2 GB) had not
been touched for 7 days or more. Because publishing stops at the limits,
new workspace states were no longer cached, while seeds for old states
and deleted workspaces stayed forever.

**Expected behavior**
Stale seeds are removed, and a new seed can replace the least recently
used ones when the cache is full.

**Steps to reproduce**
1. Run enough native sandbox runs across workspace states to reach a
seed cache limit.
2. New seeds are no longer published, and nothing is ever removed.

**Paperclip version or commit:** `master` at 837add0caa.
**Deployment mode:** any with native remote-sandbox runs.
**Agent adapter(s) involved:** native sandbox runtimes
(`packages/adapter-utils` seed cache).

## What Changed

`packages/adapter-utils/src/workspace-seed-cache.ts`:
- **Use tracking:** on a verified hit, `readWorkspaceSeedGeneration`
takes the company lock, confirms the receipt is still the one it
verified, and touches it, so its mtime records the last use. Eviction
holds the same lock from reading timestamps to removing, so a hit is
either recorded first and protected, or already gone and reported as a
miss. The reader accepts a `companyDirectory` option, which the sandbox
runtime now passes as it already does for publishing.
- **Idle expiry:** `publishWorkspaceSeedGeneration`, under the existing
company lock, first removes generations unused for
`WORKSPACE_SEED_CACHE_EVICTION.idleMs` (7 days).
- **LRU eviction:** if the new seed still does not fit, it evicts the
least recently used generations until it does: within the workspace for
workspace limits, across the company for company limits.
- **What is never evicted:**
- a generation used within `minIdleMs` (1 hour), since a run copies a
cache hit shortly after the lookup;
  - the generation being published;
- abandoned `.pending-*` directories, which still count toward the
limits as before.
- **Removal:** renames to `.evicted-<uuid>` before deleting, and removes
leftovers of an interrupted eviction. All empty workspace directories
are removed, so they no longer hold workspace slots.
- **Identity check:** publication rechecks the company directory's
identity under the lock before evicting, so a directory replaced during
verification is never touched.
- **Verification:** optional verification still runs only when the seed
can fit after eviction. A dry eviction plan decides this before
verification, and the real eviction happens under the lock.
- `publishWorkspaceSeedGeneration` accepts `eviction` and `now` options
for tests; the limits are unchanged.

No code comments were added.

## Verification

- `pnpm exec vitest run src/workspace-seed-cache.test.ts
src/workspace-checkpoint.test.ts` (in `packages/adapter-utils`): 58
passed. With `sandbox-managed-runtime.test.ts`: 168 passed. New cases:
- **Idle expiry:** generations unused for 7 days are removed on publish,
emptied workspaces are removed, newer ones stay.
  - **Use tracking:** a cache hit protects an otherwise idle generation.
- **Workspace LRU:** the least recently used generation of a full
workspace is evicted.
- **Company LRU:** company pressure evicts across workspaces, but never
a generation used within the last hour.
  - **Verification:** it runs only when eviction can make room.
- **Pending directories:** abandoned ones are never evicted, and
verification is skipped when they fill the cache.
  - **Leftovers:** remnants of an interrupted eviction are removed.
- **Concurrent read:** an eviction paused just before removing a seed
makes a concurrent read report a miss, never a hit whose files vanish.
- **Replaced directory:** a nonempty company directory swapped in during
verification keeps its seeds.
- **Empty workspace:** an abandoned empty workspace directory no longer
holds the only workspace slot.
- **Mutation checks:** removing any of these fails at least one case:
  - the touch on read;
  - idle expiry;
  - the recent-use guard;
  - the plan before verification;
  - leftover cleanup;
  - the pending-directory guard;
- the read lock (the concurrent-read case then returns a hit whose files
are gone);
  - the identity recheck;
  - removing only workspaces that had generations.

The existing `workspace-checkpoint` end-to-end test caught an earlier
version that ignored pending directories when deciding whether to
verify.
- `pnpm exec vitest run src` (in `packages/adapter-utils`): all seed
cache and checkpoint tests pass. Two runs on a heavily loaded machine
each had unrelated failures (`github-launcher.test.ts`,
`acpx-engine/execute.test.ts`). One run also failed a seed cache test
once; I could not reproduce that in 21 further runs, including 6 in
parallel.
- `pnpm exec vitest run native-workspace-sync` (in `server/`): 23
passed.
- `pnpm run typecheck` (in `server/`, after building
`packages/adapter-utils`): pass.
- Not run: server build and boot smoke.

## Risks

- **First publish after deploy:** on an instance with a full cache of
old seeds, it removes them, about 8 GB on the instance above. The
removal is a rename plus a recursive delete under the company lock.
- **Eviction during use:** a run that looked up a seed more than an hour
earlier and has not yet copied it could see it evicted. It would fail
that preparation and retry. In practice the copy follows the lookup
within the same preparation.
- **No new settings:** the 7-day and 1-hour thresholds are constants
exported as `WORKSPACE_SEED_CACHE_EVICTION`.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), 1M context, through Claude Code
with tool use (shell, file edits, tests, read-only measurements on a
live instance).

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-23 14:05:34 -07:00
DottaandPaperclip ce0eff7d1f perf: reduce fresh workspace clone and sandbox staging waits (#16487)
## Thinking Path

> - Paperclip manages AI agents and their work.
> - Each run needs a private workspace and safe persistence.
> - Fresh isolated repository sources download upstream Git history
again.
> - Sandbox staging also verifies an optional seed cache before it
uploads the workspace.
> - That verification holds a company lock during a full extraction and
hash scan.
> - This pull request reuses pristine Git objects through an
authenticated remote clone.
> - It also overlaps seed verification with upload and moves
verification outside the publication lock.
> - The benefit is less repeated transfer and less serial preparation
work, with the same task isolation and recovery contract.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

Fresh task-private repository preparation and inbound sandbox workspace
staging.

**Current behavior**

Each isolated remote source clones the full upstream history. Optional
seed verification then extracts and hashes another full tree before
upload. One workspace's verification can hold the company cache lock
while other workspaces wait.

**Proposed behavior**

Use a company and credential scoped cache of pristine upstream objects
for GitHub HTTPS sources. Each task still clones from the current
remote. It disconnects from the cache before publication. Run seed
verification alongside upload. Hold the company lock only for final
capacity checks and publication.

**Reason and benefit**

These operations lie on the workspace preparation path. Removing
repeated Git pack transfer and serial verification can reduce startup
time. Local measurements below cover these boundaries. They do not
establish hosted agent latency.

Related: #16425, #16454, #16398, #16461, #16468, #16473. The cache
inspection and durable folder proposal in #13376 covers separate product
work.

## What Changed

- Add a pristine upstream Git object cache for fresh isolated GitHub
HTTPS sources. Key it by company, repository and current credential
fingerprint. Do not store credentials.
- Keep online clone authorization and current remote refs. Use
`--reference-if-able --dissociate` so task objects remain independent. A
reference failure retries once online without the cache and resolves
credentials again.
- Cap cache storage at 64 entries and 4 GiB per company, with 2 GiB per
entry. Skip contended publication. Bound priming by a cooperative
five-second budget and a 4,096-entry scan allowance. Keep the existing
free-space reserve.
- Overlap optional seed verification and required workspace upload.
Settle both before returning or deleting temporary files. Recheck quotas
and company directory identity under the publication lock.
- Keep the independent fresh upstream reproducibility proof, task
commits, dirty source copies, ignored files, nested repositories and
durable recovery behavior.
- Suppress optional Git server-info generation during dissociation so
generated storage passes the existing safe-expiration checks. This uses
[the supported Git
setting](https://git-scm.com/docs/git-config/2.43.0#Documentation/git-config.txt-repackupdateServerInfo).
- Add bounded timing phases, regression coverage and two repeatable
local benchmarks. No first-party Telemetry or dependency changes.

## Verification

- `pnpm -r typecheck`: passed.
- `pnpm build`: passed.
- Affected adapter suites: 240 tests passed. Native workspace
persistence/history and tracing: 33 tests passed. Server
checkout/isolation/tracing batch: 53 tests passed. Final object-cache
authority, path and capacity suite: 12 tests passed. These batches
overlap.
- Cache and expiration/restoration lifecycle batch: 34 tests passed. A
direct generated-storage check covers cached clones.
- Real HTTP Git fixture: a matching cache does not bypass revoked remote
access. Other fixtures cover current refs/tags, hidden cached commits,
independent objects after cache removal, corruption fallback,
credential/company scope, quota limits and publication contention.
- Initial restricted runs hit SQLite write and Unix socket permission
failures. The adapter batch passed with disposable writable instance
state and socket access. The first typecheck rejected the test use of
`Promise.withResolvers`; typed deferred gates now use the supported
library contract.
- Repository benchmark on final head
`9047368bf3b7fa2061e00d698b8b51cf4ae1da32`: five alternating samples,
real local `file://` Git transport, synthetic 32 MiB history, no
injected latency. Median 1143.8 ms to 240.5 ms; ranges 1085.7–1486.6 ms
and 188.0–254.8 ms. Received Git pack bytes fell from 33,569,788 to
zero. Initial cache priming took 1208.4 ms and is reported separately.
- Sandbox benchmark, five alternating samples per mode on 10,000 files
with real packing, extraction and verification: with a controlled
3-second transfer delay, median 9.450 s to 7.620 s (ranges 9.280–12.115
s and 7.296–7.983 s). With no injected delay, median 6.808 s to 6.450 s
(ranges 6.255–7.029 s and 5.709–6.902 s). The no-delay ranges overlap;
local copying competes with verification for disk. Both modes verify
final tree contents, ignored-file exclusion and Git HEAD. These are
synthetic local fixtures, not hosted agent runs.
- Reproduce with `pnpm --filter @paperclipai/server exec tsx
../scripts/benchmark-repository-object-cache.ts` and `pnpm --filter
@paperclipai/server exec tsx ../scripts/benchmark-workspace-staging.ts
--baseline-ref=307eb16c0c --transfer-delay-ms=3000`.
- Prior head `af766bc820` received Greptile 5/5 with no findings. Its
E2E shard 7 failed in `plan-task-fixture` cleanup: company deletion
raced an `activity_log` foreign key. That fixture has no provider run or
Git workspace. The failure is retained here; final-head CI must pass.
The corrected corruption fixture now forces a pack and verifies a real
damaged archive. Current head `9047368bf3b7fa2061e00d698b8b51cf4ae1da32`
has Greptile 5/5 with no review threads or findings. Final head has 55
passing checks and two optional Storybook skips. [The first
workspace-shard
attempt](https://github.com/paperclipai/paperclip/actions/runs/38802441857/job/116655485350)
passed all 1,778 cases, then reported a late `EPIPE` from unchanged
`server-utils.ts` while the ACP reuse test was active. That case mocks
workspace preparation and passed locally in isolation. [Its single retry
on the same
commit](https://github.com/paperclipai/paperclip/actions/runs/38802441857/job/116659728034)
passed, as did the dependent verification gate. The earlier E2E cleanup
fixture passed on this final head. Both initial failures remain recorded
here. A full local suite was not duplicated; the affected boundaries
were run locally and the complete suite runs in CI.

## Risks

- First cache population adds local copying. Cold clones, credential
rotation, capacity exhaustion and a corrupt cache retain the online
clone path. No eviction is added. Cached data is bounded, but many
companies can each consume their allowance.
- Individual filesystem calls and cleanup can exceed the cooperative
priming budget. Failed reference clones make one additional online
attempt.
- Parallel verification and upload compete for disk and CPU. The local
no-delay benchmark shows a smaller gain than a slow-transfer case.
Verification still checks contents before cache publication, and
required upload failures remain failures.
- Local fixtures do not prove hosted throughput or the 15–30-second
end-to-end target. The fresh checkout still writes its working tree, and
model startup, copyback and other preparation costs remain.

## Model Used

OpenAI Codex, GPT-6 family. The exact serving model ID and context
window are not exposed in this session. Used reasoning, source editing,
shell tools and test execution.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` /
`Closes: #` / `Refs: #` OR (b) described the issue in-PR following the
relevant issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change and contains no internal task
identifiers
- [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-16 12:21:47 -05:00
DottaandPaperclip f329b24ee1 feat: give tasks durable project-optional workspaces (#15749)
Give tasks durable company-scoped workspace bindings independent of project assignment, channel defaults and authorized repository preparation. Reuse verified input seeds and sparse output recovery while preserving trust, privacy and controller provisioning boundaries.

Full CI and E2E, Superagent, legacy checks and Greptile Apex 5/5 passed on reviewed head 753680b61c. All review findings are resolved; the PR records focused verification and historical acceptance limits.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-12 20:45:18 -05:00