Skip to content

fix(brain): update existing pages again under gbrain's revision contract - #409

Open
jacklau04 wants to merge 1 commit into
TheWeiHu:mainfrom
jacklau04:fix/brain-put-revision-required
Open

jacklau04 wants to merge 1 commit into
TheWeiHu:mainfrom
jacklau04:fix/brain-put-revision-required

Conversation

@jacklau04

Copy link
Copy Markdown
Contributor

In plain words: since gbrain 0.51 (2026-09-17), devbrain brain put and devbrain rebuild could create a page but never update one that already existed. Every re-fold of an existing brain page died with revision_required, and the distill skill threw the error away, so the search index quietly froze at each page's first version. This PR makes updates land again and makes a failing put visible.

Problem

gbrain's "make concurrent writes durable and revision-safe" release (garrytan/gbrain#5149, v0.51.0.0) changed put: replacing an existing page now needs --expected-revision <rev> (or --force). Otherwise gbrain exits 1 with a JSON receipt on stdout and this on stderr:

Error [revision_required]: The page already exists; an expected revision is required.

devbrain forwards put verbatim (internal/brain/brain.go, passthrough) and Rebuild runs gbrain put <slug> with the file on stdin (internal/brain/rebuild.go). Neither sends a revision, so with gbrain ≥ 0.51:

  • devbrain brain put <slug> < page.md on an existing slug exits 1;
  • devbrain rebuild aborts at the first already-indexed page (its set -e semantics);
  • /distill step 5 ran devbrain brain put … >/dev/null 2>&1, so nobody saw it. On my machine the index held 227 pages frozen at their first write, and gbrain doctor had nothing to say because the writes never reached it.

Fix

putPage (new, internal/brain/put.go) runs the put; when gbrain answers revision_required it runs the same put once more with --force, replaying the same stdin bytes. Both call sites (Run passthrough and Rebuild) use it.

Why --force rather than a read-then-write with --expected-revision: devbrain's Markdown pages on disk are the source of truth and this wrapper is the index's only writer, so the disk copy always wins. A revision conflict is never a real one here, and a get round-trip would add a failure mode without adding safety.

  • Older engines never answer revision_required, so they never see --force (compatible with gbrain < 0.51).
  • A caller who passes --force or --expected-revision themselves gets gbrain's own answer (those outcomes carry other codes, so no retry). Any other failure is relayed unchanged with gbrain's exit code.
  • stdin is buffered only when gbrain would read it (not for inline --content <text> / --content=<text>), so an interactive put --content cannot block on a terminal.
  • The first attempt's conflict receipt is dropped when the retry succeeds; the existing keyword-only hint still prints.
  • assets/skills/distill/SKILL.md step 5: the put loop now prints brain put failed for <slug>: <last line> instead of >/dev/null 2>&1, and tells the agent to carry that into the fold summary.

Tests

All through the built binary with a gbrain stub that reproduces gbrain 0.60's exact failure shape:

  • TestBrainPutRetriesWithForceOnRevisionRequired: exit 0; gbrain calls are exactly put <slug> then put <slug> --force; both receive identical stdin; no revision_required leaks into the output; the retry's output is forwarded.
  • TestBrainPutLeavesOtherFailuresAlone: a different error (invalid_params, exit 2) is not retried and passes through with its exit code.
  • TestRebuildForcesUpdatesOfExistingPages: devbrain rebuild with one on-disk page: same retry, exit 0, progress line printed.
  • TestPutNeedsStdin: stdin versus inline --content detection, including gbrain's "a trailing --content reads stdin".

I watched the three behavioral tests fail on main first (exit 1 with the receipt, a single gbrain call). make test is green (30 packages).

Verified against the real engine (gbrain 0.60.248.0): the built binary updates an existing page (exit 0; gbrain's receipt reports status: skipped for identical content), while the stock 1.5.30 binary fails with Error [revision_required] on the same call.

🤖 Generated with Claude Code

gbrain 0.51 (garrytan/gbrain#5149, 2026-09-17) made `put` refuse to
replace an existing page unless the caller sends the revision it read:
exit 1, a JSON receipt on stdout and `Error [revision_required]` on
stderr. devbrain forwards `put` verbatim and Rebuild runs `gbrain put
<slug>` with the file on stdin, so with gbrain >= 0.51 every update of
an already-indexed page failed and `devbrain rebuild` aborted at the
first one. The distill skill ran its put loop with output discarded, so
the index quietly froze at each page's first version.

putPage runs the put and, when gbrain answers revision_required, runs
it once more with --force, replaying the same stdin. The Markdown page
on disk is devbrain's source of truth and this wrapper is the index's
only writer, so the disk copy always wins; a revision conflict is never
real here, and a get round-trip for --expected-revision would add a
failure mode without adding safety. Older engines never answer
revision_required and so never see --force; a caller's own --force or
--expected-revision gets gbrain's answer unchanged, as does any other
failure. stdin is buffered only when gbrain would read it (not for an
inline --content), so an interactive put cannot block on a terminal.
Both call sites use it. The distill skill now reports a failing put
instead of hiding it.

Tests: black-box put retry, put non-retry on another error, rebuild
retry (all watched failing on main first), and the stdin/--content
detection table. make test green; verified against gbrain 0.60.248.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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