Skip to content

Report the session title from the turn, not from its end - #22

Merged
omartelo merged 2 commits into
mainfrom
session-title-in-turn
Sep 2, 2026
Merged

omartelo merged 2 commits into
mainfrom
session-title-in-turn

Conversation

@omartelo

Copy link
Copy Markdown
Collaborator

Client side of omartelo/lich#418, which moves the contract's event mapping. The payload does not change, so no fixture does either.

Why

Claude Code fires its title call in parallel with the turn's own first model call — the ai-title is on disk about three seconds after the prompt. Reporting only on Stop meant a first turn that runs for ten minutes showed the card's placeholder name for all ten, and an interrupted turn never got a name at all, with the real title sitting there the whole time.

Codex and Antigravity are worse served still: their title is the user's own first message, which exists before the turn does.

What changed

harness before now
Claude Code Stop PostToolUse + Stop
Codex Stop PostToolUse + Stop
Antigravity Stop PreInvocation + Stop
opencode, omp unchanged — both already report during the turn
Crush, Cursor unchanged — neither has a title to report

Antigravity gets PreInvocation because its PostToolUse is matched to write tools only, and PreInvocation is its earliest event either way.

The once argument

The in-turn registrations pass once, and that is the whole difference: the script latches on the first title lich accepts and stops looking. Finding a title means scanning a transcript that grows all turn — 324ms on a 168MB one — and the in-turn hook fires per tool call, so without the latch a fifty-tool turn pays that fifty times to learn what it knew after the first.

  • The latch is an empty $TMPDIR/lich-title-$LICH_SESSION_ID; its existence is the signal, and it is checked before stdin is even read.
  • curl gains -f, so a refusal counts as one and does not latch — a title lich did not take is worth looking for again next call, rather than waiting for Stop.
  • Stop runs unlatched and keeps scanning. That is the path a later retitle arrives on.

Tests

runHook now gives every hook a scratch $TMPDIR of its own — a hook keeps state there and every test here shares one session id, so without it the latch leaked between tests and into the developer's /tmp. A test that means to exercise what carries across calls passes the same directory to both.

Three new cases: once reports one title and then stops; the unlatched form reports every time and carries a retitle; a refused report does not latch.

  • node --test tests/*.test.mjs — 194 pass (was 191)
  • Mutation-checked: removing the latch fails session-title reported once per session stops scanning
  • sh -n clean on every hook script
  • Fixtures untouched, so the drift job stays green

Claude Code writes its generated ai-title about three seconds into a turn, in
parallel with the turn's own first model call. Reporting only on Stop meant a
long first turn showed the card's placeholder name for its whole run, and an
interrupted turn never got a name at all — with the real title on disk the
whole time. Codex and Antigravity are worse served still: their title is the
user's own first message, which exists before the turn does.

Register report-title.sh on the turn's first event that can carry it —
PostToolUse on Claude Code and Codex, PreInvocation on Antigravity, which has
no post-tool event that is not matched to write tools — and keep the Stop
registration as the backstop.

The in-turn registrations pass `once`, which latches: finding a title means
scanning a transcript that grows all turn (324ms on a 168MB one, measured
warm) and the hook fires per tool call, so without it a fifty-tool turn would
pay that fifty times to learn what it knew after the first. The latch is an
empty file under $TMPDIR keyed by session id, written only once lich has
accepted the title — curl gains -f so a refusal counts as one and is looked
for again next call. Stop still runs unlatched, which is the path a later
retitle arrives on.

runHook now gives every hook a scratch $TMPDIR of its own, since a hook keeps
state there and every test here shares one session id; a test that means to
exercise what carries across calls passes the same directory to both.

Contract: lichdotdev/lich#418.
Not this branch's change: lich added a `cursor chat id` case to the
session-start fixture when it gained the provider, and the vendored copy here
predates it, so the drift job was already red on main. Refresh the copy and
name cursor in the provider set the suite asserts against.

Nothing else follows from it: lich installs no plugin on Cursor — the CLI runs
the Claude Code registration — so there is no registration here to add, and
PENDING_UPSTREAM stays empty.
@omartelo
omartelo merged commit 5ea2d26 into main Sep 2, 2026
3 checks passed
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