Skip to content

feat(store): record git branch per session; fix Claude firstPrompt - #699

Merged
lis186 merged 4 commits into
mainfrom
fix/session-branch-firstprompt
Oct 17, 2026
Merged

lis186 merged 4 commits into
mainfrom
fix/session-branch-firstprompt

Conversation

@lis186

@lis186 lis186 commented Oct 17, 2026

Copy link
Copy Markdown
Owner

Summary

Per-PR cost attribution needs to know which branch a session ran on and what it was asked. Two gaps fixed:

  • Git branch per turn and per session. Each recorded turn stores branch (read from .git/HEAD, following a linked worktree's gitdir:; no git spawn; 5 s per-cwd cache; omitted outside git / detached HEAD). Imported Claude and Codex transcripts use their own recorded branch. GET /_api/sessions / sessions.json keep the branch of the latest-received turn; ccxray usage --json adds topSessions[].branch. rebuild-index does not back-fill.
  • Claude firstPrompt was always null with Claude Code 2.1.302. The first user message opens with several <system-reminder> blocks; the prompt block is now picked, and an injected-only message never latches an empty value.
  • firstPrompt ignores Claude's title-generation request. Claude Code sends it first under the same session id with the prompt wrapped in <session>…</session>; only main turns latch firstPrompt now (reuses isAnthropicSubagent). Codex/Grok input[] bodies are unaffected.

Verification

  • node --test unit group (2 workers): 2069/2069 pass. Heavy group: all pass except the box-local Herdr Node launcher fallback › still uses the mise Node when it is supported, which fails the same way on main on this machine.
  • Real routed Claude Code (haiku) session in a herdr worktree through a hub running this branch: the entries carry branch, /_api/sessions shows branch and the typed prompt as firstPrompt (no <session> wrapper), and ccxray usage --json shows topSessions[].branch. Raw captures of the test session were deleted afterwards.

lis186 and others added 2 commits October 17, 2026 21:15
Claude Code 2.1.302 opens the first user message with several
<system-reminder> text blocks before the typed prompt. The title-gen
anchor skipped the whole message (it starts with a reminder), fell back
to the first text block, stripInjectedTags emptied it, and because
firstUserMsg was already latched it was never re-evaluated: firstPrompt
stayed null for the whole session.

- Anchor = first user text block that is non-empty after stripping
  injected tags (Grok <user_info> still skipped, <user_query> unchanged).
- recordFirstUserMsg never latches an empty value; firstUserMsg and
  firstPrompt are set together and re-evaluated while firstPrompt is null.
- session-index _upsert pulls firstPrompt from the store, like title: it is
  recorded in detectSession before the first entry creates the row, so
  setFirstPrompt found no row and sessions.json kept null.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Per-PR cost attribution needs the branch a turn ran on; nothing recorded it.

- server/git-branch.js: resolve the checked-out branch of a cwd by reading
  .git/HEAD (follows a linked worktree's `gitdir:` file), no process spawn,
  per-cwd 5s TTL cache, never throws. Detached HEAD, non-git, missing or
  unknown cwd -> null.
- Entry: new append-only INDEX_FIELDS `branch` (omitted when null), set by
  both wire parsers' buildEntryFields from the entry cwd. rebuild-index passes
  branch:null (the record-time branch is not in _req/_res). Importer uses the
  transcript's own branch (Claude `gitBranch`, Codex session_meta git.branch).
  responseId merge fills a missing branch from another copy.
- Session: session index keeps the branch of the latest-received turn that
  recorded one (never rolled back by older or branchless turns); exposed via
  sessions.json and /_api/sessions. `ccxray usage --json` adds
  topSessions[].branch; --cwd grouping unchanged.
- Docs: data-model, usage --json schema, wire-protocol, CHANGELOG, CLAUDE.md.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-17T14:05:18.427976Z d8df918 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

lis186 and others added 2 commits October 17, 2026 22:03
Claude Code 2.1.302 sends its title-generation request before the first
main turn, under the same session_id. Its user text wraps the typed prompt
in <session>...</session> plus title instructions, and detectSession latched
it as firstPrompt. detectSession now records firstUserMsg/firstPrompt only
for main turns, reusing isAnthropicSubagent (the classifier behind
entry.isSubagent). The gate applies to messages[] bodies only, so Codex and
Grok input[] bodies are unchanged. A title-gen-only session keeps firstPrompt
null until the main turn arrives.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d8df918ce3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread server/session-index.js
Comment on lines +487 to +489
if (entry.branch && (!s.branch || recvAt >= (s._branchRecvAt || 0))) {
s.branch = entry.branch;
s._branchRecvAt = recvAt;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Persist branch recency across session-index reloads

After a restart, sessions.json restores s.branch but not _branchRecvAt, because flush() omits every underscore-prefixed field. If the importer then supplies an older missing turn for this session, (s._branchRecvAt || 0) makes that older branch overwrite the persisted latest branch, contradicting the latest-received rule and corrupting /_api/sessions until a rebuild. Persist the branch timestamp or reconstruct it before accepting incremental entries.

Useful? React with 👍 / 👎.

@lis186
lis186 merged commit 955489d into main Oct 17, 2026
4 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