Files
PaperClipAI/server
DottaandClaude Opus 5.5 e8a418cc4d fix(workspaces): settle an uncertain remote provisioning row once its sandbox is stopped (#19862)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A remote task workspace runs the project's provisioning commands in
a sandbox. Each command has a `remote_workspace_provision` journal row,
because an arbitrary command cannot be replayed after an ambiguous reply
> - A row left `running` with `uncertain: true` blocks every later
reservation of that workspace, and nothing ever settles it
> - A crash or lost reply during provisioning therefore leaves the
task's workspace unusable, with no next action
> - This pull request settles such a row only when the earlier run's
sandbox is provably stopped. Otherwise the 409 names the row, its run,
and the operator's action
> - The benefit is that a crashed provisioning attempt no longer locks
the workspace forever, and the effect is never assumed to have succeeded

## Linked Issues or Issue Description

**Bug description**
The Phase 0 ownership inventory (finding 4) lists
`remote_workspace_provision` rows left `running`/`uncertain`
(`remote-workspaces.ts`) as a record with no automatic release.
`reserveRemoteWorkspace` rejects every other run with
`remote_workspace_provision_unverified`. A command that simply exited
non-zero also stayed `uncertain`.

**Steps to reproduce**
1. Start remote provisioning and lose the runner's reply (or have the
command exit non-zero).
2. End the run and stop its sandbox.
3. Start another run on the task. Reservation fails with
`remote_workspace_provision_unverified`, and the next attempt fails the
same way.

**Expected behavior**
After the earlier sandbox is provably stopped, the row settles and the
workspace can be reserved again. While that cannot be proven, the error
says what to do.

Refs #19846

## What Changed

- `remote-workspaces.ts` `reserveRemoteWorkspace`: for each uncertain
provision row from another run, `provisionOwnerProvenStopped` requires
two things. The row's run is terminal, and every environment lease of
that run carries a provider stop or destroy receipt
(`hasRemoteTerminationReceipt` or `hasRecoveredCleanupStopReceipt`). A
proven row is settled as `failed` (`uncertain: false`, `settledBy:
"owner_sandbox_stopped"`) and logged as
`workspace.remote_provision_settled`. It is never marked succeeded.
- If a later reservation check (for example a writer hold) rejects the
attempt, the proven rows are re-proved and settled in their own
transaction, so they do not block again.
- An unprovable row still blocks. The 409 now carries `operationId` and
`runId` and names the action: stop the earlier sandbox, or set the
task's Workspace to "New isolated workspace". That picker option already
exists in `IssueProperties`.
- `provisionRemoteWorkspace`: a reported non-zero exit (not a timeout)
is recorded as `failed` with its `exitCode` instead of staying
uncertain.

## Verification

- `npx vitest run src/services/remote-workspaces.integration.test.ts
src/services/remote-workspace-save-concurrency.test.ts`: 126 passed.
- New case "settles an uncertain provisioning command once its run's
sandbox is provably stopped". A failed run alone still gets the 409 with
`operationId`/`runId`. After a stop receipt, the row settles, and the
activity is logged once.
- New case "records a provisioning command that reported a failing exit
as finished, not uncertain".
- Red before the fix: with the old `remote-workspaces.ts`, both new
cases fail. Green after.
- Server `tsc --noEmit` shows no errors in `remote-workspaces.ts`.
- Not run: a live browser check of the "New isolated workspace" path for
this error. I confirmed in code that it reserves a different workspace,
so it bypasses this check.

## Risks

- A settled row means "effect unknown". The next writer provisions again
in its own epoch, as every new writer already does. A partially applied,
non-idempotent command could see its partial effect. That is the same
exposure as any failed provisioning, and it happens only after the
earlier sandbox has stopped, so the two commands never run at the same
time.
- Previous-release story: no persisted shape changes. Uncertain rows
saved by the previous release are evaluated by the same proof on the
next reservation.
- No new waits before `turn.start`. The proof runs only when an
uncertain row exists, inside the existing reservation transaction.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used with tool use and
code execution 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-07 10:54:52 -06:00
..