Skip to content

fix: Stop naming sessions /clear and refresh stale harvested titles - #1761

Merged
huang195 merged 3 commits into
rossoctl:mainfrom
huang195:fix/harvest-clear-title
Nov 10, 2026
Merged

huang195 merged 3 commits into
rossoctl:mainfrom
huang195:fix/harvest-clear-title

Conversation

@huang195

@huang195 huang195 commented Nov 10, 2026 •

Copy link
Copy Markdown
Member

What

A session that starts with /clear and stays idle shows /clear in 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 an ai-title.

Why

There are two causes:

  1. The harvester accepts /clear as a prompt. Claude Code writes the /clear turn as the first user turn of the new transcript. promptCandidate changed the envelope to /clear. For an idle session, that turn is the only prompt in the file.
  2. A named row is not read again. untitledSettled started a harvest only for rows with no harvested title. The harvested title also beats the served title. So the cached /clear hid the prompt, the ai-title and the proxy's own title. Only a restart or a picker visit cleared it.

Change

  • promptCandidate (core/observe/claude/prompt.go) rejects the /clear command. The last-prompt record and all user-turn tiers go through it. A session with only the /clear turn 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 cached LogModTime. This costs one Stat for 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, so untitledBackoff limits 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 UnwrapsSlashCommands case "name only" used /clear and expected the title /clear. It now uses /review.

Verification

  • go test -race ./observe/claude/ (core) and go test -race ./tui/ (cmd/agentop) pass. go vet is clean on both packages.
  • Each new test fails when its fix is removed: 4 subtests of TestTitleFromTranscript_ClearNamesNothing, and the "changed since the harvest" case of TestUntitledSettled_NamedRowWhoseTranscriptMoved.
  • Real transcript: the first 8 lines of a session started by /clear (the file as it was before the first prompt) give /clear on the released binary and the cwd on this branch. The full file gives its ai-title on both.

Not in this PR

  • A cached /clear entry whose transcript is gone, or is in a config dir that the harvest does not scan, keeps its title.
  • Other built-in commands with no arguments, for example /permissions or /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.
  • The cwd is still a "title" to 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

  • Bug Fixes
    • Named sessions are now rechecked when their transcript changes, helping keep session titles up to date without repeatedly rechecking unchanged transcripts.
    • The /clear command no longer becomes a session title. When it appears alongside another prompt, that prompt can still provide the title; similar commands such as /clearcache remain eligible.

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>
@huang195
huang195 requested a review from a team as a code owner November 10, 2026 01:36
@coderabbitai

coderabbitai Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Explain Change →

Warning

Review limit reached

You'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.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 888870a2-7e02-4d83-8628-6acdc9c1a13d
Commits

Reviewing files that changed from the base of the PR and between 42d3f40 and 6b67e7e.

Files selected for processing: 2

  • core/observe/claude/harvest.go
  • core/observe/claude/harvest_test.go

No actionable comments were generated in the recent review.

Walkthrough

The changes prevent /clear from becoming a session title and allow the Sessions pane to re-harvest a named session when its transcript changes. Tests cover prompt selection and transcript-based refresh behavior.

Changes: Session title handling

Layer / File(s) Summary
Filter /clear from session titles
core/observe/claude/prompt.go, core/observe/claude/harvest_test.go
promptCandidate rejects /clear when it is the first field. Tests cover /clear inputs, neighboring prompts, and /clearcache.
Refresh named sessions with changed transcripts
cmd/agentop/tui/session_metadata.go, cmd/agentop/tui/sessions_title_test.go
untitledSettled checks whether a named session's transcript has a newer modification time than its recorded harvest time. Tests cover re-harvesting and subsequent checks when the transcript has not changed.

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Possibly related issues

  • rossoctl/cortex#962 — The issue concerns session titles, which these changes affect by filtering /clear and refreshing titles after transcript changes.

Possibly related PRs

Suggested labels: ready-for-ai-review

Suggested reviewers: evaline-ju, clawgenti

Merge Risk: Minimal · previous review 42d3f

The title-filtering and transcript-refresh changes are ready to merge after normal checks.

Pre-merge checks: Passed 5

Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly summarizes both changes: rejecting /clear as a session title and refreshing stale harvested titles.
Docstring Coverage Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentat…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 6 functions across 4 files.

Finishing Touches

Generate unit tests (beta)

  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

Recent review info

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 6c1c6548-6ef4-4d9b-940e-c229bf679303
Commits

Reviewing files that changed from the base of the PR and between 5e76949 and 42d3f40.

Files selected for processing: 4

  • cmd/agentop/tui/session_metadata.go
  • cmd/agentop/tui/sessions_title_test.go
  • core/observe/claude/harvest_test.go
  • core/observe/claude/prompt.go

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

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 clawgenti left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@huang195
huang195 merged commit 7352c9e into rossoctl:main Nov 10, 2026
37 checks passed
@huang195
huang195 deleted the fix/harvest-clear-title branch November 10, 2026 02:48
@github-project-automation github-project-automation Bot moved this from New/ToDo to Done in Rossoctl Issue Prioritization Nov 10, 2026

@clawgenti clawgenti left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 /clear entries pinned by the incremental skip): addressed in 6b67e7e and covered by TestHarvest_IncrementalReparsesAClearTitle.
  • Suggestion on prompt.go (other built-in no-arg commands such as /permissions and /model can 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 (a LogFile outside the scanned config dir keeps transcriptMoved true, 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants