wiki/index.md contains a truncated UTF-8 sequence — the index writer cuts mid-character #93
Closed
opened 2026-08-17 22:11:01 +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
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#93
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.
Found by the frontmatter lint shipped in #90. It is the one file in
mathias/brainthat the lint cannot classify, because it cannot be decoded at all.Symptom
\xe2\x80is the first two bytes of a three-byte character (em-dash\xe2\x80\x94, ellipsis\xe2\x80\xa6, …) with its third byte missing — immediately followed by a complete\xe2\x80\xa6. So a title was cut mid-character and then the truncation marker was appended.That is the signature of truncating by bytes where the intent was characters: something like
s[:n] + "…"over a byte slice, or a Gostringsliced at a fixed index without a rune boundary check.Why it matters beyond one file
yaml.safe_load,utf8.Valid, most JSON encoders) rejects the whole index, not just the bad line. The frontmatter lint had to be written to report-and-continue specifically so this one file could not hide the other 17 findings.Where to look
The index writer that emits
wiki/index.md— the- [[wing-slug|Title]]list lines. Find where a title gets shortened before the…is appended.Fix shape
Truncate on a rune boundary, not a byte offset:
Acceptance
caveman: me cut long title between letters, not inside letterutf8.Validasserted over the generated index in a test — the property, not one examplewiki/index.mdregenerates clean;task lint:frontmatterreports zero findings acrossmathias/brainRefs
ingestion/internal/frontmatterFixed —
125c99b, plusmathias/brain@8ef0564regenerating the file.task checkgreen.utf8.Validasserted as a property, not on one examplewiki/index.mdregenerates clean;frontmatterlintreports 0 broken across the whole brainIt was four call sites, not one
len()counts bytes, the slice cuts bytes, so onå ä ö(2 bytes) or an em dash (3) the cut lands mid-character:internal/wiki/index.go:66wiki/index.mdon diskinternal/mcp/tools_context.go:176brain_contextexcerptsinternal/mcp/tools_context.go:197brain_contextexcerptsinternal/search/search.go:431brain_queryexcerptsOnly the first one was visible, and that is the part worth keeping.
wiki/index.mdis written raw to disk, so a strict decoder rejects the whole file — which is how the frontmatter lint found it. The other three feed MCP responses, whereencoding/jsonsilently substitutes U+FFFD. Those have been quietly eating the last character of every truncated Swedish excerpt, with no error, no log line, and nothing to notice — for however long they have existed.So the loud bug was the lucky one. Fixing only
index.gowould have closed this issue and left three live instances feeding models slightly-corrupted text.The test is the property, not the example
Walking every offset matters here specifically: a single example passes or fails on where that one cut happens to land, so an off-by-one could ship by picking a lucky boundary. Three more cases pin behaviour a naive rune fix gets wrong — the marker is appended only when something was actually dropped (an ellipsis on complete text is a lie about the content), the limit counts characters rather than bytes, and a non-positive limit returns empty instead of panicking.
The repair
8 entries in the index had an orphaned lead byte where an em dash was sliced in half:
Regenerated with the fixed writer rather than patched by hand — the file is generated, so hand-patching the bytes would have lasted until the next rebuild.
Closing.