link cleared sessions back to the conversation they continue

/clear starts a new claude session and the list showed it as an unrelated
conversation. claude records no parent link anywhere — not in compactMetadata,
logicalParentUuid, summary.leafUuid, the per-session slug, or the live process
registry — so infer it: a cleared transcript opens with /clear, and /clear
happens inside one process, so the parent is the conversation in the same group
that was writing to disk at the instant this one began (4ms apart, measured).

matching is on per-minute activity rather than updatedAt, because resuming a
parent moves its end time past its child's birth and lost the link entirely for
two of the three cleared sessions here. the window is symmetric because clearing
makes claude summarise the conversation it is ending, so the parent's final
record can land after the child's first. ambiguity fails closed — the wrong
parent also renames the conversation.

read-only: nothing is written back to claude's store, and an explicit title
always wins.

also: cleared sessions were titled "<command-name>/clear</command-name>" because
claude does not set isMeta on slash commands; and the chat header was hardcoded
to undefined, so it read "New chat" above every conversation you opened.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 00:22:28 +00:00
co-authored by Claude Opus 5
parent 236541fa2a
commit 942cc2b61d
7 changed files with 313 additions and 17 deletions
+70
View File
@@ -275,6 +275,76 @@ step 4 above is the one I most want you to actually try, because it is the bug t
---
## 14. `/clear` no longer loses the thread
**What to click.** Open `/chat` on this project's group. Look at the top of the list. This conversation
should be titled **“Platform Arch 2”**, with a second meta line reading _continues Platform Arch_. Open
it: the header — which used to say “New chat” for every conversation you opened — now says “Platform
Arch 2”, and under it is a link back to the original. Click it, and you are in the conversation this one
grew out of.
Nothing was migrated to make that happen. It is derived from transcripts that were already on disk, so
it applied to your history the moment the server restarted.
**What was actually wrong.** You guessed right: `/clear` starts a genuinely new Claude session, and the
list was showing it as an unrelated conversation. Two separate defects were stacked on top of that.
The first was cosmetic but ugly: cleared sessions were titled `<command-name>/clear</command-name>`.
Claude records slash commands as ordinary user entries and — this is the part that fooled the original
code — **does not set `isMeta` on them**, so the “first human message” that becomes the title was the
`/clear` itself. Skipping `<command-name>` entries fixes it, and is why these sessions now fall back to
their first real message when no parent can be found.
The second is the interesting one. **Claude records no parent link anywhere.** I checked all five places
it could plausibly live: `compactMetadata` and `logicalParentUuid` (those are in-file compaction — see
below), `summary.leafUuid` (every one resolves inside its own transcript, never across files), the
per-session `slug` (a random name like `precious-brewing-kazoo`, not a lineage), and
`~/.claude/sessions/<pid>.json` (a live process registry, gone when the process is). So the link has to
be inferred, and this is the only inference in that file.
**What it infers from.** A cleared transcript opens with `/clear`, and `/clear` happens inside one
process — the old transcript's last write and the new one's first write are the same moment. Measured
here: 4ms apart. So the parent is the conversation in the same group that was writing to disk at the
instant this one began.
Two things about that rule are not obvious, and both were found by running it against your real history
rather than by reasoning:
- **The window is symmetric.** Clearing makes Claude summarise the conversation it is ending, and that
costs a model call — so the parent's _final_ record can land a few seconds **after** the child's first
one. One session here sits exactly there, and a before-only window silently lost it.
- **It matches on activity, never on “when did it end”.** My first version compared the child's birth to
the parent's last-modified time. That threw away any parent you later went back and **resumed**, because
resuming moves its end time days past its child's birth. Two of your three cleared sessions were in
that state and found no parent at all. Each transcript now carries the set of minutes it wrote in, which
survives resumption.
**Where it says nothing.** If two transcripts in the group were active in that minute, it names neither —
no parent is a far smaller mistake than the wrong parent, because the wrong parent also **renames** the
conversation. One session of yours (in `~/dockers`) still shows no parent: its nearest candidate was
active three hours away, which means the conversation it came from is genuinely not on disk any more.
That is the rule working, not failing.
**It is read-only, on purpose.** Nothing is written back into Claude's store, so the numbering is a
display-time guess that costs nothing if it is wrong and disappears on the next read. An explicit title —
Claude's own summary, or a rename you typed — always wins and is never overwritten. My suggestion is to
live with it for a week; if it never guesses wrong, we can promote it to a real `summary` record so the
name sticks in the terminal too. That is deliberately not built yet.
**`/compact` needed nothing.** I had assumed it forked a session like `/clear` does. It does not — it
appends to the same transcript and keeps the same session id (63 in-file compactions in the parent of
this conversation alone). So compaction was already invisible in the list, correctly.
**Also fixed in passing:** the chat header was hardcoded to `sessionTitle={undefined}`, so it read “New
chat” above every conversation you opened, resumed or not. It now takes the title from the same place
the list row does, which is also what keeps the two from disagreeing about the numbering.
**Not verified:** the inference itself I ran against your real transcripts and checked case by case (the
numbers above are measurements, not estimates). The UI — the meta line, the header, the link back — has
not been through a browser.
---
## Things noticed and deliberately left alone
- **`useChatWebSocket` silently ignores unparseable frames.** That one is intentional and the comment