feat(clickhouse): add llm_message table - #183
Conversation
4a282c5 to
16940b1
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
1f9b6db to
d26ef02
Compare
d26ef02 to
8821af2
Compare
|
guillaume-hexamind
left a comment
There was a problem hiding this comment.
One blocking issue, approve of the logic otherwise
|
Agreed the column belongs in the sort key — but not in the position suggested. After message_seq, occurred_at only separates retries of one event. Third is the position that pays: it matches the order the PR 6 endpoint returns rows in (occurred_at, message_seq), so the read is in-order rather than sorted — and when session_id is empty, where the prefix stops being selective, it's the only thing left pruning. It has to be first after session_id because anything grouping the rows ahead of it wins; a sub-agent's message_seq restarts at 0, so time is all that orders the lists against each other. ORDER BY (project_id, session_id, occurred_at, message_seq, event_id) turn_key leaves the key — with occurred_at ahead of it, it would only break ties event_id already resolves. It stays a column for grouping and gap detection. Also added run_id, mirroring llm_invocation, so a transcript is attributable to its run when session_id is empty. A column, not a key entry: before occurred_at it would split the session read into per-run blocks, and uuid4 wouldn't order runs chronologically anyway. Applied both files in a throwaway clickhouse local (image clickhouse/clickhouse-server:24.10): system.tables reports the sorting key above, and system.columns lists all 17 columns in the intended order. |
e6a3184 to
5d091b4
Compare
Storage for the hexgate.messages scope: one row per model call with the shared envelope, turn_key / message_seq / resynced / truncated, and the three gen_ai.* content columns as ZSTD-compressed JSON. Sorted by (project_id, session_id, turn_key, message_seq, event_id) because the read is "reconstruct this session's transcript in order"; same engine, partitioning and 180-day TTL as the sibling tables. Ships with the hand-applied migrations/0002_add_llm_message.sql (init/ never re-runs on a populated volume) and the §5.1 entry in the audit pipeline spec. Nothing writes to the table yet.
The migration header described the emitter PR's behaviour as present fact: it named an insert_llm_messages_batch that does not exist and claimed verify_all refuses to boot on a missing table, when verify_all is passed (verify_audit_schema, verify_llm_schema) only and never looks at llm_message. Skipping the migration today is inert, so say that and put the loud failure modes after the emitter PR is deployed. Also drop HEXGATE_LOG_MESSAGES from the schema comment and the doc — no such env var exists; capture is simply off until an emitter ships. Comments only; the two CREATE TABLE statements stay identical.
message_seq only counts within one turn_key and restarts at 0 for a sub-agent's or handoff's list, so wall-clock time is the only thing that orders a session's rows across its several lists. Move occurred_at to the first position after session_id, which is also the order the read endpoint returns rows in; MODIFY ORDER BY cannot add an existing column later, so this has to be right before the table ships. turn_key leaves the sort key: with occurred_at ahead of it a list's rows are no longer adjacent anyway, so there it would only break ties event_id already resolves. It stays a plain column for grouping and gap detection. Add run_id mirroring llm_invocation, so a transcript is attributable to its run even when session_id is empty. A column, not a key entry: ahead of occurred_at it would split the session read into per-run blocks, and uuid4 does not order runs chronologically.
5d091b4 to
043657c
Compare
What is changing
platform/clickhouse/init/schema.sql— new tablehexgate_audit.llm_message: the shared eight-column envelope, thenmodel,turn_key,message_seq,resynced,truncated, andinput_messages/output_messages/system_instructionsasString CODEC(ZSTD(3))holding the officialgen_ai.*JSON shapes.ReplacingMergeTree(received_at),PARTITION BY toYYYYMM(received_at),ORDER BY (project_id, session_id, turn_key, message_seq, event_id), 180-day TTL.platform/clickhouse/migrations/0003_add_llm_message.sql— the sameCREATE TABLE, byte-identical once comments are stripped, for hand application to volumes that already exist. Header follows0001: apply before the enricher that writes to the table is deployed, and what breaks if it is skipped.docs/internals/audit-pipeline.md§5.1 — the table, its sort key, and why it is a separate table.Nothing writes to or reads from the table yet; that is PR 4 and PR 6 of the stack.
Design: LLM message logging design · implementation spec.
Why is this change necessary
Message content is large, opt-in (nothing emits
hexgate.messagesyet) and read by session, so it does not belong as columns onllm_invocation, which is aggregated by user and model. The sort key is session-first with(turn_key, message_seq)next because the one read pattern is "reconstruct this session's transcript in order": a list's rows land adjacent and ordered, andevent_idlast keepsReplacingMergeTreededup to SDK retries of the same event.init/runs once on an empty volume, so the committed migration is how staging and production get the table.Tests
clickhouse local(imageclickhouse/clickhouse-server:24.10):init/schema.sqlfollowed by the migration, confirming the statements parse and the migration is idempotent against a volume that already ran init.system.columnsshows the 16 columns in the intended order;system.tablesshows the sort keyproject_id, session_id, turn_key, message_seq, event_idand partition keytoYYYYMM(received_at).CREATE TABLEstatements is empty.verify_schemathat exercise the table arrive with PR 4.🤖 Generated with Claude Code