Skip to content

recover: say when a snapshot is a before-image - #162

Merged
Amitgb14 merged 2 commits into
mainfrom
fix/cli-baseline-blindness
Sep 9, 2026
Merged

recover: say when a snapshot is a before-image#162
Amitgb14 merged 2 commits into
mainfrom
fix/cli-baseline-blindness

Conversation

@Amitgb14

@Amitgb14 Amitgb14 commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Reported as lost work: an agent wrote a file, was killed before committing, and the restored branch did not contain it.

The restore was correct. The snapshot never held the file, because it was a baseline — the workspace as the run started, captured before the agent ran — and nothing said so.

Why every recent snapshot is one

A run started from Studio records only a baseline: the daemon takes one before-image at launch and closes the session immediately. So killing an agent before it commits leaves exactly one session, holding the state from before it did anything.

On the reporter's machine all seven OneNutri sessions were baselines, and recover list reported every one as clean — the same word a finished run's snapshot gets:

20260908-234614-4f4acb  …  claude  7h ago   1  baseline     ← was: clean
20260908-181034-593f27  …  claude  13h ago  1  baseline     ← was: clean
…
7 of these are baselines: the workspace as the run *started*, captured before the
agent ran. Restoring one gives you the starting state, not the work.

Same defect, other front end

The daemon already guards against this — #150 taught it to skip baselines when looking for something to recover and to hide them from the listing. That fix never reached internal/cli, which did no filtering on Outcome at all.

runs.go's comment beside the literal was already right:

a literal that has to match in three places is one typo away from offering a run's starting state as its work

It matched in two. OutcomeBaseline now lives in internal/rescue beside the other outcomes, and the daemon's private literal refers to it — which is what makes the third place impossible to forget.

What changed

  • Status() names it, ahead of the closed-session case that was swallowing it.
  • IsBaseline() is the question callers actually ask.
  • The listing counts them and says what they are.
  • The restore says it before the git commands it prints, because it changes what those commands are for — somebody who reads "created branch X" and goes looking will find their pre-run state and conclude the snapshot lost their work, rather than that it never held it.

Marked, not hidden — deliberately different from the daemon

Studio hides baselines because its screen offers a Restore button beside every row. This listing is what somebody reads while hunting for lost work, where "no snapshots recorded" is the worse answer: there was a run, and this is all it left.

Not fixed here

That a Studio run has no periodic snapshots at all is the real gap, and it is deliberate-for-now — see docs/security/open-items.md. This change makes it legible; it does not close it. Raised separately with the repro.

🤖 Generated with Claude Code

https://claude.ai/code/session_011ba7PUJ23wb1V1aBhVbnE8

Reported as lost work: an agent wrote a file, was killed before committing, and
the restored branch did not contain it. The restore was correct. The snapshot
never held the file, because it was a *baseline* — the workspace as the run
started, captured before the agent ran — and nothing said so.

A run started from Studio records only that: studioapi takes one before-image at
launch and closes the session immediately. So killing an agent before it commits
leaves exactly one session, holding the state from before it did anything. On the
reporter's machine every one of seven OneNutri sessions was a baseline, and
`recover list` reported all of them as `clean` — the same word a finished run's
snapshot gets.

The daemon already guards against this: #150 taught it to skip baselines when
looking for something to recover and to hide them from the snapshot listing. That
fix never reached internal/cli, which did no filtering on Outcome at all. Same
defect, other front end.

`OutcomeBaseline` moves to internal/rescue beside the other outcomes, and
studioapi's private literal now refers to it. Its comment there was already
right — "a literal that has to match in three places is one typo away from
offering a run's starting state as its work" — and it had matched in two, which
is how the CLI came to be blind to a value the daemon has been writing for weeks.

`Status()` names it, ahead of the closed-session case that was swallowing it, and
`IsBaseline` is the question callers actually ask. The listing counts them and
says what they are; the restore says it *before* the git commands it prints,
because it changes what those commands are for — somebody who reads "created
branch X" and goes looking will find the state they had before the run and
conclude the snapshot lost their work rather than that it never held it.

Marked rather than hidden, and that is a deliberate difference from the daemon.
Studio hides them because its screen offers a Restore button beside every row.
This listing is what somebody reads while hunting for lost work, where "no
snapshots recorded" is the worse answer: there *was* a run, and this is all it
left.

Claude-Session: https://claude.ai/code/session_011ba7PUJ23wb1V1aBhVbnE8
@vercel

vercel Bot commented Sep 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
sandbox-cli Ready Ready Preview Sep 9, 2026 7:59am UTC

Three findings. The first is the one that mattered: the baseline notice printed
in the branch case alone, which left it out of the two modes where it costs most.

`--into-worktree` **overwrites**. Restoring a baseline there replaces the tree
with the state from before the run and then prints "Keep it with: git commit" —
so somebody hunting for missing work could commit the pre-run state over the
agent's, which is a sharper version of the exact report this PR exists to
prevent. `--patch` writes a diff nothing labels either, and `recover show`
streams one with nothing saying which side of the run it is from.

The notice is now printed before the mode-specific output, so every mode carries
it, and `show` says it before handing off to git — after that point git streams
its own output and anything appended scrolls past.

Two smaller ones, both in text I had just written. "%d of these are baselines"
reads "1 of these are baselines" in the *common* case, since a daemon run leaves
exactly one session; the singular is not an edge worth skipping. And the same
line emphasised a word with markdown asterisks, which a terminal prints
literally — `printFindings` a few lines up already uses ANSI for the same job.

`docs/usage/recovery.md` and `docs/GUIDE.md` both describe the STATE column and
neither listed the new value, while the website copy added alongside did. The
sample listing in those docs is what people compare their own output against, so
it now shows a baseline row and says what the values mean.

Claude-Session: https://claude.ai/code/session_011ba7PUJ23wb1V1aBhVbnE8
@Amitgb14
Amitgb14 merged commit dc5cf58 into main Sep 9, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant