feat: brain_update — supersede-by-slug write verb (+ read-after-write: brain_get, brain_write handle) #45
Closed
opened 2026-06-22 06:06:16 +00:00 by mathias
·
2 comments
No Branch/Tag Specified
main
fix/pipeline-writes-158
fix/edit-path-io-192
fix/create-regular-188
fix/guard-special-179
fix/linkcheck-keeps-write-178
fix/write-dir-redirect-173
fix/tunnel-detect-174
fix/pipeline-root-157
w3/154-mcp-auth-wiring
fix/tunnel-traversal
test/in-brain-targets-171
fix/brain-scan-root-157
fix/graph-wiki-root-157
fix/vectorstore-root-157
fix/notes-root-157
fix/search-root-157
fix/brainwide-root-157
fix/raw-symlinks-153
feat/symlink-tripwire-157
fix/rest-auth-149
fix/123-brain-edit-envelope-check
feat/106-page-aware-extraction
fix/105-source-drift-on-main
fix/104-surface-merge-conflicts-on-main
agent/b1-compile-on-ingest
agent/104-surface-merge-conflicts
agent/105-source-drift
agent/107-loud-unconfigured-alerting
feat/brain-read-edit
fix/capture-i1-refusal-names-target
renovate/configure
feat/consolidate-single-harness
icebox/cmd-routing
feat/brain-pending-promote
fix/close-session-classification-refresh
fix/capture-summarywriter
v0.24.0
v0.23.1
v0.23.0
v0.22.0
v0.21.1
v0.21.0
v0.20.1
v0.20.0
v0.19.1
v0.19.0
v0.18.4
v0.18.3
v0.18.2
v0.18.1
v0.18.0
v0.17.0
v0.16.1
v0.16.0
v0.15.0
v0.14.0
v0.13.2
v0.13.1
v0.13.0
v0.12.1
v0.12.0
v0.11.2
icebox/cmd-routing-2026-07-01
v0.11.1
v0.11.0
v0.10.0
v0.9.0
v0.8.0
v0.7.0
v0.6.0
v0.5.0
v0.4.1
v0.4.0
v0.3.0
v0.2.0
v0.1.0
Labels
Clear labels
agent/ready
blocked/client
blocked/supplier
blocked/upstream
risk/high
risk/low
risk/medium
size/l
size/m
size/s
size/xs
status:dispatch
status:dispatched
status:review
status:reviewing
type/chore
type/decision
type/docs
type/feat
type/fix
type/ops
type/research
type/test
Meets the agent-ready contract; authorizes unattended execution
Waiting on the client
Waiting on a supplier
Waiting on an upstream project or vendor
Irreversible, security-relevant, or control plane
Reversible, local, test-covered
Touches shared state, deploys or data
Too big - split it
Under two days
Under half a day
Under an hour
Maintenance, no behaviour change
Human decision required; never agent/ready
Documentation only
New capability
Corrects broken behaviour
Infrastructure, deploy, runtime
Investigate; output is a finding, not code
Test coverage only
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: mathias/hyperguild#45
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Context
The keystone gap from the brain intent↔interface analysis (#43, evidence in #44).
update/supersedeis the single highest-value missing verb: 5/5 agent mismatch, flagged top-priority in both columns. The brain today is append + keyword-search — there is no verb to revise an existing note. Agents that need to revise blind-re-write the same slug via a secondbrain_write(3 of 5 observed re-writes happened within 30s of each other), producing duplicates and silent contradictions with no way to tell whether a re-write deduped or forked.This issue tracks the agent-facing write verb decided in the 2026-06-22 review session. Shape and provenance model are settled (see Decisions below); signatures here are the implementation target.
This blocks the session-retro skill — that skill emits findings that must revise prior notes, and without a supersede verb it reproduces exactly the duplicate-and-contradict failure documented in #44.
Decisions taken (2026-06-22)
.history/sidecar, no in-tree tombstone. The prior version is recoverable from git; superseded content is not kept queryable. Frontmatter stamps the supersession for human/agent legibility.brain_update(andbrain_write) return the landed id + content hash, giving agents read-after-write confirmation for free on the write path. A thinbrain_get(id)covers the create-path / get-by-id half of theverify_write_landedgap (#44 cluster 2, 4/4 mismatch) without its own issue.Out of scope here: semantic read (#44 cluster 3),
brain_answertrust (#44 cluster 4), the human promote half (#38), consumer-split redesign (deferred per #43).Primary tool:
brain_updateSupersede an existing note in place: replace body, rewrite frontmatter, re-sync embedding.
Behaviour:
wing/hall/slug(or full path). If no such note exists → error (do not silently create; that'sbrain_write's job). Clear error so the agent can fall back to create.content.updated_at(now),supersedes(priorcontent_hash), andsupersede_reasonifreasongiven. Preservewing,hall,created_at, and any custom fields.id,path,content_hashof the landed note,superseded: true.Secondary: read-after-write on the create path
brain_writereturn contract extended to include{ id, path, content_hash }(currently does not return a stable handle — this is why agents lexically re-query their own fresh note).brain_getis read-only — LOW risk tier.brain_updateis a content-replace — MEDIUM risk tier (reversible via git; the no-silent-create guard prevents accidental new-note creation).Layout note
The promote-half issue (#38) and the older specs assume
brain/raw/→brain/wiki/. Per #43 the current curation-state split isknowledge/(unreviewed) →wiki/(promoted).brain_updateoperates on notes wherever they live (bothknowledge/andwiki/) — it is a revise-in-place verb, orthogonal to the curation-state move. Reconcile path conventions with #38 at implementation time.Acceptance criteria
brain_updatesupersedes an existing note: body replaced,updated_at/supersedesfrontmatter stamped, prior fields preservedbrain_updateon a non-existent target → clear error, no note created (no silent create)brain_updatereturns{ id, path, content_hash }brain_writereturn contract extended to include{ id, path, content_hash }brain_get(id|path)returns the landed note (frontmatter + body) — read-after-write confirmation without lexical re-querytask checkpassesingestion/internal/mcp/server.goRelated
brain_pending/brain_promote); complementary, not duplicated heretbd_ship) — same intent-vs-mechanism fix applied to the gitea repoDispatch — koala-Claude implementation handoff
Implement this issue in
mathias/hyperguild. The decisions in the body above are settled — don't relitigate the shape. Verify everything against the brain MCP code as you go. This unblocks the session-retro skill, so correctness of the supersede + re-index path is the whole point — a half-workingbrain_updatethat doesn't refresh retrieval is worse than none.Where the code lives
All in
ingestion/internal/mcp/:server.go— package doc comment (lists tools),handleCalldispatch switch, server wiring (WithHybridRetrieval= embedder+pgvector,WithGraph= entity/edge re-index).handlers.go—tools()descriptor list, plus thebrainWrite/brainQuery/brainTunnelhandlers to mirror. NotebrainWritecallsapi.WriteNote(...)and currently returns only{"path": relPath}.handlers_test.go,integration_test.go— test patterns to follow.api.WriteNoteiningestion/internal/api/. Find its read/resolve counterpart (or add one) for the supersede path.A tool is registered in three places — all three must be updated for any new tool: (1) the
tools()descriptor slice, (2) thehandleCallswitch, (3) the package doc comment at the top ofserver.go.Step 0 — the load-bearing investigation, do this FIRST
The supersede contract requires retrieval to reflect the new content after an update. Two indexes are downstream of a write:
Graph — handled explicitly in-handler via
s.indexInGraph(ctx, op, relPath). Easy:brain_updatecalls it the same waybrain_writedoes.pgvector embeddings —
brain_queryreadss.vector+s.embedderlive (search.QueryContext), but there is no visible embedding re-index call inside the write handler. Before writing any code, trace how embeddings get (re)built for a note: is it a watcher, a separate sync job, an on-read embed, or somethingapi.WriteNotetriggers indirectly?Report what you find before implementing the re-sync. If embeddings are rebuilt out-of-band (e.g. a periodic sync), then "re-sync on update" may mean "ensure the out-of-band job re-embeds the changed file" rather than an in-handler call — and the acceptance criterion "a query reflects the new content, not the old" needs to be satisfied against whatever that real mechanism is. Do not invent an embedding call that doesn't match the existing architecture. If embeddings are only ever built by a separate process and
brain_updategenuinely can't trigger a refresh, stop and flag it in a PR comment — that's a design question for Mathias, not something to paper over.What to build
1.
brain_update— supersede-by-slug, whole-note replaceslug(or full path),wing+hall(required if slug isn't a full path),content(new full body),reason(optional).brain_write's job). Error must be clear enough that a caller knows to fall back tobrain_write.content. Rewrite frontmatter: setupdated_at(now, UTC),supersedes(prior content hash),supersede_reasonif given. Preservewing,hall,created_at, and any custom frontmatter fields.s.indexInGraph). Rebuild the wing_index.mdif it lands in the structured wiki (mirrorbrainWrite'sBuildWingIndexcall). Auto-tunnel optional on update — matchbrain_writeunless it causes duplicate links.{ id, path, content_hash, superseded: true }.2.
brain_get— fetch by id or path (NEW, read-only)idORpath(one required).{ id, path, content_hash, frontmatter, body }.3. Extend
brain_writereturn contract{"path": relPath}. Addidandcontent_hashso the create path also gives a stable handle (this is why agents currently lexically re-query their own fresh notes).pathfor backward compat; add fields, don't rename.Decide and state: what is
id, what iscontent_hash?There's no stable note id in the current surface (paths are the de-facto handle). Pick the simplest correct thing — likely
id= the relative path (or a slug-derived stable key) andcontent_hash= sha256 of the post-write body. Whatever you choose,brain_get(id)andbrain_get(path)must both resolve, and theidreturned by write/update must round-trip throughbrain_get. State your choice in the PR description.Conventions
main.task checkis the gate — must pass before the PR is mergeable.handlers_test.go):brain_getby idbrain_getby pathbrain_writereturns{id, path, content_hash}tools(), andhandleCallfor both new tools.Layout note
Per #43 the curation-state split is
knowledge/(unreviewed) →wiki/(promoted).brain_updateoperates on notes wherever they live — revise-in-place, orthogonal to the curation-state move (#38'sbrain_promoteis the move verb, separate issue). Don't conflate them.Open the PR, don't auto-merge
Open a PR against
mainreferencing #45 and post your Step 0 embedding findings as a PR comment. Mathias reviews and merges from claude.ai. If Step 0 surfaces that the re-sync can't be done in-handler, raise it in the PR before building the rest — the one thing worth blocking on.Report back
brain_updatetriggers it).id/content_hashchoice.task checkresult.Implemented — PR #46 (open, not merged per dispatch)
PR: #46
Step 0 — embedding re-index (load-bearing finding)
No MCP write handler re-embeds in-handler — not even
brain_write. TheServerholds only the read side (s.vector,s.embedder); it has novectorstore.Store(Upsert/Delete), so it cannot embed in-handler. Embeddings refresh strictly out-of-band:vectorstore.StartSync— background ticker (cmd/server/main.go:301, default 5 min), mtime-driven: re-embeds any file whose mtime exceeds its oldest chunk'supdated_at, deleting stale chunks first.POST /backfill-embeddings— manual full sync.So "re-sync on update" = ensure the out-of-band Sync re-embeds the changed file.
brain_updaterewrites the note viaos.WriteFile→ mtime advances → next Sync pass re-embeds the new body + drops the stale chunk. Exactly howbrain_writebehaves. I did not add a divergent in-handler embed call. Verified against the real mechanism inTestBrainUpdateReembedsNewContent(no stub of the re-index path).Design note: semantic/vector retrieval has a ≤5 min staleness window post-supersede; BM25 +
brain_getare immediate. Deferred as #47 (wire avectorstore.Storeinto the MCP server only if a consumer needs sub-5-min vector read-after-write).id/content_hashchoiceid= brain-relative slash path (the de-facto handle).brain_get(id)andbrain_get(path)resolve identically; the id from write/update round-trips throughbrain_get.content_hash= lowercase hex sha256 of the full file bytes on disk.brain_getrecomputes → identical;brain_updatehashes the prior file and stamps it assupersedes.Acceptance criteria — all met
brain_updatesupersedes: body replaced,updated_at/supersedesstamped, prior fields (created_at/wing/hall/custom) preservedbrain_updatereturns{id, path, content_hash}(+superseded)brain_writeextended{path}→{id, path, content_hash}(path kept for compat)brain_get(id|path)returns frontmatter + bodytask checkpasses (exit 0; lint 0 issues, vet clean,-racetests green)tools(),handleCall, package docCommits (trunk-style, one logical change each)
feat(brain): add UpdateNote/ReadNote supersede primitives + frontmatter editorfeat(mcp): register brain_update + brain_get, extend brain_write handlechore(context): re-sync derived adapters—task checkcontext gate flagged pre-existing root-AGENT.mddrift; committing the regen is the documented remedy, not a content change here.No tag/SemVer bump — happens on merge to main from claude.ai, not on the branch.
Follow-up filed
Brain
wiki/hyperguild/facts/brain-mcp-embeddings-out-of-band-sync.md— the "write handlers don't re-embed; Sync is out-of-band + mtime-driven" footgun, so the next person adding a write verb doesn't re-derive it.Unmet criteria: none.