AI session titler overwrites user-set custom titles in long sessions (64 KB head/tail guard blind spot)

Status Open
Reported on v2.1.220
Maintainer reply None cached
Activity 1 comment · opened Aug 10, 2026

AI session titler overwrites user-set custom titles in long sessions (64 KB head/tail guard blind spot)

Component: Claude Code VS Code extension
Extension version reproduced on: 2.1.220 (darwin-arm64); offending code confirmed still present in 2.1.224
Platform: macOS (Darwin 25.2.0)
Severity: Low impact per event, but continuous — the title flips back dozens of times per session

Summary

When a user renames a conversation in the sidebar, the rename is persisted as a custom-title
record appended to the session .jsonl. The automatic AI titler is supposed to stand down once a
custom title exists, but its check only scans the first 64 KB and last 64 KB of the transcript.
In long sessions the custom-title record falls outside both windows, the guard misses it, and an
ai-title record is appended — overwriting the user's title, since the last title record wins.

The result is a persistent tug-of-war: the user renames, the titler reverts it, repeat.

Root cause

extension.js, storage-layer renameSession(sessionId, title, onlyIfNoCustomTitle):

async renameSession(e, t, r) {
  let n = Hc(this.projectRoot), i = Vn.join(n, `${e}.jsonl`);
  if (r) {                                  // r = onlyIfNoCustomTitle (AI titler path)
    let s = await nB(i);
    if (!s) return true;
    if (ia(s.tail, "customTitle") || ia(s.head, "customTitle")) return true;   // <-- guard
    let a = { type: "ai-title", sessionId: e, aiTitle: t };
    await dn.promises.appendFile(i, JSON.stringify(a) + "\n");
    this.customTitles.set(e, t);
    return false;
  }
  let o = { type: "custom-title", sessionId: e, customTitle: t };              // user rename path
  await dn.promises.appendFile(i, JSON.stringify(o) + "\n");
  this.customTitles.set(e, t);
  return false;
}

The guard depends on nB(), which reads only two fixed-size windows — not the whole file:

const uu = 65536;  // 64 KB

async function nB(e, t) {
  // ...
  let n = await r.stat(),
      i = Buffer.allocUnsafe(uu),
      o = await r.read(i, 0, uu, 0);          // head: first 64 KB
  let s = i.toString("utf8", 0, o.bytesRead),
      a = Math.max(0, n.size - uu),
      c = s;
  if (a > 0) {
    let l = await r.read(i, 0, uu, a);        // tail: last 64 KB
    c = i.toString("utf8", 0, l.bytesRead);
  }
  return { mtime: ..., size: n.size, head: s, tail: c };
}

ia(str, "customTitle") is a plain substring scan over those windows. Two consequences:

  1. Blind spot (primary): in a multi-megabyte transcript, everything between the first and last

64 KB is invisible to the guard. Sessions with large tool results can push a custom-title
record out of the tail window after only a handful of subsequent turns.

  1. TOCTOU (secondary): nB() samples the file before the append. A rename that lands between

the read and the write is not seen either.

Because resolution is last-record-wins, a single missed guard is enough to revert the title.

Evidence

Four sessions in one project, all 2.1.220. In every case the user set a JIRA-key title and the
final title record in the file is an ai-title:

| session | size | ai-title records | custom-title records | user's title | final record |
|---|---|---|---|---|---|
| 962f9301… | 4.1 MB | 108 | 67 | tri-3551/tri-3562/saas-3999 | ai-title |
| 187bc3c3… | 3.9 MB | 47 | 43 | tri-3547/saas-3994 | ai-title |
| 5b14c626… | 2.8 MB | 19 | 13 | tri-3531 | ai-title |
| faf39d00… | 1.5 MB | 21 | 7 | tri-3572 | ai-title |

Title records in file order from 962f9301…jsonl (2026-08-04 → 2026-08-10) — strict alternation,
64 consecutive clobbers:

line 1424   custom-title  tri-3551/tri-3562/saas-3999
line 1425   ai-title      Unpost original readings without deletion
line 1447   custom-title  tri-3551/tri-3562/saas-3999
line 1448   ai-title      Unpost original readings without deletion
line 1466   custom-title  tri-3551/tri-3562/saas-3999
line 1467   ai-title      Unpost original readings without deletion
...          (pattern repeats through line 1631)

The custom-title at line 1424 sits ~200 lines from EOF — far outside the 64 KB tail window — so
every later titler run re-appends its own title.

Reproduction

  1. Open a conversation in the VS Code extension and rename it in the sidebar (e.g. TRI-1234).
  2. Continue working until the transcript exceeds a few hundred KB, with large tool results so that

more than 64 KB of content accumulates after the rename.

  1. Observe the sidebar title revert to an auto-generated one.
  2. Confirm in ~/.claude/projects/<project>/<sessionId>.jsonl: an {"type":"ai-title",…} record

has been appended after the {"type":"custom-title",…} record.

Expected behavior

A user-set title is sticky. Once a custom-title record exists for a session, the AI titler must
never overwrite it, regardless of transcript size.

Suggested fixes

  • Track custom-title state outside the transcript scan — e.g. consult the already-maintained

this.customTitles map, or a per-session sidecar/index — instead of substring-scanning a sampled
window of the .jsonl.

  • Make resolution precedence-based rather than last-write-wins: when reading back, let any

custom-title beat any ai-title no matter the order.

  • If the transcript must remain the source of truth, scan the full file (or maintain a marker in the

head, which is always read) rather than head+tail sampling.

  • Stop re-appending an identical ai-title on every run — the alternation above wrote ~175 title

records into a single session.

Additional note

There appears to be no user-facing escape hatch: claude-code-settings.schema.json in 2.1.224
exposes no setting to disable AI session titling (terminalTitleFromRename is unrelated). Users who
rely on stable, self-chosen conversation titles currently have no way to opt out.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗