parser: capture sessionKind and promptSource from Claude Code transcripts - #1253
Conversation
roborev: Combined Review (
|
|
looking |
e35f71d to
0db83fc
Compare
roborev: Combined Review (
|
roborev: Combined Review (
|
roborev: Combined Review (
|
roborev: Combined Review (
|
7219443 to
386d033
Compare
roborev: Combined Review (
|
386d033 to
2f23a52
Compare
roborev: Combined Review (
|
roborev: Combined Review (
|
roborev: Combined Review (
|
roborev: Combined Review (
|
…ipts Claude Code JSONL entries carry two launch/prompt-provenance fields that the indexer currently discards: - `sessionKind` (session-level, e.g. "bg"): distinguishes headless / background sessions from interactive ones. - `promptSource` (per user turn, e.g. "typed", "queued", "system", "sdk"): distinguishes a human-typed prompt from a queued, system, or SDK-injected one. Neither is currently recoverable from the archive, so downstream consumers cannot tell a headless run from an interactive one, nor a typed prompt from an injected one. Both are captured by mirroring the existing `entrypoint`/`agent_label` handling exactly: - `sessionKind` becomes a session-identity field alongside `entrypoint` (first-non-empty-wins, incremental identity escalation, sessions column). It is blanked for the Cowork and Qoder parsers that reuse the Claude parse body, matching how they blank `entrypoint`. - `promptSource` becomes a per-message column alongside `source_type` and `is_sidechain`, including the retained-field whitelist in compactClaudeEntry and the message token fingerprints that the PG/DuckDB push fast paths compare against. Both columns default to empty, so historical transcripts that predate the fields and other agents that never emit them are unaffected. Capture is mirrored across the SQLite, PostgreSQL, and DuckDB stores and the artifact manifest. No counting or aggregation logic changes; this is field capture only. The source of truth for both fields is the Claude Code transcript format itself, where `sessionKind` and `promptSource` appear as top-level keys on session JSONL entries.
The batched MessageTokenFingerprints query omitted prompt_source while tokenFingerprintRow.appendTo already hashed it, so batched fingerprints disagreed with the per-session MessageTokenFingerprint for any non-empty value, causing perpetual push mismatches. The fixture now carries a non-empty prompt_source so the batched/per-session identity test pins the field. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rows now bind 26 parameters, so 39 rows per statement overflowed the historic 999-variable limit (1014 binds). Reduce to 38 rows (988). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cile The full-resync orphan copy's message column allowlist omitted prompt_source, silently dropping it for sessions whose source files no longer exist. The transcript-revision reconcile comparison now treats prompt_source as a peer of source_subtype; as with that column's own introduction, sources predating the column skip the reconcile pass. Adds a round-trip test covering session_kind and prompt_source survival. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Parser output changed but the data version stayed at 71, so already archived Claude sessions would never re-parse and their session_kind and prompt_source columns would stay empty indefinitely. Bumping forces the standard full resync, matching how agent_label/entrypoint landed in v66. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
recomputeHeapBytes sums per-message string field sizes to bound heap release; the new prompt_source field was missing from the sum. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per the provider format provenance rule, note the 2026-07-24 local reverification that observed the two top-level keys the parser now captures; evidence class stays no-public-source. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
segmentMessage mirrored source_subtype but omitted the new prompt_source field, so artifact exports dropped it. The canonical golden is unchanged because the field is omitempty and absent there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follows the dataVersion bump for session_kind/prompt_source backfill; the pin test's name and reason now reference the new version. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SidebarSessionIndexRow carried agent_label and entrypoint but not the new session_kind, so the sidebar API could surface one identity field and not the other. Adds the column to the SQLite, PostgreSQL, and DuckDB sidebar queries and scans, keeping all backends in lockstep. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
npm run generate:api picked up the new fields on DbMessage, DbSession, and DbSidebarSessionIndexRow. Unrelated generator drift from earlier merges (duckdb attach_timeout, vector push flags, source-layout stats) is left out to keep this change scoped; it needs its own regen chore. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The mirror gained sessions.session_kind and messages.prompt_source; per the mirror invariant, a schema change bumps SchemaVersion and forces a full rebuild. The column shape gate already rebuilds old mirrors, so this makes the version metadata agree with the shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
extractQueuedCommand dropped the top-level promptSource on type=attachment queued_command entries, so queued prompts stored an empty messages.prompt_source while standard user turns captured it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Exports already carry prompt_source on segment messages, but segmentMessage.dbMessage() dropped it during import, so round-tripped archives lost the field. Copy it through and extend the round-trip test to cover the decode side.
session_kind is export-visible via the artifact manifest, but the artifact_sessions_update_queue trigger's change-detection list did not include it, so a session-kind-only update on an already-published session left its manifest stale. Add the column to the trigger and pin it with an export-queue test.
The manifest and segment schemas gained session_kind and prompt_source without a version bump, so pre-provenance peers would import new artifacts under versions they claim to understand, silently drop the unknown fields, and mark the import complete. Advance the manifest format to v3 and the message segment format to v2 so older peers park new artifacts behind the future-version gate until they upgrade, and add explicit min-decode versions so v2 manifests and v1 segments still decode with the provenance fields defaulting to empty.
artifactMessageRawBytesSQL bounds the raw string bytes materialized for one artifact export, but it omitted the new prompt_source column, so preflight undercounted message bytes against the explicit MessageBytes limit. Include the column and pin the boundary with a PromptSource raw-byte test.
ac9b3ed to
bd05ef3
Compare
roborev: Combined Review (
|
What
Capture two Claude Code launch/prompt-provenance fields that the indexer currently reads past and discards:
sessionKind— session-level (e.g."bg"). Present on background/headless sessions, absent on interactive ones. Lands in a newsession_kindcolumn onsessions.promptSource— per user turn (e.g."typed","queued","system","sdk"). Marks whether a turn was a human-typed prompt or a queued/system/SDK-injected one. Lands in a newprompt_sourcecolumn onmessages.Why
Both signals exist on essentially every current Claude Code transcript but are thrown away at parse time, so the archive can't answer two questions it otherwise has the data for: was a session interactive or headless, and was a given prompt typed by a human or injected. This mirrors the recently-added
entrypointcapture — same shape of information, same reason to keep it.How
The change follows the existing
entrypoint/agent_labelandsource_type/is_sidechainhandling rather than inventing a new pattern:sessionKindis treated as a session-identity field alongsideentrypoint: first-non-empty-wins across the file, wired into the incremental identity-escalation check, and blanked by the Cowork and Qoder parsers that reuse the Claude parse body (exactly as they blankentrypoint).promptSourceis treated as a per-message source column alongsidesource_type/is_sidechain, including the retained-field whitelist incompactClaudeEntryand the message token fingerprints the PG/DuckDB push fast-paths compare against.Both columns are
NOT NULL DEFAULT '', so transcripts that predate the fields and other agents that never emit them read empty and are otherwise untouched. AdataVersionbump (79) triggers the standard reparse so existing Claude rows backfill both fields. Capture is mirrored across the SQLite, Postgres, and DuckDB stores (mirrorSchemaVersion9) and the artifact wire format, where the manifest advances to v3 and message segments to v2: older artifacts still decode with the fields empty, while pre-provenance peers park new artifacts behind the existing future-version gate instead of silently dropping the fields. The fields also round-trip through artifact import, re-enqueue a published session's export when onlysession_kindchanges, and count toward the export preflight's message-byte bound.Scope / limitations
session_kind/prompt_sourcebackfill via the dataVersion-79 reparse; rows whose source files no longer exist stay empty.Where to look
internal/parser/claude.go— the capture site (sessionKindnext toentrypoint;promptSourcenext toisSidechain) and thecompactClaudeEntryretained-field whitelist.internal/parser/claude_provider.go,provider.go,internal/sync/engine.go— incremental identity threading forsessionKind.internal/db/{schema.sql,db.go,sessions.go,messages.go}— SQLite columns, migrations, and read/write plumbing.internal/postgres/*andinternal/duckdb/*— the mirrored columns and fingerprints.The source of truth for both fields is the Claude Code transcript format, where
sessionKindandpromptSourceappear as top-level keys on session JSONL entries.