Files
PaperClipAI/server/src/services/native-runtime
DottaandClaude Opus 5.5 559551e977 fix(native): wait for a busy agent directory while retiring a predecessor (#20991)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A native run on a reused warm sandbox first retires the previous
run's warm session, and that retirement collects the old session's agent
files
> - On a server without a persistent volume, agent-file collection takes
a per-agent Postgres lock, so runs of one agent collect one at a time
> - #20863 sends a run whose predecessor cannot be retired to a fresh
sandbox. It classified every retirement error as unprovable, including a
busy lock held by another run of the same agent
> - So ordinary lock contention failed the run, dropped a healthy
sandbox and rested the task
> - This pull request makes retirement wait for a busy directory lock in
the same run, and keeps the fresh-sandbox fallback for retirements that
stay unprovable after the lock is obtained
> - The benefit is that busy agents no longer fail follow-up runs on
their warm sandboxes

## Linked Issues or Issue Description

**What happened?**
A no-fault restart-chaos wave with 8 concurrent runs of one Claude agent
on Daytona, with a volume-free server, failed one monitor-due follow-up
after 6 seconds. The error was `adapter_failed`, cause
`native_predecessor_retirement_unproven`: "The previous run's session on
this sandbox could not be retired (Agent directory operation is already
active)". The run also logged `INSTRUCTION_COLLECTION_UNAVAILABLE`. The
stack shows the error came from `claimWarmNativeInstructionCopy` during
instruction preparation. The previous session had closed; only its
agent-file collection met the per-agent lock that another run held.

**Expected behavior**
A busy agent-directory lock is transient. The run waits for it and then
retires the predecessor on the same sandbox. Only a retirement that
stays unprovable after the lock is obtained takes the fresh-sandbox
fallback.

**Steps to reproduce**
1. Run several warm native tasks of one agent at once on Daytona with a
volume-free server.
2. Re-wake one task while another run of the agent collects its files.
3. The follow-up fails with `native_predecessor_retirement_unproven` and
its sandbox is dropped.

Refs #20863, #20829, #20957.

## What Changed

- `retireWarmPredecessor` (native executor): a busy agent-directory lock
(`AgentDirectoryOperationBusyError`) is waited out in the same run,
polling every second for up to five minutes
(`warmRetirementDirectoryWait`). The usual holder is another run's short
collection, so retirement then finishes on the same sandbox with the
copy this run adopted. The run is not deferred: deferring after adoption
would release the adopted copy and could skip saving the predecessor's
edits.
- Past that budget the run fails with its own error,
`native_predecessor_retirement_directory_busy`, not as an unprovable
retirement. Before failing it files the adopted copy's retirement
receipt, so the copy is kept as `pending_collection` and run cleanup
does not mark it `unavailable`. The owner is not marked unretired and
stays registered for its sandbox, so a later run retires it there.
- An idle-timeout close that meets the lock retries after the
agent-directory retry delay instead of leaving the session registered
with no retry.
- Closing idle sessions for environment delete (#20957) reports such an
owner as `busy`, not `failed`.

## Verification

- RED on master, then GREEN:
- `native-session-executor.test.ts -t "directory lock"`: two runs of one
agent contend on the lock while a follow-up retires the predecessor. On
master the claim rejects with `NativePredecessorRetirementUnprovenError`
(`Agent directory operation is already active`). Now a short-lived lock
is waited out and the predecessor is retired and collected on the same
sandbox; a lock that outlasts the budget fails with
`native_predecessor_retirement_directory_busy`, environment delete
reports `busy: 1`, a fresh-sandbox attempt does not forget the owner,
and once the lock is free a later run retires it.
- `native-session-executor.test.ts -t "idle close whose file
collection"`: on master the idle close never retries; now it retries and
closes.
- Existing suites pass: native executor, agent-directory busy heartbeat,
native pre-dispatch retry, native controller recovery and plugin runner
eviction. Server `tsc --noEmit` passes.

## Risks

- Under contention a follow-up can wait up to five minutes before its
first turn. The normal path adds no wait.
- A lock held for more than five minutes fails the run visibly instead
of dropping the sandbox.
- An idle owner whose collection stays busy keeps retrying at the 5 to
15 second agent-directory delay until the lock frees, a controller
restart, or the next admission.
- No persisted state changes.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, agentic coding with
tool use (shell, file edits, test runs) in Claude Code.

## 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
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 05:08:15 -06:00
..