Repository navigation
fix: Stop naming sessions /clear and refresh stale harvested titles - #1761
Conversation
A session that starts with /clear and stays idle gets the title "/clear". Claude Code writes the /clear turn as the first user turn of the new transcript. The harvester changed the command envelope to "/clear" and used it as the prompt title. The title then stayed. The sessions pane started a harvest only for rows with no harvested title. The prompt and the ai-title that came later did not reach the TITLE column until a restart or a picker visit. Two changes: - promptCandidate rejects the /clear command. This covers the last-prompt record and all user-turn tiers. A session with only the /clear turn now falls through to its cwd. - untitledSettled also counts a named live row when its transcript is newer than the cached LogModTime. The harvest is incremental, so it reads only that file again. A refresh that names nothing new counts as a miss, so the backoff limits how often an active session is read. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 12 minutes. View limit details
No actionable comments were generated in the recent review. Walkthrough
Merge Risk:
|
An active session's transcript changes on every turn. A re-read of its named row names nothing new, so the harvestedMsg handler counts it as a miss and the backoff widens. No test checked this. Without it, every pause in an active session could start a harvest at the 5s floor. The test also checks that no harvest starts after the re-read records the transcript's current mtime. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
clawgenti
left a comment
There was a problem hiding this comment.
Summary
Thanks @huang195. Both halves of the fix work in the case they target. I checked this against real Claude Code transcripts (v2.1.303), not only the synthetic fixtures. /clear, its aliases /reset and /new (Claude Code records all of them under the canonical <command-name>/clear</command-name>), and /clear <name> titled /clear / /clear my-feature on base. On this branch they fall through to the cwd. A typed /clear that Claude Code sends to the model as an ordinary message (lastPrompt: "/clear") also falls through. /CLEAR still titles as /CLEAR, which is correct: Claude Code does not recognise it and sends it to the model as a real prompt. /clearcache is kept.
The refresh cannot clobber anything a fresh launch would not produce. It re-runs titleFromTranscript over the whole file with the same tier precedence (ai-title/agent-name > lastPrompt > prompts > cwd), and harvest-over-served precedence is unchanged. A re-read that names nothing new is scored as a miss (pinned by TestSessionsPane_RefreshOfNamedRowCountsAsMiss). The Stat runs only after the backoff check passes, because untitledSettled is the last operand of the && in the tick gate.
Two non-blocking notes are inline. One more, outside the diff: cache entries already titled /clear whose transcript never moves (idle or abandoned sessions) are skipped by the incremental harvest forever, as the description says. If you would rather not depend on the --merge=false remedy, a one-line heal is possible: in ReadSessions, do not take the incremental skip when isClear(since[id].Title).
Verification (local): go vet is clean, and go test -race passes for core/observe/claude and cmd/agentop/tui at 42d3f40. Mutation check: removing isClear from promptCandidate fails 4 subtests of TestTitleFromTranscript_ClearNamesNothing. Reverting the transcriptMoved term fails TestUntitledSettled_NamedRowWhoseTranscriptMoved/changed_since_the_harvest and TestSessionsPane_RefreshOfNamedRowCountsAsMiss. That matches the claims in the PR body.
Author: huang195 (MEMBER — maintainer)
Areas reviewed: Go (core/observe/claude harvester, cmd/agentop TUI harvest gate), tests
Agent/IDE config (.claude/.vscode): none
Commits: 2 commits, all signed-off: yes
CI status: passing (35 pass, 1 skipped)
Decoder check: N/A, the PR adds no decoded fields
| // and a session left idle after it was titled "/clear". | ||
| func isClear(s string) bool { | ||
| f := strings.Fields(s) | ||
| return len(f) > 0 && f[0] == "/clear" |
There was a problem hiding this comment.
suggestion: /clear is fixed, but other built-in local commands have the same shape and still name a session. Example: a session whose only user turn is <command-name>/permissions</command-name>…<command-args></command-args> (followed by <local-command-stdout>). A transcript on my machine titles as /permissions at this head, and /model, /config and the like would do the same if run first and the session left idle. Claude Code's own first-prompt picker draws the line differently: it skips a <command-name> turn when the name is in builtInCommandNames() and <command-args> is empty, and keeps it as /<name> <args> otherwise. That keeps /review some/path.md and custom commands/skills, which is what UnwrapsSlashCommands wants. Fine as a follow-up, since the PR scopes itself to /clear and that case is covered. If you generalise it, a small allowlist of no-arg local commands would match Claude Code's rule better than a prefix test.
| return false | ||
| } | ||
| st, err := os.Stat(meta.LogFile) | ||
| return err == nil && st.ModTime().After(meta.LogModTime) |
There was a problem hiding this comment.
nit: This stays true for good when the harvester cannot refresh the entry. The case is a live row whose LogFile sits under a config dir other than the one claude.Harvest scans, e.g. the entry was written with a different CLAUDE_CONFIG_DIR and is carried forward by the merge. ReadSessions never sees that file, so LogModTime never advances, and every settled pause in that session starts a harvest. The backoff bounds it at one walk per untitledBackoffCap, the same steady state the served-title rows already accept, so this is not blocking. Two possible fixes: a sentence in the doc comment, or skipping when meta.AgentConfigDir differs from the dir the harvester reads. Separately, "One Stat per named live row per tick" overstates the cost. The gate evaluates untitledSettled only after now.Sub(m.lastHarvest) >= wait, so it is one Stat per row per backoff window.
Fixes review: a cached "/clear" title on an idle transcript was kept by the mtime skip Files: - core/observe/claude/harvest.go - core/observe/claude/harvest_test.go Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
clawgenti
left a comment
There was a problem hiding this comment.
Summary
Thanks @huang195. The new commit 6b67e7e adds the heal I suggested outside the diff in my earlier review: ReadSessions no longer takes the incremental skip when the cached title is /clear. With it, an idle session that an older build titled /clear gets its real title on the next launch, and you don't have to fall back on --merge=false. It can't loop. A re-parse can't produce a /clear title, because promptCandidate now rejects it, so the next harvest skips that entry as usual. An entry whose transcript is gone, or lives in a config dir this harvest doesn't scan, is never visited, so it keeps its title. The PR description already says so.
Prior findings:
- Out-of-diff note (cached
/clearentries pinned by the incremental skip): addressed in 6b67e7e and covered byTestHarvest_IncrementalReparsesAClearTitle. - Suggestion on
prompt.go(other built-in no-arg commands such as/permissionsand/modelcan still name an idle session): unchanged. The PR lists it under "Not in this PR", so it's fine as a follow-up. - Nit on
session_metadata.go(aLogFileoutside the scanned config dir keepstranscriptMovedtrue, and the "one Stat per named live row per tick" wording overstates the cost): unchanged and still non-blocking. The backoff cap bounds the re-reads.
Verification (local, at 6b67e7e): go vet is clean, and go test -race passes for core/observe/claude and cmd/agentop/tui. Mutation check: dropping && !isClear(since[id].Title) from the skip makes both subtests of TestHarvest_IncrementalReparsesAClearTitle fail (harvested/skipped = 0/1, title kept as /clear and /clear my-feature). So the new claim in the PR description holds.
Author: huang195 (MEMBER — maintainer)
Areas reviewed: Go (core/observe/claude harvester incremental skip and prompt filter, cmd/agentop TUI harvest gate), tests
Agent/IDE config (.claude/.vscode): none
Commits: 3 commits, all signed-off: yes
CI status: passing (34 pass, 2 skipped)
Decoder check: N/A, the PR adds no decoded fields
What
A session that starts with
/clearand stays idle shows/clearin agentop's TITLE column. The title then stays for as long as agentop runs, also after the user types a prompt and Claude Code writes anai-title.Why
There are two causes:
/clearas a prompt. Claude Code writes the/clearturn as the first user turn of the new transcript.promptCandidatechanged the envelope to/clear. For an idle session, that turn is the only prompt in the file.untitledSettledstarted a harvest only for rows with no harvested title. The harvested title also beats the served title. So the cached/clearhid the prompt, theai-titleand the proxy's own title. Only a restart or a picker visit cleared it.Change
promptCandidate(core/observe/claude/prompt.go) rejects the/clearcommand. The last-prompt record and all user-turn tiers go through it. A session with only the/clearturn now falls through to its cwd.untitledSettled(cmd/agentop/tui/session_metadata.go) also counts a named live row when its transcript is newer than the cachedLogModTime. This costs oneStatfor each named live row on each tick. The harvest is incremental, so it reads only that file again. A refresh that names nothing new counts as a miss, sountitledBackofflimits how often an active session is read (5s, doubling, up to 3m).ReadSessions(core/observe/claude/harvest.go) does not skip a cached entry whose title is/clear, also when its transcript did not change. The next agentop launch re-reads it. Its new title cannot be/clear, so later harvests skip it again.The existing
UnwrapsSlashCommandscase "name only" used/clearand expected the title/clear. It now uses/review.Verification
go test -race ./observe/claude/(core) andgo test -race ./tui/(cmd/agentop) pass.go vetis clean on both packages.TestTitleFromTranscript_ClearNamesNothing, and the "changed since the harvest" case ofTestUntitledSettled_NamedRowWhoseTranscriptMoved./clear(the file as it was before the first prompt) give/clearon the released binary and the cwd on this branch. The full file gives itsai-titleon both.Not in this PR
/clearentry whose transcript is gone, or is in a config dir that the harvest does not scan, keeps its title./permissionsor/model, can still name an idle session. Claude Code's own picker skips a<command-name>turn when the name is a built-in command and<command-args>is empty. A follow-up can use the same allowlist.sessionHasTitle. This PR gives a named row a re-read when its transcript moves. It does not change what counts as named.Assisted-By: Claude (Anthropic AI) noreply@anthropic.com
Summary by CodeRabbit
/clearcommand no longer becomes a session title. When it appears alongside another prompt, that prompt can still provide the title; similar commands such as/clearcacheremain eligible.