Files
PaperClipAI/packages/adapter-utils
commitperclip[bot] 2d72f2c4c0 fix: Serialize ACPX session record saves per record to stop overlapping renames (EPERM on Windows) (#16925)
> [!NOTE]
> Opened by Prospector's automated issue-fix pipeline. A maintainer
reviews and decides; nothing here merges on its own.

## Thinking Path

- Issue #15346 reports a defect in paperclipai/paperclip.
- Root cause, as the fixing agent read it: ACPX saves one session record
from several writers (live checkpoint timer, turn end, finalize, close)
with no coordination; each save renames a temp file onto the same record
file, and on Windows overlapping renames onto one file fail with EPERM.
All of these saves go through Paperclip's session-store wrapper, which
forwarded them concurrently.
- This pull request: Serialize ACPX session record saves per record to
stop overlapping renames (EPERM on Windows)

## What Changed

-
`packages/adapter-utils/src/acpx-engine/ephemeral-session-environment.ts`:
Every ACPX save in Paperclip passes through this wrapper, so it now puts
all saves of one record id in a single queue (a promise chain shared by
all wrappers in the process). Saves of one record never overlap and keep
their order; a failed save still returns its own error and does not
block the next one; saves of different records still run in parallel.

## Verification

- Reproducing test(s), added in this PR:
`packages/adapter-utils/src/acpx-engine/ephemeral-session-environment.test.ts`
- They fail on the base commit and pass with the fix, each twice in a
fresh sandbox.
- 3 independent attempts wrote 3 reproductions; this fix passes every
one of them.
- The repository's compile command passes with the fix applied.
- 3 existing test file(s) near the change pass.
- The full test suite passes except for the 246 file(s) that already
fail on the base commit.

## Risks

- Written by an automated pipeline, not a person: review it as you would
an outside contribution.
- Risk tier of the touched paths: 2.
- The scope reviewer found it also changes, beyond the report: save()
called when no earlier save of that record is pending
- The scope reviewer found it also changes, beyond the report:
persistedStore.save throwing synchronously
- The scope reviewer found it also changes, beyond the report: Records
with missing/undefined acpxRecordId
- The scope reviewer found it also changes, beyond the report: save()
for different record ids

## Model Used

- opus, sonnet (Anthropic Claude, via Claude Code), writing and checking
the change in an isolated sandbox.

## Linked Issues

Fixes #15346

Other pull requests that name #15346:
- #15370 (closed): fix(acpx): serialize session file saves and clean
failed temporary writes

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

<!-- prospector:issue-fix v1 issue=15346 base=1c07b5903b1b
report=4fe3fefc8864e00f -->

Co-authored-by: commitperclip[bot] <284812584+commitperclip[bot]@users.noreply.github.com>
2026-10-26 20:55:37 -07:00
..

@paperclipai/adapter-utils

Shared utilities for Paperclip adapters: process spawning, environment injection, sandbox/SSH transport, workspace sync, and the round-trip helpers that move code between the local execution-workspace cwd and wherever the agent actually runs.

For the adapter-author guide see docs/adapters/creating-an-adapter.md and the in-repo notes at packages/adapters/AUTHORING.md.

Sandbox bridge deadlines

Command-managed bridge control operations (queue reads, input delivery, setup, and cleanup) have a host-enforced deadline of at most 30 seconds per shell command. A shorter configured timeout still applies. These operations cannot inherit the agent run's hours-long lifetime or wait forever on a provider that ignores its timeout. Long-lived agent session commands keep their own limits.

A control timeout reports a transport failure. It does not prove that the remote command stopped and does not replay an uncertain write. The process session closes failed input delivery and uses its existing shutdown path; normal execution settlement must still verify termination.

No-remote-git contract

The local execution-workspace cwd is the only persistence boundary across runs. No adapter may depend on a git remote for cross-run state.

Adapters that run the agent on a different host should use the SSH round-trip helpers in src/ssh.ts:

  • prepareWorkspaceForSshExecution({ spec, localDir, remoteDir }) — bundles the local cwd (tracked files, dirty edits, untracked additions, and the git history needed to reconstruct it) to remoteDir before the run starts. Runs with no git remote configured.
  • restoreWorkspaceFromSshExecution({ spec, localDir, remoteDir, ... }) — syncs the remote cwd back into localDir after the run, including any new commits the agent created. Also runs with no git remote configured.

prepareRemoteManagedRuntime in src/remote-managed-runtime.ts wraps both calls for adapters that want a per-run remote workspace and an automatic restoreWorkspace() finally hook.

The invariant is pinned by the no-remote-git contract case in src/ssh-fixture.test.ts, which asserts that a remote-only commit propagates to the local worktree through the prepare → restore round-trip with no git remote configured at any point. Do not regress that test.