Skip to content

A session keeps the title we generated for it across a restart - #44

Merged
bharathm03 merged 1 commit into
developmentfrom
fix/title-regenerated-on-restart
Aug 29, 2026
Merged

A session keeps the title we generated for it across a restart#44
bharathm03 merged 1 commit into
developmentfrom
fix/title-regenerated-on-restart

Conversation

@bharathm03

Copy link
Copy Markdown
Contributor

Reported as "title getting regenerated every session start and stop". It was, and twice per start.

What was happening

Three gates guard the model spawn in maybeGenerateTitle, and only one of them was durable:

gate lives in survives a stop/start?
isAutoNameable persisted manuallyRenamed yes — but false for a generated name, by design
namer.hasFinalTitle SessionNamer no — dropped at PTY exit
titleAttempts.refused TitleAttempts no — dropped at PTY exit

applyAutoName deliberately never sets manuallyRenamed ("a derived name is not a name the user chose"), so gate 1 stays open forever, and the persisted row recorded nothing else about the name. On the next start nothing could tell Fix S3 retry backoff from Session 3.

So the SessionStart post landed with all three gates open:

  1. the native first-message read applied immediately — rename fix(bridge): serialize checkout runtime build/teardown so sessions stay deletable #1, back to the opening prompt, because the rank that normally blocks it was forgotten with the rest of the namer state;
  2. that read reports first-message, so generation fired — rename Run a worktree.setup block before an isolated session's agent starts #2.

And #2 was not a re-derivation of the old title. buildTitleContext returns the last MAX_MSGS messages, so on a resumed session it named wherever the conversation had drifted to. That is why the name kept moving rather than settling.

Why the obvious fix does not work

Keying the durable marker on agentSessionId looks right and fails. Claude Code's --resume does not keep the session id — it copies the transcript into a new file and appends under a fresh one:

d8d7c0c0-….jsonl:  740 lines  a244e174-…   ← original
                    94 lines  f95580a7-…   ← resume 1
                    18 lines  d8d7c0c0-…   ← resume 2 (the filename)

Three ids, one conversation. Every restart rotates it, so a key on that id would never match and the bug would survive the fix. Same fact explains a second trigger: TitleAttempts is keyed by agentSessionId, so a resume gets a fresh budget even without a bridge restart.

The fix

autoTitleRank on the session row records which signal produced the current name, written by applyAutoName at the same moment as the name so the two cannot drift. SessionNamer passes its rank through to the sink; applyAutoName enforces the same precedence durably, which is what kills rename #1. maybeGenerateTitle gates on hasFinalAutoTitle, which kills rename #2.

noteConversationStart answers the question the id cannot: the launch records whether it resumed anything. The first identity report spends that claim; every rotation after it is a real /clear and releases the title. A launch that resumes nothing releases it too — that genuinely is a new conversation.

Legacy rows are not backfilled: a file written before the field cannot say whether its name was generated or read off the opening prompt, and guessing "generated" would freeze those sessions against ever being named properly. Absent means "behave as before".

Verification

  • 11 new tests across session-manager-autoname and session-namer, covering the precedence after a reload, the resume continuation, the /clear release, a resume that reports the same id (which must still spend the claim), and the legacy row.
  • Mutation-checked: breaking the claim consumption fails exactly the test that covers it.
  • Full bridge suite: 2996 pass, 0 fail (3001 across 243 files).

Nothing app-side changes — autoTitleRank is persisted-only and toWire lists its fields explicitly.

One gap, stated plainly: the agent-core gate is a one-line delegation inside a closure with no direct test, the same shape as the hasFinalTitle guard beside it. The predicate it calls is tested.

Every guard against re-naming a session lived in memory: SessionNamer's rank ordering and the attempt budget are both dropped when the PTY exits, and the persisted row recorded only manuallyRenamed, which a generated name deliberately leaves false. So a stop/start reached the naming path with nothing standing in its way and renamed the session twice — once from the native first-message read, then again from a fresh model spawn. Not back to the same title either: buildTitleContext returns the LAST few messages, so the new name described wherever the conversation had drifted to.

Keying that on the agent's session id does not work, which is the part worth writing down. Claude Code's --resume copies the transcript into a NEW file and appends under a fresh id (measured: three ids in one lineage), so the same thread comes back wearing a different name and every restart would look like a new conversation.

The winning signal is therefore persisted beside the name as autoTitleRank, and a launch records at the time it happens whether it continues the previous conversation — the first identity report spends that claim, and any rotation after it is a real /clear. applyAutoName enforces the same precedence SessionNamer does, so a first-message or OSC title can no longer displace a generated one after a reload.

Legacy rows are not backfilled: a file written before the field cannot say which signal named it, and guessing would freeze those sessions against ever being named properly.
@bharathm03
bharathm03 merged commit 7033d10 into development Aug 29, 2026
3 checks passed
@bharathm03
bharathm03 deleted the fix/title-regenerated-on-restart branch August 29, 2026 06:36
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