Brain-quality sweep, batch 1: brain_edit/brain_update correctness + close-session docs #124

Merged
mathias merged 4 commits from sweep/brain-quality-batch1 into main 2026-09-22 21:27:11 +00:00
2 changed files with 31 additions and 0 deletions
Showing only changes of commit 1e57f49595 - Show all commits
+8
View File
@@ -3,6 +3,7 @@ package mcp
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"log/slog" "log/slog"
"strings" "strings"
@@ -180,6 +181,13 @@ func (s *Server) editWithScope(args json.RawMessage, scope notes.Scope) (notes.R
} }
res, err := notes.Edit(s.brainDir, a.Path, a.OldStr, a.NewStr, scope) res, err := notes.Edit(s.brainDir, a.Path, a.OldStr, a.NewStr, scope)
if err != nil { if err != nil {
if errors.Is(err, notes.ErrDegenerate) && a.OldStr == "" {
// hyperguild#115: "old_str is empty" alone doesn't say whether the
// caller sent it empty or a hop between the caller and here lost
// it. Naming the raw argument byte count makes that distinguishable
// without a second investigation.
return notes.Result{}, nil, fmt.Errorf("%w (raw arguments were %d bytes)", err, len(args))
}
return notes.Result{}, nil, err return notes.Result{}, nil, err
} }
@@ -70,6 +70,29 @@ func TestBrainReadAndEdit_RoundTripThroughJSONRPC(t *testing.T) {
"only the matched bytes may change") "only the matched bytes may change")
} }
func TestBrainEdit_EmptyOldStrErrorNamesRawArgumentBytes(t *testing.T) {
// hyperguild#115: a session reported "old_str is empty" with an old_str
// that, on their end, was never empty. The server-side path was
// unreachable to reproduce (verified separately: a fresh brain_edit call
// with real content round-trips cleanly), which points at something
// between a specific client and this call losing the field. The fix this
// issue actually asked for isn't a behavior change — old_str=="" must
// still refuse — it's that the refusal should say how many raw argument
// bytes it received, so "client sent empty" is distinguishable from
// "server received empty after some hop lost it" without a second
// session's worth of guessing.
dir := newBrainRepo(t)
srv := mcp.NewServer(dir, nil, nil, nil)
resp := call(t, srv, "brain_edit", map[string]any{
"path": "wiki/homelab/facts/n.md", "old_str": "", "new_str": "BETA",
})
errMsg, ok := resp["error"].(map[string]any)["message"].(string)
require.True(t, ok, "%v", resp)
assert.Contains(t, errMsg, "old_str is empty")
assert.Contains(t, errMsg, "raw arguments were", "%v", errMsg)
}
func TestBrainEdit_RefusesGovernanceFilesFromTheModelFacingTool(t *testing.T) { func TestBrainEdit_RefusesGovernanceFilesFromTheModelFacingTool(t *testing.T) {
dir := newBrainRepo(t) dir := newBrainRepo(t)
srv := mcp.NewServer(dir, nil, nil, nil) srv := mcp.NewServer(dir, nil, nil, nil)