ai-gateway: split session_id from conversation_id (schema v6) (#104)#117
Merged
Conversation
conversation_id was overloaded: Codex put a real thread there, while
Claude (no per-thread id) shoved the session id in — but a Claude session
holds the main loop, subagents, and side chats, so conversation_id was
really a container of many threads, confusing anyone analyzing the data.
Introduce session_id as the always-present partition key and make
conversation_id the (nullable) thread within it:
- session_id (partition key, non-null): Claude session / Codex
metadata.session_id (fallback to a hash / the thread so it's never null)
- conversation_id (nullable): Codex thread; null for Claude
- agent_id (nullable): Claude subagent; unchanged
- parent_thread_id (nullable): Codex parent thread; unchanged
Touches: schema columns + SCHEMA_VERSION 5->6; message_projector scopes
the prior-message chain + fallback hash on (conversation_id ?? session_id,
agent_id); claude/codex projectors and backfills set the columns per the
model; settle.js groups/loads transcripts by session_id; dataset partitions
by session_id (conversation_id rides along as a non-required identity
field). Context-graph consumers (graph contract, enrich config) key Session
nodes on session_id. Tests migrated to the new model.
BREAKING: moves the Iceberg partition key, so it needs a cache recreate +
backfill. Must NOT be bundled with additive schema changes (issue #102).
Documents the model in new LLP 0030; updates LLP 0026 (conversation_id is
no longer 'the session id for Claude') and LLP 0022 (partition key).
Fixes #104
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contributor
Author
Dual-agent review —
|
| Source | Finding (severity, evidence) | Intersects |
|---|---|---|
| Claude | major — smoke flows query conversation_id as session key (backfill_claude_fixture.js:164, walkthrough_backfill_client_history.js:188,209, context_graph_projects_rows.js:209) |
Direct callers (smoke-flow consumers of ai_gateway_messages), Risks |
| Codex | major — conversationStartedAt/toolCallLookupByConversation keyed on session_id cross Codex threads (message_projector.js:248,256; exchange-projector.js:70) |
Concurrency surface, Risks |
| Codex | minor — packaged agent/skill guidance still uses conversation_id (hypaware-analyst.md, hypaware-graph SKILL.md) |
Risks |
Codex review
Fix Validations
Overloaded conversation_id in row schema/provider projection
- Status: correct
- Evidence: hypaware-core/plugins-workspace/ai-gateway/src/message_projector.js:26, hypaware-core/plugins-workspace/claude/src/projector.js:240, hypaware-core/plugins-workspace/codex/src/exchange-projector.js:70
- Assessment: The core projection shape now has required
session_id, nullable/omitted Claudeconversation_id, and Codex emits both session and thread identities.
Fallback identity and previous-message chain scope
- Status: correct
- Evidence: hypaware-core/plugins-workspace/ai-gateway/src/message_projector.js:279
- Assessment: The fallback chain uses
conversation_id ?? session_id, preserving Claude’s old session scope and Codex’s thread scope.
Findings
1) Behavioral Correctness
- Severity: major
- Confidence: high
- Evidence: hypaware-core/plugins-workspace/codex/src/exchange-projector.js:70, hypaware-core/plugins-workspace/codex/src/exchange-projector.js:89, hypaware-core/plugins-workspace/ai-gateway/src/message_projector.js:248, hypaware-core/plugins-workspace/ai-gateway/src/message_projector.js:256, hypaware-core/plugins-workspace/ai-gateway/src/message_projector.js:494
- Why it matters: Codex can now emit one
session_idwith distinct threadconversation_ids, butconversation_started_atandtoolCallLookupByConversationare memoized bysession_id, so separate Codex threads in one session can inherit the first thread’s start time and cross-resolve tool-result names if tool call ids collide. - Suggested fix: Key those two maps with the same
threadScope = conversationId ?? sessionIdused for fallback identity/chain, and add a regression with two Codex projections sharingsession_idbut using differentconversation_ids, timestamps, and overlapping tool call ids.
11) Debuggability & Operability
- Severity: minor
- Confidence: high
- Evidence: hypaware-core/plugins-workspace/claude/agents/hypaware-analyst.md:39, hypaware-core/plugins-workspace/claude/agents/hypaware-analyst.md:40, hypaware-core/plugins-workspace/context-graph/skills/hypaware-graph/SKILL.md:26, hypaware-core/plugins-workspace/context-graph/skills/hypaware-graph/SKILL.md:28, hypaware-core/plugins-workspace/claude/src/projector.js:240
- Why it matters: Packaged agent/skill instructions still tell automated analysis to group/order graph sessions by
conversation_id, which is now null/omitted for Claude and no longer the graph Session key. - Suggested fix: Update bundled query/analyst/graph skill guidance to use
session_idas the session/grouping/natural key, withconversation_iddescribed only as nullable thread identity.
No Finding
- Contract & Interface Fidelity
- Change Impact / Blast Radius
- Concurrency, Ordering & State Safety
- Error Handling & Resilience
- Security Surface
- Resource Lifecycle & Cleanup
- Release Safety
- Test Evidence Quality
- Architectural Consistency
Evidence Bundle
- Changed hot paths: ai-gateway row projection; Codex exchange projection; Claude live/backfill projection; Claude settlement; ai-gateway dataset partitioning; graph contract/session anchoring.
- Impacted callers:
createAiGatewayMessageProjectorpersistent live state at hypaware-core/plugins-workspace/ai-gateway/src/message_projector.js:151; Codex projector at hypaware-core/plugins-workspace/codex/src/exchange-projector.js:89; Claude projector at hypaware-core/plugins-workspace/claude/src/projector.js:240. - Impacted tests: test/plugins/ai-gateway-message-projector.test.js:406; test/plugins/claude-settlement.test.js:31; test/plugins/context-graph-enrich-propose.test.js:30; test/plugins/context-graph-project-e2e.test.js:25.
- Unresolved uncertainty: I did not run
npm test; review is based on the provided diff plus targeted caller/contract traces.
Claude review
Claude review
Hermetic smoke flows still query/seed conversation_id as the Claude session key — three will fail
- Severity: major
- Confidence: 88
- Evidence: hypaware-core/smoke/flows/backfill_claude_fixture.js:164, hypaware-core/smoke/flows/walkthrough_backfill_client_history.js:188, hypaware-core/smoke/flows/walkthrough_backfill_client_history.js:209-211, hypaware-core/smoke/flows/context_graph_projects_rows.js:209
- Why it matters: The PR moves the Claude session id out of
conversation_id(now null for Claude) into the newsession_idcolumn, and repoints the context-graph Session anchor tosession_id. The traditionaltest/**suite was migrated (and passes 1148/0), but the hermetic smoke tier — a distinct release gate per CLAUDE.md "Smoke Test Model", not run bynpm test— was not. Concretely:backfill_claude_fixturefilterswhere conversation_id = '${sessionId}', which now matches zero Claude rows, failing its "two rows for the backfilled session" assertion;walkthrough_backfill_client_historyfilterswhere conversation_id in ('${claudeSession}', ...)androws.filter(r => r.conversation_id === claudeSession), so its "claude session produced two rows" assertion fails; andcontext_graph_projects_rowsseeds a fixture row withconversation_id: 'conv-1'and nosession_id, so the real ai-gateway-graph Session node/edges (now keyed onsession_id) get a null key and the asserted "7 nodes / 6 edges, Session === 1" projection collapses.gateway_claude_capture.js:261(order by conversation_id) is stale but non-fatal since its row assertions key onmessage_id/role. - Suggested fix: Migrate the three Claude-consuming smoke flows to the new model — query/filter on
session_idfor Claude (backfill_claude_fixture,walkthrough_backfill_client_history), and addsession_idto thecontext_graph_projects_rowsfixture row (line 209) so the Session anchor resolves. Runnpm run smoke -- backfill_claude_fixture,walkthrough_backfill_client_history, andcontext_graph_projects_rowsto confirm green, sincenpm testdoes not cover this tier.
Reports: /Users/phil/workspace/hypaware/.git/worktrees/dual-review-pr-117/dual-review/pr-117
…umers Review fixes for the schema v6 session_id split: - Key conversationStartedAt and toolCallLookupByConversation by threadScope (conversation_id ?? session_id), the same scope used for the prior-message chain and fallback identity. A Codex session_id can carry several thread conversation_ids; keying these maps by session_id let a later thread inherit the first thread's start time and cross-resolve tool-result names on colliding tool_call ids. Adds a regression with two Codex threads sharing a session_id. - Migrate the hermetic smoke flows off conversation_id as the Claude session key: query/filter on session_id (null conversation_id for Claude), seed session_id in the context-graph fixture, and update the stale order-by. (Two of these smokes also hit a pre-existing icebird partition-pruning limitation for >16-char string partition values, present on master and unrelated to this change.) - Update bundled analyst/graph/query skill guidance to treat session_id as the session/grouping/natural key and conversation_id as a nullable thread identity. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts: # test/plugins/ai-gateway-message-projector.test.js
Re-scope the restart-replay seen-set seeding (issue #108) onto session_id (the LLP 0030 partition key) — Claude conversation_id is null, so the prior conversation-scoped seed never deduped. Update the re-settle (#105), aux graph-contract / context-graph (#106), and restart-replay (#108) test fixtures to the v6 schema (non-null session_id partition key).
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.
Problem
conversation_idwas overloaded and confused both humans and LLMs analyzingai_gateway_messages. Codex puts a real thread there; Claude has no conversation id, so we shoved the session id in — but a Claude session holds the main loop plus N subagents, side chats, title/recap calls, etc. Soconversation_idwas really a container of many threads, and anyone scoping a conversation silently mixed them.New model
session_id(partition key, non-null)metadata.session_idconversation_id(nullable)agent_id(nullable)parent_thread_id(nullable)Touches
message_projector.js): addsession_id(non-null), makeconversation_idnullable,SCHEMA_VERSION5→6.(conversation_id ?? session_id, agent_id)— Claude scopes by session (conversation_id null), Codex by its thread. Behaviour is unchanged from before the split, since Claude's old conversation_id was the session id.session_id(with a non-null fallback so the partition key is never null),conversation_id= null.session_id=metadata.session_id(fallback to thread), keepconversation_id= thread andparent_thread_id.settle.js: group/load transcripts bysession_id(Claudeconversation_idis now null).dataset.js: partition bysession_id(required identity field);conversation_idrides along as a non-required identity field; partition label bumped; legacy partitions still discovered.session_id.BREAKING
Moves the Iceberg partition key, so it needs a cache recreate + backfill. Must not be bundled with additive schema changes (issue #102 — those evolve in place; this can't).
LLP
New LLP 0030 documents the model and the breaking boundary; LLP 0026 updated (
conversation_idis no longer "the session id for Claude"); LLP 0022 updated for the new partition key. All@refs verified to resolve.Tests
Migrated the affected suites (ai-gateway dataset/materializer/projector, claude/codex backfill, settlement, graph contract, context-graph project/propose) to the new model.
npm test: 1148 pass / 0 fail / 1 skip.Fixes #104
🤖 Generated with Claude Code