Skip to content

A failed re-review permanently deletes the comments you wrote #37

Description

@jtomaszewski

The problem

Press re-review, have the run fail, and every comment you wrote by hand — plus every AI comment you rewrote — is gone from disk. Not hidden, not recoverable: deleted.

The failure does not have to be exotic. A model error, a network blip, a checkout that goes wrong mid-run, claude not being logged in — anything that throws after the run has started. The row comes back as failed, and your writing is not in it.

This contradicts the rule the repo states explicitly in CLAUDE.md:

Don't let a re-review discard human work: comments the user wrote or edited are carried across (carryOverComments), never regenerated away.

That guarantee currently holds only when the run succeeds.

Why it happens

reviewPr (src/runner/review.ts) writes the artifact three times, and the ordering is the bug:

  1. Before calling Claude it builds a fresh artifact literal with comments: [] (:176-207) and saves it (:211). The user's comments leave the disk here — the file now holds an empty list. Everything that protects them from this point on lives in memory, in the existing variable.
  2. On success, carryOverComments(existing, artifact) (:269) puts them back, re-anchored to the new diff. This is the path the rule describes, and it works.
  3. On failure, the catch handler (:278-289) spreads that same pre-run object — still comments: [] — sets status: "failed", and saves. carryOverComments is never reached.

So the human comments are destroyed at step 1 unconditionally, and restored only by step 2. Step 3 makes the deletion permanent.

Reproducing it

// a ready artifact holding one comment the user wrote
await saveArtifact(withUserComment("old-sha"));
expect((await loadArtifact(ID))!.comments).toHaveLength(1);

prInfo.mockResolvedValue(pr("new-sha"));
claude.mockRejectedValue(new Error("model unavailable"));
await expect(reviewPr(REF, { withSource: false })).rejects.toThrow();

const after = (await loadArtifact(ID))!;
expect(after.status).toBe("failed");
expect(after.comments).toHaveLength(0);   // passes — the comment is gone

Both assertions pass today. Observed output: status=failed comments on disk=0.

Suggested fix

The failure path needs the same carry-over the success path has — the human comments are known (humanComments(existing), src/core/refresh.ts) and nothing about a failed run makes them less valid.

Options, roughly in order of how much they change:

  • Minimal: in the catch handler, restore the human comments from existing before saving the failed artifact.
  • Better: don't drop them in the first place. Seed the pre-run artifact from humanComments(existing) rather than [], so no window exists where the file is missing the user's writing. The success path's carryOverComments then re-anchors as it already does.
  • Belt and braces: have the failure write merge onto what is on disk rather than saving a whole object built minutes earlier — the same discipline mergeConcurrentEdits (src/core/revise.ts) applies to chat turns, and the same one Send during an in-flight run can post the review to GitHub twice #36 needs.

Wants a regression test on the failing path specifically: user comment in, run throws, comment still on disk.

Scope

Found while verifying docs/lifecycle.md (#34) against the code. Third of three defects that review turned up; see also #35 and #36. Priority is set higher than those two because this one silently destroys the user's own writing, and there is no way to get it back.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpriority-lowCosmetic or follow-up workreleasedrisk-mediumOrdinary change with testswontfixThis will not be worked on

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions