brain_edit always fails with "old_str is empty" even when old_str is non-empty #115
Closed
opened 2026-08-31 05:10:28 +00:00 by mathias
·
1 comment
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
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#115
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.
Symptom
Every
brain_editcall this session failed identically:old_strwas never actually empty on my end — tried three times with different content, all failed the same way:old_str(verbatim copy frombrain_read's own output for the target note).old_str: "a cold \gemma4:31b` completion"`.All three targeted the same real note (
wiki/homelab/failures/brain-answer-fallback-cold-load-exceeds-shared-timeout-budget.md), all copied exactly from a freshbrain_readof that note (not from abrain_queryexcerpt — read the tool description, know that rule). All three came back with the identicalold_str is emptyerror.Where this points in the code
ingestion/internal/notes/edit.go:105:guards
oldStr == ""insideEdit(brainDir, path, oldStr, newStr, scope). The caller,editWithScopeiningestion/internal/mcp/tools_notes.go:176, just doesjson.Unmarshal(args, &a)intobrainEditArgs{OldStr string \json:"old_str"`}and passesa.OldStrstraight through — nothing server-side looks buggy in isolation, which is why I'm not claiming a root cause here: something between my tool call and this unmarshal is losingold_str`specifically, and I don't have visibility into that hop from this session.
What this rules out
brain_query excerpt") — all three attempts used fresh
brain_readoutput, byte-for-byte.(
brain_query,brain_write,brain_readon the same note) — so it's not a session-widetransport failure, it's specific to
brain_edit'sold_strargument.Suggested follow-up
specific to this harness) before assuming server-side.
ErrDegenerate's message could echo back whatold_strlength/hash the server actually received, which would have made this a 30-second diagnosis
instead of a dead end — right now "old_str is empty" gives no way to tell client-sent-empty
apart from server-received-empty-after-a-marshaling-bug.
Workaround used
Fell back to
mcp__brain__capture'ssupersede_slugon theinsightsarray — that path writes awhole new note body under the same path/slug rather than patching a byte range, so it didn't hit
whatever
brain_editis hitting.Verified and merged in mathias/hyperguild#124, commit
5a5d938e8e.Second-round fix (
e2cb315) builds the claim envelope map fromfm(post-merge, pre-write) instead of the caller's rawcontentFMfragment, in ingestion/internal/api/update.go. Independently re-verified before merging:contentFMagain, confirmed the test goes red ("An error is expected but got nil"); restored the fix, confirmed green.half_life: durablealone -> correctly rejected, file unchanged.updated_at/supersedescannot be spoofed via the caller's fragment: both are unconditionally re-set after the merge loop.supersede_reasonset via the caller's own fragment survives whenopts.Reasonis empty -- same-actor two ways to set the same field, not a cross-update provenance leak like the envelope bug, so not treated as blocking.CI green on the merged head SHA (run 7795,
e2cb315, conclusion success).