paperclip/packages/adapter-utils
Dotta 9031516a7e
fix: recover legacy Daytona startup failures from task and inbox (#13272)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Legacy conversation adapters can run in Daytona sandboxes.
> - A server restart during provisioning can occur before the invocation
event exists.
> - Recovery then lacks the old adapter identity and leaves a hold that
ordinary user retries cannot clear.
> - A remote launch can also fail when its host relay looks for Node in
the sandbox PATH.
> - This pull request records the adapter at claim time and restores
explicit user continuation after verified cleanup.
> - Users can recover from the task or inbox while the failed run and
uncertain action history remain intact.

## Linked Issues or Issue Description

Refs #13237, #13239, #13254. Those changes cover recorded conversation
runs, native user continuation, and explicit remote Stop. This change
covers legacy failure before `adapter.invoke` and exact task/inbox
Retry.

Refs #9771 for overlapping generated-command quoting. This change also
supplies the absolute host Node executable. Refs #13163 and #13264 for
the separate native restart and retained-workspace work.

**What happened?**
A legacy Daytona run interrupted during provisioning became
`process_lost` without an invocation event. Recovery preserved an
execution hold, and Retry or a new task reply could not resume it.
Cleanup could also run before the Daytona plugin was ready. On a macOS
host, a subsequent ACP relay launch failed with `env: node: No such file
or directory` because the remote launch environment did not contain the
host Node path.

**Expected behavior**
An interrupted conversation can continue after its previous execution
stops. Explicit Retry and new user replies should start a fresh turn
with the task history. Cleanup failures must remain visible and
recoverable. The host relay must use the host Node executable.

**Steps to reproduce**
1. Use a legacy Claude adapter with a Daytona environment.
2. Interrupt the server after it acquires the sandbox lease and before
it records `adapter.invoke`.
3. Restart and inspect the task hold.
4. Retry from the task or inbox, or send a new task reply.
5. Confirm the old sandbox has stopped and one new response arrives.

**Paperclip version or commit**
Reproduced from master at `3bafac12f796fbea02e609e1074a9639f872e9c4`.
The branch is rebased on `51b0e01ea`, including #13261 and #13270.

**Deployment mode**
Built from source on macOS with a real Daytona sandbox and the legacy
Claude ACP adapter.

## What Changed

- Count new browser specs with the scheduler's median duration in the
shard-balance check. This fixes a false policy failure after new specs
arrive from both branches. The balance threshold is unchanged.

- Persist server-owned adapter identity in the queued-to-running claim
before provisioning starts.
- Wait for provider plugin startup before restart cleanup. Keep failed
cleanup leases as active ownership blockers.
- Admit exact board retries and new user comments after verified
termination. Retain the old run, task history, approvals, and unknown
action outcomes.
- Adopt repeated Retry requests. Permit one scoped cleanup attempt per
explicit user Retry after the automatic limit, with an activity record.
A later user Retry can recover after a transient provider failure;
automatic attempts remain capped.
- Resume replies deferred during cleanup, including historical legacy
startup failures.
- Launch the host ACP relay through the absolute host Node executable.
- Add a task-level Retry button and return actionable blockers when
retry admission is refused.
- Add database regressions and three browser recovery journeys. Exclude
installed third-party dependency skills from the shipped-skill audit.

## Verification

- Current head: `d23c84181`, rebased on `51b0e01ea`. Conflict resolution
retains the saved-message recovery, local stop receipts, and wait
reasons from #13270 alongside exact legacy Retry support.
- Real Daytona: interrupted the server after lease acquisition and
before adapter invocation. Restart cleanup confirmed provider
termination. Task Retry cleared a seeded historical hold and a real
Claude agent returned `Recovery verified.` in the task. Removed the
disposable sandbox and environment after testing.
- All three browser recovery journeys passed again after the final
rebase. Task Retry, Inbox Retry, and a new reply each produced one fresh
successor, completed the task, preserved the failed run, and retained
the answer after reload.
- All 29 e2e/server shard-partition tests passed. The balance check now
uses the scheduler's median fallback for unmeasured specs, with the same
balance threshold.
- Server typecheck passed after rebuilding the generated runner
dependencies. The combined recovery/route run passed 136 of 137 tests.
Its remaining route test timed out during the first cold module import
at its explicit 10-second limit; an isolated rerun reproduced that
timeout and passed the other 51 route cases. The complete CI suite
passed on this head. The same route file passed all 52 cases in CI,
including the first cold import in 7.5 seconds.
- Before the final rebase, recursive typecheck, full build, UI token
gates, 132 targeted server tests, and the complete [CI
workflow](https://github.com/paperclipai/paperclip/actions/runs/34650004085)
passed. The subsequent CI failure was the shard-balance accounting
mismatch fixed here.
- Greptile reviewed `d23c84181` at 5/5 with no outstanding actionable
findings. The complete [current CI
workflow](https://github.com/paperclipai/paperclip/actions/runs/34653327949)
passed on attempt 2. All test, typecheck, build, and canary jobs passed
on the first attempt. Docker setup timed out fetching BuildKit from
Docker Hub; retrying that job and its dependent aggregate succeeded.

## Risks

- Recovery admission changes executable authority. Company, task, agent,
user, approvals, process ownership, and provider termination checks
remain required.
- Explicit continuation starts a fresh conversation with history. It
does not certify unknown external action outcomes or rerun
non-conversation adapters automatically.
- Changing task status alone does not clear an execution hold. The task
now offers an explicit Retry action.
- Historical adapter claims and invocation events take precedence over
current agent settings. Known process or webhook runs retain their hold.
Pre-upgrade rows with no adapter evidence may receive only a new
explicit user turn after termination proof; they do not become eligible
for automatic replay.
- No schema migration or sandbox-image change is required. This branch
has not been deployed to production.

## Model Used

OpenAI GPT-6 through Codex, with repository inspection, code execution,
browser automation, and test execution. The exact deployment model ID
and context window are not exposed in 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
- [ ] 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-09-11 18:08:14 -05:00
..
src fix: recover legacy Daytona startup failures from task and inbox (#13272) 2026-09-11 18:08:14 -05:00
CHANGELOG.md fix: bound workspace Git scans (#11572) 2026-08-17 22:11:30 -05:00
README.md PAPA-430: workspace finalize gates + no-remote-git enforcement (#6969) 2026-05-29 08:25:29 -07:00
package.json feat(connections): add self-serve intent runtime (#12345) 2026-08-29 12:08:34 -05:00
tsconfig.json Fix root TypeScript solution config 2026-03-09 14:09:30 -05:00

README.md

@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.