Files
PaperClipAI/packages/adapter-utils
d7ed84c775 fix(adapter-utils): charge duplex bridge body admission from the declared length
Round 4 of the bridge body admission fix. reserveBodyRead and readBody
took a flat maxBodyBytes charge for every active read. That charge was
too large for a small control-plane call and too small for the file
gateway, whose real retained peak reaches several times the raw body
size once it decodes, re-serializes, and re-encodes the body.

Read the Content-Length header before the gateway reserves anything.
Reject a body that declares more than the cap with a clean 413, before
the gateway reserves or reads one byte. When the header is absent (a
chunked request), fall back to the cap as the worst case, the same as
before.

Charge the reserve and the read buffer from the declared length times a
named per-gateway factor:
- The duplex gateway adds a fixed frame allowance to the declared
  length, because it holds one body buffer plus at most one in-flight
  frame that does not grow with the body.
- The file gateway multiplies the declared length by a factor of 15,
  the sum of the four representations it holds: the raw buffer, the
  decoded string, the JSON-escaped string, and the write buffer. The
  comment on the constant names each one and its worst-case size.

Fix the outbound stdout writer so a stream error settles every queued
write, not only the one at the head of the queue. The old top-of-loop
check discarded queued entries with no reject call, and the resume path
after a stalled write could remove and orphan a line a caller queued
after the stream had already failed. Both left a promise that never
settled, which stalled the reserve release and shed every later request
until the process restarted.

Add unit tests for the writer fix and end-to-end tests on both gateways
for the declared-length admission model: the pre-reserve 413, the
worst-case fallback for a request with no declared length, many small
concurrent requests the old flat charge would have shed, and the
admission-bound math at the new per-gateway reserve.

Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 20:16:52 +00: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.

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.