Skip to content

fix(session-cache): stop /clear from listing a phantom session - #157

Open
abate wants to merge 1 commit into
devsuitup:mainfrom
abate:fix/clear-phantom-session
Open

fix(session-cache): stop /clear from listing a phantom session#157
abate wants to merge 1 commit into
devsuitup:mainfrom
abate:fix/clear-phantom-session

Conversation

@abate

@abate abate commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Symptom

Clearing a session adds a session to the sidebar. Reported with this view — two entries titled /clear clear </com, the top one appearing the instant /clear ran:

▾ masia/ha
    /clear clear </com      just now · 2 msgs    21e03eba
    /clear clear </com      22h ago   · 2 msgs    f99ea0e1

Cause

/clear does not stay in the current transcript — the CLI opens a new .jsonl and writes only bookkeeping into it:

{"type":"user","isMeta":true,"message":{"content":"<local-command-caveat>Caveat: …</local-command-caveat>"}}
{"type":"user","message":{"content":"<command-name>/clear</command-name>\n<command-message>clear</command-message>\n<command-args></command-args>"}}
{"type":"system","subtype":"local_command","content":"<local-command-stdout></local-command-stdout>"}

readSessionFile() skipped the caveat but accepted the <command-name> record as the summary. Two consequences:

  1. Phantom session. The bookkeeping-only file has a summary and messageCount 2, so it is indexed and listed — a session the user never started.
  2. Wrong title, permanently. Every session actually started by /clear is titled from that same record; cleanDisplayName() strips the tags and leaves the truncated fragment /clear clear </com. 53 of the 99 transcripts in one project folder on the reporter's machine are titled this way.

Same class, found while fixing it: <local-command-stdout> also arrives as a type: "user" record, so a /model before the first prompt titled the session <local-command-stdout>Set model to Sonnet 5….

Fix

read-session-file.js now classifies each user record (classifyUserText()), shared by both readers:

kind records used as summary
skip <bash-input>, <bash-stdout>, <local-command-caveat>, <local-command-stdout> never
command <command-name>/x</command-name> + <command-args> fallback only
prompt everything else yes

A command record becomes the title only when the transcript also holds an assistant turn — so a headless /code-review high run still reads as /code-review high instead of raw XML. With no real turn, both readSessionFile() and readSessionDisplayHeader() return null and nothing is indexed, exactly how a brand-new session stays out of the sidebar until its first prompt.

Migration v9

Rows written by the old parser cannot heal on their own:

  • a phantom row sits on a file that never changes again, so the watcher never revisits it;
  • a mistitled row keeps its summary because the header-only refresh path merges h.summary || cachedEntry.summary, and the header read now declines to re-derive a bookkeeping summary.

v9 therefore purges rows whose summary starts with <command-name> or <local-command-stdout> (plus their search_map/search_content/search_fts entries) and drops the cache_meta gate for their folders, so the next reconcileCacheFromFilesystem() re-reads exactly those files — every other file in the folder still hits the fileMtime fast path. No full re-index, and a no-op on installs that never hit the bug.

Verification

Against the reporter's real ~/.claude/projects tree: the phantom (21e03eba, 2 KB) is no longer indexed, and every other session in that folder now shows its own first prompt instead of /clear … or <local-command-stdout>….

  • test/read-session-file-slash-command.test.js — the bookkeeping-only transcript, /clear + real prompt, command-as-fallback title, /model output, <scheduled-task> precedence
  • test/db-purge-command-summaries.test.js — v9 purges only the affected rows, clears only the affected folder gate, leaves the search tables clean, and is a no-op otherwise
  • .ai/contexts/session-cache.md — records the rule and why the migration exists
  • lint 0 errors, 593/593 tests pass

Takes effect on next launch; the affected folders re-index once at that startup.

/clear does not stay in the current transcript: the CLI opens a NEW jsonl
and writes only bookkeeping into it — a local-command caveat, the
<command-name>/clear</command-name> record, empty stdout. The indexer took
that record as the session summary, so every /clear added a "2 msgs"
sidebar entry titled "/clear clear </com", and every session actually
started by /clear kept that title instead of its first prompt (53 of the
99 transcripts in one project folder here).

The first-prompt scan now classifies user records. Local-command
bookkeeping is skipped, <local-command-stdout> included — the CLI writes a
command's own output back as a user record, which titled every session
where /model preceded the first prompt. A bare slash-command record is
only a fallback title, used when the transcript also holds an assistant
turn (a headless "/code-review high" run); with no real turn both readers
return null and nothing is indexed, the same way a brand-new session stays
out of the sidebar until its first prompt.

Migration v9 purges the rows the old parser wrote, plus the cache_meta
gate of their folders so the next reconcile re-reads exactly those files.
Neither kind of row can heal on its own: a phantom sits on a file that
never changes again, and a mistitled one keeps its summary because the
header-only refresh path only overwrites a summary it can re-derive.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant