fix(brain): unwrap wikilinks that name no entry at the write boundary
Every brain write accepted [[targets]] that resolve to nothing — skill names, repo names, agent-local memory-file slugs — and each one became a permanent [high] broken-link finding. The brain-audit digest grew 19 -> 63 high findings between 2026-08-26 and 2026-09-16; 53 of the 63 were exactly this, and none are fixable by brain-gardener's BrokenLinkFixer, which only repoints edit-distance-1 renames. There is no target to repoint to. StripUnresolvedLinks resolves each link against knowledge/ + wiki/ using brain-gardener's own resolver semantics (bare slug or wing/hall/slug path form, code spans excluded) and unwraps the misses to plain text, reporting them as Ref.Unlinked / the MCP `unlinked` field. Unwrap rather than reject: a refused write on claudewatcher's unattended ingest path is a lost learning, and the author's words are worth keeping even when the link is not. An empty corpus is a no-op, so a misconfigured brainDir cannot strip every link in a note. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -233,6 +233,15 @@ type brainWriteArgs struct {
|
||||
Hall string `json:"hall,omitempty"`
|
||||
}
|
||||
|
||||
// unlinkedNote explains the `unlinked` field to the caller that caused it.
|
||||
// The write landed; what is gone is the [[...]] around a target that names no
|
||||
// brain entry. Said here rather than only in a doc comment because the caller
|
||||
// is a model that will otherwise repeat it on the next write.
|
||||
const unlinkedNote = "these [[wikilink]] targets resolve to no brain entry, " +
|
||||
"so they were unwrapped to plain text. Link only entries that exist " +
|
||||
"(a filename under knowledge/ or wiki/); write the target entry first if " +
|
||||
"you want the link, and keep skills, repos and agent-local memory files as prose."
|
||||
|
||||
func (s *Server) brainWrite(ctx context.Context, args json.RawMessage) (json.RawMessage, error) {
|
||||
var a brainWriteArgs
|
||||
if err := json.Unmarshal(args, &a); err != nil {
|
||||
@@ -253,7 +262,12 @@ func (s *Server) brainWrite(ctx context.Context, args json.RawMessage) (json.Raw
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(map[string]string{"id": ref.ID, "path": ref.Path, "content_hash": ref.ContentHash})
|
||||
out := map[string]any{"id": ref.ID, "path": ref.Path, "content_hash": ref.ContentHash}
|
||||
if len(ref.Unlinked) > 0 {
|
||||
out["unlinked"] = ref.Unlinked
|
||||
out["unlinked_note"] = unlinkedNote
|
||||
}
|
||||
return json.Marshal(out)
|
||||
}
|
||||
|
||||
type brainUpdateArgs struct {
|
||||
@@ -297,9 +311,14 @@ func (s *Server) brainUpdate(ctx context.Context, args json.RawMessage) (json.Ra
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(map[string]any{
|
||||
out := map[string]any{
|
||||
"id": ref.ID, "path": ref.Path, "content_hash": ref.ContentHash, "superseded": ref.Superseded,
|
||||
})
|
||||
}
|
||||
if len(ref.Unlinked) > 0 {
|
||||
out["unlinked"] = ref.Unlinked
|
||||
out["unlinked_note"] = unlinkedNote
|
||||
}
|
||||
return json.Marshal(out)
|
||||
}
|
||||
|
||||
type brainGetArgs struct {
|
||||
|
||||
Reference in New Issue
Block a user