Repository navigation
Report the session title from the turn, not from its end - #22
Merged
Merged
Conversation
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.
3 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-titleis on disk about three seconds after the prompt. Reporting only onStopmeant 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
StopPostToolUse+StopStopPostToolUse+StopStopPreInvocation+StopAntigravity gets
PreInvocationbecause itsPostToolUseis matched to write tools only, andPreInvocationis its earliest event either way.The
onceargumentThe 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.$TMPDIR/lich-title-$LICH_SESSION_ID; its existence is the signal, and it is checked before stdin is even read.curlgains-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 forStop.Stopruns unlatched and keeps scanning. That is the path a later retitle arrives on.Tests
runHooknow gives every hook a scratch$TMPDIRof 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:
oncereports 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)session-title reported once per session stops scanningsh -nclean on every hook script