Repository navigation
fix: fill Claude's unified columns from the transcript; keep forked Codex rollouts' identity - #5
Merged
Merged
Conversation
…odex rollouts' identity Claude transcripts carry the data for most unified columns that read_conversations left NULL. Per record: response_id (message.id, else requestId), reasoning_effort (perTurnEffort, else effort), reasoning_tokens (usage.output_tokens_details.thinking_tokens), event_type (record type; a system record's subtype; an attachment's kind), parent_uuid falling back to logicalParentUuid / leafUuid, usage_scope/usage_source_line, record_id, model_provider, originator, metadata (session facts + unmapped record fields). Per turn: turn_id (promptId) and thread_source (turnOrigin / origin.kind). Per session: agent-name, agent-setting, pr-link, bridge-session and continued-in records apply to every row of the session (agent_nickname, agent_role, repository, channel, forked_from_session_id); session span; cwd/slug/git_branch/version/client carried onto session-state rows. Subagents take agent_role/agent_nickname from agent-*.meta.json and their root_turn_id from the parent turn that issued the spawning tool call. Reader defects fixed on the way: a thinking-only line is a thinking row (redacted thinking keeps usage with NULL content) instead of an empty assistant row; attachment rows no longer put their kind in tool_name (role system; tool_name only when the attachment cites a real tool call); empty system content is NULL. Codex: a forked/spawned rollout replays its parent's session_meta, which overwrote the child's id and fork pointer; the first session_meta now keeps the thread's identity. channel falls back to `phase`. Tests: test/sql/unified_columns.test (fixtures test/data_claude_unified, test/data_codex_unified) with a COLUMNS(*) whole-schema blank check per source; test/mapping/claude_raw_mapping.sql compares every mapped column with its raw-derived value via duck_diff and inventories raw fields via finetype, run as its own CI job with the DuckDB CLI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mapping check Queued prompts interleave, so a promptId can resume after another turn. thread_source now keeps the origin that turn already had, matching the per-turn derivation in the duck_diff check, which is green on all 595,276 rows of a real Claude home. The mapping check reports differing columns with examples instead of 20 raw rows, and ft_unpack runs once per row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
read_conversationsleft about 20 unified columns entirely NULL for Claude, even though the transcripts carry the data. It also had several reader defects that the same coverage measurement exposed. This PR fills those columns and fixes the defects. One Codex bug is fixed too: a forked rollout was relabelled with its parent's session id.Column → source (Claude)
message.id(msg_…), elserequestIdperTurnEffort, elseeffortmessage.usage.output_tokens_details.thinking_tokens'response'/ line number, on rows that carry usagetype; asystemrecord'ssubtype; anattachment's kindparentUuid, elselogicalParentUuid(compaction boundary), elseleafUuid(last-prompt)claude:+ FNV-1a of session, project dir, file, line, sub-row'anthropic'entrypoint, same asclient(Codex derives client from originator)promptId, carried to every row of its turnmeta.jsontoolUseId)turnOrigin, elseorigin.kind, for the turnagent-namerecord; subagent:meta.jsonnameagent-settingrecord; subagent:meta.jsonagentType (Explore, general-purpose, …)pr-linkprRepository(all links in metadata.session.prLinks)bridge-sessionbridgeSessionIdcontinued-inrecord names this one{"session": titles, mode, permissionMode, sessionKind, prLinks, worktreeSession, subagent meta.json, …, "record": every top-level field not already in a column}Session-level records (agent-name, agent-setting, pr-link, bridge-session, continued-in, custom-title, ai-title, mode, permission-mode, worktree-state) have no uuid; NULL uuid on those rows is correct. Their facts are now applied to every row of the session, not only to the record itself.
Reader defects fixed
assistantrow: 59,973 rows on the author's data, from redacted (signature-only) thinking. When the thinking had text, the line produced an emptyassistantrow beside thethinkingrow. It is now onethinkingrow that keeps the line's usage, effort and response_id. Redacted thinking has NULL content.tool_name. Now the kind is inevent_typeandmessage_role = 'system'.tool_name/tool_use_idare set only when the attachment'stoolUseIDcites a realtool_usein the same session.event_type= subtype. The payload (hookInfos, durationMs, compactMetadata, …) is inmetadata.record. Empty content is NULL instead of''.Codex
session_metaas its second line. The reader adopted it, so the child's rows took the parent's session id and lostforked_from_session_id. The firstsession_metanow keeps the thread's identity, and a later one with a different id is treated as replay. On the author's data, forked_from_session_id goes from 100% NULL to filled for every forked thread.channelfalls back tophase(commentary/final_answer), the name current rollouts use.Columns that stay NULL, and why
git_commit: transcripts record only the branch, never a commit.archived: Claude has no archive state in its files.parse_error: NULL whenever the transcript is clean.parent_uuid: current rollouts write no parent item ids. The reader still readsparent_idwhen present.project_dir: this is Claude's encodedprojects/directory, and Codex has no equivalent.agent_role: the reader fills it fromthread_spawn.agent_role. Every local rollout writes it asnull, and the fixture proves the mapping.cache_creation_tokens: Codex usage reports only cached input, not cache creation.Tests
test/sql/unified_columns.testuses the new fixturestest/data_claude_unified(main session + subagent with meta.json + a continued-from session) andtest/data_codex_unified(spawned rollout with a replayed parent session_meta, phase, index). It asserts each mapped column on fixture lines that carry the field, plus:SUMMARIZE (SELECT nullif(trim(COLUMNS(*)::VARCHAR), '') …). No column may be entirely NULL/blank except the allowlist above.test/mapping/claude_raw_mapping.sql(new CI jobClaude raw-to-column coverage) runs the following checks:schema_diff,table_diff, andtable_diff_summarycompare the mapped columns with the same columns derived again fromraw_event(and the subagent meta.json) in SQL, keyed byfile_path, line_number, record_id. The run fails unlessn_total = n_identical.ft_unpack(raw_event)inventories the raw top-level fields. Any field that is neither read into a column nor kept inmetadata.recordfails the run.ft_profilechecks that the session span columns are the same ISO-8601 type astimestamp.COLUMNS(*)+ UNPIVOT, 0 < pct < 100) for review. There is also a failing check that no assistant row is an empty reply, and that no attachment names a tool outside its session's tool calls.These checks run as their own job with the DuckDB CLI on the linux_amd64 artifact. The sqllogictest suite runs on every distribution platform, where community builds of duck_diff and finetype cannot be relied on.
The full suite passes locally against the freshly built release binary: 37/37 sqllogictest files and 16 Rust unit tests. The runner loads
build/release/agent_data.duckdb_extensionvia--external-extension. Two existing expectations changed on purpose:claude_attachments.test(tool_name → event_type) andclaude_open_types.test(attachment role is system).The mapping check was also run once against the author's real Claude home (595,329 rows). Results:
schema_diffok on all 27 columns.table_diff_summaryn_identical = n_total = 595,329.The
ft_unpackinventory step does not finish at that scale. The CI job runs it on the fixture. Running it on a full home is not supported.Does not overlap #4 (modified_after).
🤖 Generated with Claude Code