From e1bef60d2d50962cf789f252bc61601aec1e028d Mon Sep 17 00:00:00 2001 From: Hongyi Shen Date: Sun, 20 Sep 2026 13:01:18 -0700 Subject: [PATCH 1/2] claude: drop the user records the harness wrote itself MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude Code files background-task reports, local command output and its caveat, and injected reminders as type:"user" records. catchup read them as turns, so a handoff put XML in the user's mouth, --last N spent exchanges on records nobody typed, and a listing could match or quote them. A user record whose text is nothing but those wrappers is now skipped, recognised by an explicit list of wrapper names matched at the start of the message. A slash command and a ! bash line stay: those record something the user did, and their text is the request. So does a record that says something of its own, and pasted markup — a message opening with
or is a turn the user meant to send. --- internal/claude/claude.go | 68 ++++++++++++++++- internal/claude/claude_test.go | 135 +++++++++++++++++++++++++++++++++ internal/cli/cli_test.go | 31 ++++++++ 3 files changed, 232 insertions(+), 2 deletions(-) diff --git a/internal/claude/claude.go b/internal/claude/claude.go index fb03c7d..d5c8188 100644 --- a/internal/claude/claude.go +++ b/internal/claude/claude.go @@ -17,8 +17,9 @@ // // Ignored by default: tool_use, successful tool_result, thinking, queue/mode/ // permission bookkeeping, file-history snapshots, last-prompt, subagent -// (isSidechain) and injected (isMeta) entries, subagent files under -// */subagents/*, and .claude/transcripts/ses_*.jsonl (v1). +// (isSidechain) and injected (isMeta) entries, user records the harness wrote +// itself (see harnessWrappers), subagent files under */subagents/*, and +// .claude/transcripts/ses_*.jsonl (v1). package claude import ( @@ -289,6 +290,9 @@ func readThread(fi fileInfo) (session.Thread, error) { if role == "" { continue } + if role == session.RoleUser && harnessOnly(text) { + continue // a user record the harness wrote itself, not a turn + } entries = append(entries, session.Entry{Kind: session.KindMessage, Role: role, Text: text, Time: ts}) } @@ -362,6 +366,66 @@ func joinText(blocks []claudeBlock, raw json.RawMessage) string { return strings.Join(parts, "\n") } +// harnessWrappers are the XML wrappers Claude Code writes into type:"user" +// records that are neither the user's words nor the user's act: a background +// task's report, what a local command printed, its caveat, and an injected +// reminder. The wrappers that record something the user did — the +// slash-command trio, a ! bash line — stay on the timeline, however noisy, +// because their text is the request. +// +// Only these names, and only spelled in full: markup a user pasted (
, +// ) is a turn they meant to send, and is +// not . +var harnessWrappers = map[string]bool{ + "local-command-caveat": true, "local-command-stdout": true, + "system-reminder": true, "task-notification": true, +} + +// harnessOnly reports whether a user message is nothing but harness wrappers. +// One record can carry several in a row. A wrapper with no closing tag is not +// one the harness wrote, so the record is kept. +// +// Nothing but: a record that still says something once its wrappers are read +// off is kept whole, text and wrappers together. Every injected record in the +// corpus this was measured against is wrapper-only, so trimming a mixed one +// would buy no quiet while risking the words of a user whose message happens to +// open with or a pasted reminder. +func harnessOnly(text string) bool { + rest, seen := strings.TrimSpace(text), false + for { + name, ok := openTag(rest) + if !ok || !harnessWrappers[name] { + return seen && rest == "" + } + seen = true + _, end := session.IndexFold(rest, "") + if end < 0 { + return false + } + rest = strings.TrimSpace(rest[end:]) + } +} + +// openTag returns the lowercased name of a bare element opening s. +// Anything else — attributes, a closing tag, a name with punctuation — is not +// one of the wrappers, which the harness always writes bare. +func openTag(s string) (string, bool) { + if !strings.HasPrefix(s, "<") { + return "", false + } + end := strings.IndexByte(s, '>') + if end < 2 { + return "", false + } + name := s[1:end] + for _, r := range name { + if r != '-' && r != '_' && !('0' <= r && r <= '9') && !('a' <= r && r <= 'z') && !('A' <= r && r <= 'Z') { + return "", false + } + } + return strings.ToLower(name), true +} + func normalizeRole(role string) string { switch role { case session.RoleUser, session.RoleAssistant: diff --git a/internal/claude/claude_test.go b/internal/claude/claude_test.go index 76b8418..0d9f51f 100644 --- a/internal/claude/claude_test.go +++ b/internal/claude/claude_test.go @@ -84,6 +84,141 @@ func TestReadThread(t *testing.T) { } } +// The harness files records of its own under role user: a background task's +// report, what a local command printed, reminders. Every record below is +// synthetic. The ones that keep their text are the reason the rule is a name +// list rather than "starts with a tag" — and why a slash command and a ! bash +// line, which are things the user did, stay on the timeline. +const harnessTranscript = `{"type":"user","sessionId":"sess-h","cwd":"/home/u/src/catchup","timestamp":"2026-06-26T10:00:00Z","message":{"role":"user","content":"land the parser fix"}} +{"type":"user","sessionId":"sess-h","timestamp":"2026-06-26T10:00:10Z","message":{"role":"user","content":"t1completedthe agent reported back"}} +{"type":"user","sessionId":"sess-h","timestamp":"2026-06-26T10:00:20Z","message":{"role":"user","content":"/review\nreview\nthe parser branch"}} +{"type":"user","sessionId":"sess-h","timestamp":"2026-06-26T10:00:30Z","message":{"role":"user","content":"branch is clean"}} +{"type":"user","sessionId":"sess-h","timestamp":"2026-06-26T10:00:40Z","message":{"role":"user","content":"ls internal"}} +{"type":"user","sessionId":"sess-h","timestamp":"2026-06-26T10:00:50Z","message":{"role":"user","content":"claude\nsession"}} +{"type":"user","sessionId":"sess-h","timestamp":"2026-06-26T10:01:00Z","message":{"role":"user","content":"\n\n mind the budget"}} +{"type":"user","sessionId":"sess-h","timestamp":"2026-06-26T10:01:10Z","message":{"role":"user","content":[{"type":"text","text":"the command was run locally"}]}} +{"type":"user","sessionId":"sess-h","timestamp":"2026-06-26T10:01:20Z","message":{"role":"user","content":"cut short"}} +{"type":"user","sessionId":"sess-h","timestamp":"2026-06-26T10:01:30Z","message":{"role":"user","content":"be brief\n\nwhy did the build fail?"}} +{"type":"user","sessionId":"sess-h","timestamp":"2026-06-26T10:01:40Z","message":{"role":"user","content":[{"type":"text","text":"/mine is my own tag, keep it"}]}} +{"type":"user","sessionId":"sess-h","timestamp":"2026-06-26T10:01:50Z","message":{"role":"user","content":"
hello
why does this not render?"}} +{"type":"user","sessionId":"sess-h","timestamp":"2026-06-26T10:02:00Z","message":{"role":"user","content":"my own notes"}} +{"type":"user","sessionId":"sess-h","timestamp":"2026-06-26T10:02:10Z","message":{"role":"user","content":"the log shows after every run"}} +{"type":"assistant","sessionId":"sess-h","timestamp":"2026-06-26T10:02:20Z","message":{"role":"assistant","content":[{"type":"text","text":"reading it now"}]}} +` + +func TestHarnessWrittenUserRecordsDropped(t *testing.T) { + root := t.TempDir() + writeTranscript(t, root, "-home-u-src-catchup", "sess-h", harnessTranscript, time.Now()) + + p := New() + roots := session.Roots{Claude: root} + src, err := p.Resolve(context.Background(), roots, "sess-h") + if err != nil { + t.Fatal(err) + } + th, err := p.Read(context.Background(), src) + if err != nil { + t.Fatal(err) + } + want := []string{ + "land the parser fix", + // A slash command and a ! bash line are things the user did: the args + // and the command are the request, so both records stay. + "/review\nreview\nthe parser branch", + "ls internal", + "claude\nsession", + // A wrapper that never closes is not the harness's spelling: kept. + "cut short", + "be brief\n\nwhy did the build fail?", // mixed: kept whole + "/mine is my own tag, keep it", + "
hello
why does this not render?", + "my own notes", + "the log shows after every run", + "reading it now", + } + var got []string + for _, e := range th.Entries { + got = append(got, e.Text) + } + if len(got) != len(want) { + t.Fatalf("got %d entries, want %d: %q", len(got), len(want), got) + } + for i, w := range want { + if got[i] != w { + t.Errorf("entry %d = %q, want %q", i, got[i], w) + } + } + // A dropped record's text is searchable nowhere: not in a query, not in a row. + for _, gone := range []string{"the agent reported back", "branch is clean", + "mind the budget", "run locally"} { + if strings.Contains(th.VisibleText(), gone) { + t.Errorf("harness text %q survived into the timeline", gone) + } + } +} + +// The record the harness wrote must not answer a query, title a listing, or +// stand in for the session's opening message. +func TestHarnessRecordsDoNotAnswerListings(t *testing.T) { + root := t.TempDir() + writeTranscript(t, root, "-home-u-src-catchup", "sess-h", harnessTranscript, time.Now()) + p := New() + roots := session.Roots{Claude: root} + + sums, err := p.List(context.Background(), roots, session.ListOptions{Query: "the agent reported back"}) + if err != nil { + t.Fatal(err) + } + if len(sums) != 0 { + t.Errorf("query matched harness text: %+v", sums) + } + sums, err = p.List(context.Background(), roots, session.ListOptions{Query: "parser fix"}) + if err != nil { + t.Fatal(err) + } + if len(sums) != 1 { + t.Fatalf("got %d rows for a query the user's own words satisfy, want 1", len(sums)) + } + if sums[0].Preview != "land the parser fix" { + t.Errorf("preview = %q, want the first typed message", sums[0].Preview) + } + if sums[0].Match == nil || !strings.Contains(sums[0].Match.Text, "parser fix") { + t.Errorf("match = %+v, want the passage that contains the query", sums[0].Match) + } +} + +func TestHarnessOnly(t *testing.T) { + tests := []struct { + name, in string + want bool + }{ + {"whole message", "report", true}, + {"trailing newline", "ok\n", true}, + {"several wrappers", "cr", true}, + {"leading blank lines", "\n\nr", true}, + {"slash command", "/xy", false}, + {"bash line", "ls", false}, + {"uppercase", "r", true}, + {"mixed case close", "r", true}, + {"unclosed", "r without an end", false}, + {"wrapper then prompt", "r\n\nship it", false}, + {"caveat then prompt", "c ship it", false}, + {"wrapper after prompt", "ship it r", false}, + {"pasted html", "
hi
", false}, + {"custom element", "hi", false}, + {"shared prefix", "mine", false}, + {"attributes", `x`, false}, + {"plain text", " hello ", false}, + {"closing tag first", "
stray", false}, + {"empty", "", false}, + } + for _, tt := range tests { + if got := harnessOnly(tt.in); got != tt.want { + t.Errorf("%s: harnessOnly(%q) = %v, want %v", tt.name, tt.in, got, tt.want) + } + } +} + func TestListSkipsSubagents(t *testing.T) { root := t.TempDir() writeTranscript(t, root, "-proj", "main-1", transcript, time.Now()) diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index 19ee5d7..2076484 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -385,6 +385,37 @@ func TestLastTurns(t *testing.T) { } } +// A record the harness filed under role user must not spend one of the +// exchanges --last was asked for, nor stand in for the session's last word. +func TestLastTurnsSkipsHarnessRecords(t *testing.T) { + root := t.TempDir() + dir := filepath.Join(root, "projects", "proj") + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatal(err) + } + body := `{"type":"user","sessionId":"sess-h","cwd":"/home/u/proj","timestamp":"2026-06-26T10:00:00Z","message":{"role":"user","content":"first question"}} +{"type":"assistant","sessionId":"sess-h","timestamp":"2026-06-26T10:00:05Z","message":{"role":"assistant","content":[{"type":"text","text":"first answer"}]}} +{"type":"user","sessionId":"sess-h","timestamp":"2026-06-26T10:01:00Z","message":{"role":"user","content":"second question"}} +{"type":"assistant","sessionId":"sess-h","timestamp":"2026-06-26T10:01:05Z","message":{"role":"assistant","content":[{"type":"text","text":"second answer"}]}} +{"type":"user","sessionId":"sess-h","timestamp":"2026-06-26T10:02:00Z","message":{"role":"user","content":"completeda background agent reported"}} +` + if err := os.WriteFile(filepath.Join(dir, "sess-h.jsonl"), []byte(fxBody(body)), 0o644); err != nil { + t.Fatal(err) + } + + out := run(t, session.Roots{Claude: root}, "claude", "--last", "1") + for _, want := range []string{"second question", "second answer"} { + if !strings.Contains(out, want) { + t.Errorf("--last 1 missing %q:\n%s", want, out) + } + } + for _, unwanted := range []string{"task-notification", "a background agent reported", "first question"} { + if strings.Contains(out, unwanted) { + t.Errorf("--last 1 shows %q:\n%s", unwanted, out) + } + } +} + // TestClampEntriesBoundsFailures covers the two fields a failure carries: a // dumped output takes the generated ceiling, an oversized input the pasted // one, and the entry keeps its kind and tool either way. From 6e2cb713217fd04f30b9d9a4041a7b2df71eddf6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 20 Sep 2026 20:43:42 +0000 Subject: [PATCH 2/2] claude: keep one test for the harness-record rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The listing, --last and harnessOnly unit tables each re-checked what the end-to-end test already decides. TestHarnessWrittenUserRecordsDropped reads a transcript through Resolve and Read and asserts the exact entry list, so it covers both halves that matter: the records that go, and the ones a name list exists to keep — a slash command, a ! bash line, an unclosed wrapper, a reminder with a question after it, pasted markup, and . Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01K1bnmAA2o7tPADLVLD46cA --- internal/claude/claude_test.go | 62 ---------------------------------- internal/cli/cli_test.go | 31 ----------------- 2 files changed, 93 deletions(-) diff --git a/internal/claude/claude_test.go b/internal/claude/claude_test.go index 0d9f51f..d560a93 100644 --- a/internal/claude/claude_test.go +++ b/internal/claude/claude_test.go @@ -157,68 +157,6 @@ func TestHarnessWrittenUserRecordsDropped(t *testing.T) { } } -// The record the harness wrote must not answer a query, title a listing, or -// stand in for the session's opening message. -func TestHarnessRecordsDoNotAnswerListings(t *testing.T) { - root := t.TempDir() - writeTranscript(t, root, "-home-u-src-catchup", "sess-h", harnessTranscript, time.Now()) - p := New() - roots := session.Roots{Claude: root} - - sums, err := p.List(context.Background(), roots, session.ListOptions{Query: "the agent reported back"}) - if err != nil { - t.Fatal(err) - } - if len(sums) != 0 { - t.Errorf("query matched harness text: %+v", sums) - } - sums, err = p.List(context.Background(), roots, session.ListOptions{Query: "parser fix"}) - if err != nil { - t.Fatal(err) - } - if len(sums) != 1 { - t.Fatalf("got %d rows for a query the user's own words satisfy, want 1", len(sums)) - } - if sums[0].Preview != "land the parser fix" { - t.Errorf("preview = %q, want the first typed message", sums[0].Preview) - } - if sums[0].Match == nil || !strings.Contains(sums[0].Match.Text, "parser fix") { - t.Errorf("match = %+v, want the passage that contains the query", sums[0].Match) - } -} - -func TestHarnessOnly(t *testing.T) { - tests := []struct { - name, in string - want bool - }{ - {"whole message", "report", true}, - {"trailing newline", "ok\n", true}, - {"several wrappers", "cr", true}, - {"leading blank lines", "\n\nr", true}, - {"slash command", "/xy", false}, - {"bash line", "ls", false}, - {"uppercase", "r", true}, - {"mixed case close", "r", true}, - {"unclosed", "r without an end", false}, - {"wrapper then prompt", "r\n\nship it", false}, - {"caveat then prompt", "c ship it", false}, - {"wrapper after prompt", "ship it r", false}, - {"pasted html", "
hi
", false}, - {"custom element", "hi", false}, - {"shared prefix", "mine", false}, - {"attributes", `x`, false}, - {"plain text", " hello ", false}, - {"closing tag first", " stray", false}, - {"empty", "", false}, - } - for _, tt := range tests { - if got := harnessOnly(tt.in); got != tt.want { - t.Errorf("%s: harnessOnly(%q) = %v, want %v", tt.name, tt.in, got, tt.want) - } - } -} - func TestListSkipsSubagents(t *testing.T) { root := t.TempDir() writeTranscript(t, root, "-proj", "main-1", transcript, time.Now()) diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index 2076484..19ee5d7 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -385,37 +385,6 @@ func TestLastTurns(t *testing.T) { } } -// A record the harness filed under role user must not spend one of the -// exchanges --last was asked for, nor stand in for the session's last word. -func TestLastTurnsSkipsHarnessRecords(t *testing.T) { - root := t.TempDir() - dir := filepath.Join(root, "projects", "proj") - if err := os.MkdirAll(dir, 0o755); err != nil { - t.Fatal(err) - } - body := `{"type":"user","sessionId":"sess-h","cwd":"/home/u/proj","timestamp":"2026-06-26T10:00:00Z","message":{"role":"user","content":"first question"}} -{"type":"assistant","sessionId":"sess-h","timestamp":"2026-06-26T10:00:05Z","message":{"role":"assistant","content":[{"type":"text","text":"first answer"}]}} -{"type":"user","sessionId":"sess-h","timestamp":"2026-06-26T10:01:00Z","message":{"role":"user","content":"second question"}} -{"type":"assistant","sessionId":"sess-h","timestamp":"2026-06-26T10:01:05Z","message":{"role":"assistant","content":[{"type":"text","text":"second answer"}]}} -{"type":"user","sessionId":"sess-h","timestamp":"2026-06-26T10:02:00Z","message":{"role":"user","content":"completeda background agent reported"}} -` - if err := os.WriteFile(filepath.Join(dir, "sess-h.jsonl"), []byte(fxBody(body)), 0o644); err != nil { - t.Fatal(err) - } - - out := run(t, session.Roots{Claude: root}, "claude", "--last", "1") - for _, want := range []string{"second question", "second answer"} { - if !strings.Contains(out, want) { - t.Errorf("--last 1 missing %q:\n%s", want, out) - } - } - for _, unwanted := range []string{"task-notification", "a background agent reported", "first question"} { - if strings.Contains(out, unwanted) { - t.Errorf("--last 1 shows %q:\n%s", unwanted, out) - } - } -} - // TestClampEntriesBoundsFailures covers the two fields a failure carries: a // dumped output takes the generated ceiling, an oversized input the pasted // one, and the entry keeps its kind and tool either way.