mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-08 11:13:44 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The heartbeat service records each agent turn and releases its working files. > - A turn can save its work and finish before instruction-copy cleanup runs. > - A cleanup exception can replace that completed result with an adapter failure. > - This also loses result accounting and can prevent environment lease release. > - This pull request records a cleanup warning and keeps the original run outcome. > - The existing recovery sweep retries cleanup from the durable working-copy record. ## Linked Issues or Issue Description Related cleanup and lock work: #14866 and #14869. Related, distinct work: #14695 retains warm-process files; #12021 handles provider-process SIGTERM after a terminal result. **What happened?** An agent saved its plan, posted a comment, and requested approval. The provider completed its turn. Instruction-copy cleanup then timed out on a directory lock. Its exception escaped a `finally` block and replaced the provider result, so the completed turn showed `Run failed`. **Expected behavior** Keep the provider outcome, usage, cost, saved work, and pending approval. Record a cleanup warning and let the existing recovery sweep retry. A real provider failure must keep its original error. A failed file save must keep its failed-save receipt. **Steps to reproduce** 1. Complete a legacy adapter turn that saves work and requests approval. 2. Make instruction-copy release throw a directory-lock timeout. 3. Read the run result. Before this change, the cleanup error replaces the provider outcome. The new heartbeat tests reproduce the failure without a live provider or external service. **Paperclip version or commit** The regression reproduces on `c83df091b1a5207375eaf23466bb5c62e4e1518e`. This branch is rebased onto `cf8ad63c80`. **Deployment mode** Server-managed agent execution with persistent instruction working copies. ## What Changed - Catch instruction-copy release failures in both heartbeat teardown paths. Stop repeating a failed cleanup attempt within the same run. - Write a sanitized `instruction_cleanup` warning. A warning-write failure also preserves the run result. - Test successful, failed, and throwing providers; both teardown paths; warning-write failure; accounting; approval state; and execution-control release. - Extend the held-lock test to prove a fresh recovery worker removes the deferred copy and preserves its failed-save receipt. - Document deferred cleanup and the run-log event. ## Verification - Red proof: all five new heartbeat regression cases fail with the original release calls. - At head `20bea4f431c916d2f5db1970213aab85f5daa34c`, all 417 tests passed across heartbeat process recovery, agent directory working copies, and directory merge locks. - Full local `pnpm -r typecheck`, `pnpm build`, and `git diff --check` passed. - [GitHub CI](https://github.com/paperclipai/paperclip/actions/runs/37031119795) passed at this head. All 53 reported checks passed; the two Storybook checks were correctly skipped. This includes general and serialized tests, browser shards, runner verification, build, typecheck, and the canary dry run. - Greptile reviewed this head with 5/5, no code comments, and no unresolved review threads. The branch has no merge conflicts. - The local `pnpm test:run` attempt was stopped after it reported eight failures in unchanged suites. Four Slack/AgentMail cases selected an unrelated ancestor skills directory and failed with `ENOENT`; the two Slack cases passed with a temporary local skill-root link, which was then removed. Three company-skill cases reproduced macOS `EACCES` errors when renaming read-only cache directories. One gateway case passed when rerun alone. No full local-suite pass is claimed; the complete CI test jobs passed. ## Risks - Cleanup errors now leave recovery work pending. The durable working-copy record remains available for the existing retry sweep. - This change preserves provider failures and failed-save receipts. It does not claim that unsaved file edits were saved. - Native instruction reservation errors retain their existing behavior because they guard process ownership. - No schema, lockfile, workflow, API, or UI changes. ## Model Used OpenAI Codex, based on GPT-6, with reasoning, repository tools, and code execution. The exact backend model ID and context-window size are not exposed by this session. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge Co-authored-by: Paperclip <noreply@paperclip.ing>
216 lines
13 KiB
Markdown
216 lines
13 KiB
Markdown
# Persistent agent files
|
||
|
||
Each managed agent has one current directory, scoped by company and agent. The
|
||
Instructions Editor reads and writes this directory. `AGENTS.md` (or the
|
||
configured entry) is one file in it. Agents may create ordinary files and nested
|
||
folders for notes, memory, and other personal working material. Files in the
|
||
task working directory remain task files.
|
||
|
||
Agents can edit their own managed files under the responsible user’s current
|
||
target permissions. Access to another agent’s files additionally requires the
|
||
caller’s own target-scoped configuration permission; shared company membership
|
||
or a responsible user alone does not grant peer access.
|
||
|
||
## Layout
|
||
|
||
The canonical host directory keeps its existing physical location:
|
||
|
||
```
|
||
<instance>/companies/<company>/agents/<agent>/
|
||
instructions/ current agent files (editor)
|
||
AGENTS.md
|
||
notes/
|
||
any-supported-file
|
||
file-sync/ controller-only operational state
|
||
adopted.json
|
||
canonical-manifest.json metadata/hash cache, no file contents
|
||
runs/<initial-run>/
|
||
owner.json current run owning the writable copy
|
||
live/ writable copy for the provider lifetime
|
||
```
|
||
|
||
The process starts in its existing task workspace. `AGENT_HOME` points to the
|
||
registered writable agent copy. Adapter `HOME` and `CODEX_HOME` keep their
|
||
existing meanings and are not personal-file storage. Local copies are outside
|
||
the task workspace. Remote providers currently confine file sync to their
|
||
workspace: their independent agent copy therefore lives under the excluded
|
||
`.paperclip-runtime/agent-files/<agent>/<run>/` area. It is not included in task
|
||
workspace sync, Git staging, or task deliverables.
|
||
|
||
Regular files (including binary bytes) and directories are supported, up to
|
||
100,000 entries (files and folders), 256 MiB per file and 2 GiB total. Symlinks,
|
||
hardlinks, and special
|
||
files are rejected, rather than followed or silently skipped. The instruction
|
||
entry remains valid UTF-8, at most 1 MiB, and cannot be deleted. The editor edits
|
||
text up to 1 MiB and offers downloads for binary or larger files. The reserved
|
||
`.paperclip-runtime` directory and the compatibility-only virtual file
|
||
`promptTemplate.legacy.md` are not user storage. Task cache and Git ignore
|
||
exclusions do not apply to this directory.
|
||
|
||
These storage limits are separate from the 1 MiB instruction/editor limit. Large
|
||
files are hashed and downloaded as streams; listings bound concurrent reads,
|
||
and only editor-sized text is buffered. Storage counts uncompressed file bytes,
|
||
not allocated disk
|
||
blocks. These are sync validation limits, not live filesystem quotas: an agent
|
||
can write beyond them while running. Storage limits never pause an agent, fail
|
||
a provider run, or block future task admission. A folder at or above a limit
|
||
produces a warning on each run until enough files have been removed or shrunk.
|
||
Existing saved files are restored even when already over quota, so the agent
|
||
can continue working and clean them up with ordinary filesystem tools. Unsafe
|
||
paths and links still fail validation; bypassing a storage quota does not
|
||
bypass those checks.
|
||
|
||
An API save above a storage limit returns 422 without changing the saved files.
|
||
If a stopped run exceeds a storage limit, none of its agent-folder changes are
|
||
saved. The run shows a nonblocking storage warning and its save receipt reports
|
||
`AGENT_FILES_LIMIT_EXCEEDED` with the specific limit
|
||
and, for an oversized file, its path. The previous saved folder is used on the
|
||
next run. The temporary run copy is discarded, including on a limit failure;
|
||
there is no retained recovery archive or partial-save option. Transient sync
|
||
failures get up to three attempts at the stop boundary before cleanup and an
|
||
explicit failure receipt. Individual file writes are atomic, but an I/O failure
|
||
partway through a sync can leave some files updated; a failed receipt does not
|
||
claim whole-folder success.
|
||
|
||
Sync failures are diagnostics for the affected run, not errors on the current
|
||
files in the Instructions Editor. Historical failures remain in the run log;
|
||
the run detail also shows warnings from its save receipt. The editor only shows
|
||
preserved instruction-only candidates that may need review, alongside errors
|
||
from the current browser edit. Later successful saves do not erase run history.
|
||
|
||
Initial restoration copies the canonical folder once. Warm native Codex turns
|
||
reuse that working directory. Checkpoints enumerate file metadata, hash files
|
||
whose identity/size/mode/mtime/ctime changed, and temporarily copy and transfer
|
||
only changed file contents. Deletions and empty directories travel as manifest
|
||
entries. An unchanged image is neither rehashed nor recopied after its first
|
||
checkpoint. A modified file is transferred in full; this is a file-level delta,
|
||
not block-level deduplication. Canonical hash caches and manifests contain no
|
||
file contents. Temporary checkpoint payloads are removed after application.
|
||
The operator still provisions storage for the canonical folders, active working
|
||
copies and changed-file payloads; these are not aggregate disk quotas.
|
||
|
||
## Run lifecycle
|
||
|
||
1. Under the agent lock, restore current files into a private run copy and save
|
||
a baseline of paths, kinds, modes, and hashes. This is sync metadata, not a
|
||
revision history.
|
||
2. Stage the copy through the existing workspace transport. Point `AGENT_HOME`
|
||
and instruction guidance at that registered root.
|
||
3. For warm native Codex, capture a manifest and changed-file payload at each
|
||
terminal turn boundary before admitting another turn. Check file metadata
|
||
before and after streaming and hash the captured payload independently on
|
||
the host. Retry an unstable checkpoint up to three times; if it cannot be
|
||
validated, close the owned provider and perform the stopped collector. Other
|
||
execution paths retain their stopped-provider collection boundary.
|
||
4. Recheck the responsible user's current authorization. Under the same agent
|
||
lock used by editor writes, apply only files changed or deleted relative to
|
||
the last acknowledged baseline. For a competing edit or deletion of the same
|
||
file, the last synchronization to acquire the lock wins. Unchanged files do
|
||
not overwrite another run's changes; newly added unrelated files survive.
|
||
5. Record the save receipt and advance the baseline only after application. A
|
||
warm session keeps its directory and hands ownership to the next run using
|
||
a controller-owned marker. Old callbacks cannot collect or remove the next
|
||
owner's files. On session retirement, collect any later writes and remove
|
||
the private directory. No per-run file versions or conflict copies accumulate.
|
||
|
||
These are validated **per-file checkpoints**, not an atomic snapshot of arbitrary
|
||
background writers across an entire directory. Writes after a checkpoint remain
|
||
pending until the next checkpoint or verified session retirement. A save receipt
|
||
acknowledges only the captured bytes. Lost remote bytes or missing stop proof
|
||
cannot become a successful save.
|
||
|
||
Warm reuse requires the actual live session, the same remote environment and
|
||
provider lease, and unchanged canonical files since its last checkpoint. An
|
||
editor or another task changing canonical files retires that session before a
|
||
fresh copy is restored. A directory-path mismatch also forces retirement. Only
|
||
the loaded instruction entry participates in the runtime instruction digest;
|
||
ordinary memory/image edits do not change it. Changes to loaded instructions,
|
||
policy, credentials or provider configuration may still replace the process.
|
||
Relative supporting files are read from `AGENT_HOME`, not the read-only prompt
|
||
snapshot. External instruction bundles remain read-only and use their existing
|
||
lifecycle.
|
||
|
||
The editor supplies the hash of the file it read. A stale browser save returns
|
||
409 and retains the user's unsaved draft. Run synchronization itself uses
|
||
per-file last-sync-wins: a later run can overwrite a saved browser edit to the
|
||
same file. There is no text merge or historical copy to recover the overwritten
|
||
version. Ordinary task files continue using their existing workspace contract.
|
||
|
||
## Upgrade and recovery
|
||
|
||
Migration 0287 creates the preview tables idempotently after master’s 0285/0286.
|
||
Existing preview receipts, rows, constraints, and pending captures are retained.
|
||
On first use, while holding
|
||
the agent row lock, import any deployed revision heads into the existing managed
|
||
directory once. A controller-owned marker outside agent files prevents any
|
||
later replay of those heads. Existing revision rows remain readable for recovery;
|
||
new saves never append to them. Old UUID-based clients receive content tokens
|
||
and can still submit their previously recorded revision IDs, which are checked
|
||
against the corresponding bytes before a write.
|
||
|
||
Working-copy receipts and native runtime inputs record the new file contract.
|
||
A restored native session with no contract field keeps the old instruction-only
|
||
copy shape, prompt digest, paths, and collector. Its writes use the compatibility
|
||
bridge into current files, with the original baseline fence. Existing pending
|
||
legacy candidates remain resolvable. Neither old task workspaces nor arbitrary
|
||
external instruction roots are imported as agent directories.
|
||
|
||
Stock-agent and plugin resets update their declared files while retaining unrelated
|
||
personal files and formerly configured entries. Automatic stock upgrades first
|
||
record baseline hashes in the existing resource binding, then apply and finalize
|
||
under the agent lock. A failed file write or database commit retries against
|
||
those hashes and already-applied bytes. Removed, unchanged stock files are
|
||
removed; intervening personal edits stop the retry. This pending operation
|
||
metadata is cleared on success and does not retain file revisions.
|
||
|
||
External bundles retain their existing behavior. Their migration to managed
|
||
storage is an explicit configuration action. Historical task cwd, provider-home,
|
||
checkpoint, and workspace restoration formats are not rewritten.
|
||
|
||
Backups must include the persistent instance filesystem as well as the database.
|
||
New current-file bytes are not database revision rows. Old instruction-only
|
||
candidates are retained solely for upgrade compatibility.
|
||
|
||
Crash recovery can collect a stopped working copy without starting a model.
|
||
Cleanup does not wait for a directory lock before process-stop proof exists, or
|
||
after the copy is superseded or cleanup is complete. An unavailable copy keeps
|
||
its failed-save receipt. Recovery can later clean an unavailable remote copy
|
||
after destruction of its exact lease and executes no remote command. An
|
||
unavailable local copy can still contain uncollected edits; this cleanup path
|
||
preserves those bytes even if local stop proof arrives later.
|
||
Deferred cleanup retries after a
|
||
delay so one blocked copy does not prevent other copies from being cleaned.
|
||
If releasing a run's instruction copy fails, the run records a cleanup warning
|
||
and leaves the durable copy for the recovery sweep. Cleanup does not replace the
|
||
provider's result, discard usage accounting, or prevent environment lease release.
|
||
It does not claim that unsaved agent-file changes were saved; collection failures
|
||
keep their separate failed-save receipts. A run attempts failed cleanup only once
|
||
before handing it to recovery, rather than repeating the lock wait in teardown.
|
||
Re-preparing an existing run uses the same lock as cleanup and rechecks its
|
||
receipt under that lock. Preparing a new run keeps its separate admission path.
|
||
Missing stop proof or lost remote bytes produce a visible diagnostic, never a
|
||
save receipt. An interrupted apply can replay its changed files with the same
|
||
last-sync-wins rule. Cleanup resumes for terminal runs; no copy is retained as
|
||
an archive after cleanup succeeds.
|
||
|
||
## Verification
|
||
|
||
`agent-directory-working-copies.test.ts` exercises nested/binary files, directory
|
||
isolation, last-sync-wins edits and deletions, terminal cleanup, link rejection, old-head
|
||
adoption, and stable prompt digests. The legacy working-copy and native-tool
|
||
suites exercise compatibility. Workspace merge tests exercise preflight and
|
||
interrupted replay.
|
||
|
||
The explicit Product E2E `instruction-persistence` suite creates a file through
|
||
the browser editor, runs an agent that changes instructions and supporting files,
|
||
checks exact binary bytes via the public download route, restarts the server,
|
||
and asks a fresh task to prove restored contents using an independent nonce. A
|
||
third task edits its entry while the browser saves that same file; the later
|
||
run sync wins while a separate browser-created file survives, with no conflict
|
||
candidate or manual resolution. Three more tasks save a sparse file at its
|
||
256 MiB boundary, exceed that boundary with a nonfatal save rejection, then
|
||
remove it and save a new small file. All tasks must succeed, with warnings
|
||
visible in run details while full and cleared after cleanup.
|
||
Run results, including unavailable credentials, must be reported separately from
|
||
unit or matcher results; a passing matcher does not prove a live run.
|