@gobing-ai/ts-llm-jsonl-importer 0.4.37 → 0.4.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"mappers.d.ts","sourceRoot":"","sources":["../src/mappers.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"mappers.d.ts","sourceRoot":"","sources":["../src/mappers.ts"],"names":[],"mappings":"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"}
package/dist/mappers.js CHANGED
@@ -125,10 +125,10 @@ export function claudeSplit(raw) {
125
125
  const entries = [];
126
126
  const sessionId = s(raw.sessionId, raw.conversation_uuid) ?? 'unknown';
127
127
  const seq = typeof raw.seq === 'number' ? raw.seq : typeof raw.messageIndex === 'number' ? raw.messageIndex : 0;
128
- const ts = s(raw.ts, raw.timestamp, raw.createdAt) ?? new Date(0).toISOString();
128
+ const ts = timestampOf(raw.ts, raw.timestamp, raw.createdAt);
129
129
  const role = mapRole(raw.type ?? raw.role);
130
130
  const recordType = String(raw.type ?? '');
131
- const model = s(raw.model);
131
+ const model = s(raw.model, o(raw.message).model);
132
132
  const cwd = s(raw.cwd, raw.dir);
133
133
  // Check for system/turn-duration records
134
134
  if (raw.subtype === 'turn_duration' && typeof raw.durationMs === 'number') {
@@ -165,13 +165,13 @@ export function claudeSplit(raw) {
165
165
  }
166
166
  // Main message
167
167
  const hasUsage = typeof raw.usage === 'object' && raw.usage !== null;
168
- const usage = hasUsage ? raw.usage : undefined;
169
- const inputTokens = typeof raw.inputTokens === 'number' ? raw.inputTokens : undefined;
170
- const outputTokens = typeof raw.outputTokens === 'number' ? raw.outputTokens : undefined;
171
- const cacheRead = (usage?.cacheReadTokens ?? usage?.cache_read_tokens ?? undefined);
172
- const cacheWrite = (usage?.cacheWriteTokens ?? usage?.cache_write_tokens ?? undefined);
168
+ const usage = (hasUsage ? raw.usage : o(raw.message).usage);
169
+ const inputTokens = (raw.inputTokens ?? usage?.input_tokens);
170
+ const outputTokens = (raw.outputTokens ?? usage?.output_tokens);
171
+ const cacheRead = (usage?.cacheReadTokens ?? usage?.cache_read_tokens ?? usage?.cache_read_input_tokens);
172
+ const cacheWrite = (usage?.cacheWriteTokens ?? usage?.cache_write_tokens ?? usage?.cache_creation_input_tokens);
173
173
  const costUsd = computeCost(inputTokens, outputTokens, model);
174
- const contentBlocks = raw.content;
174
+ const contentBlocks = raw.content ?? o(raw.message).content;
175
175
  const contentText = extractContentText(contentBlocks);
176
176
  const messageSplitIndex = entries.length;
177
177
  entries.push({
@@ -238,10 +238,9 @@ export function piSplit(raw, context) {
238
238
  const recordType = String(raw.type ?? raw.recordType ?? '');
239
239
  const sessionId = sessionIdFromContext(context, raw);
240
240
  const seq = context?.sourceLine ?? (typeof raw.seq === 'number' ? raw.seq : 0);
241
- // Prefer the ISO `timestamp`; a numeric epoch-ms `ts` is converted, never stringified.
242
- const ts = s(raw.timestamp, raw.createdAt) ??
243
- (typeof raw.ts === 'number' && Number.isFinite(raw.ts) ? new Date(raw.ts).toISOString() : s(raw.ts)) ??
244
- new Date(0).toISOString();
241
+ // Prefer the ISO `timestamp`; numeric epoch values are converted (task 0580 R6), never
242
+ // stringified, and a missing ts persists as null rather than a 1970 sentinel (D4).
243
+ const ts = timestampOf(raw.timestamp, raw.createdAt, raw.ts);
245
244
  // Meta lifecycle/custom records collapse to one meta row keyed by the source session,
246
245
  // never the unique event id, and never a guessed role.
247
246
  if (recordType === 'title' ||
@@ -366,7 +365,7 @@ export function ompSplit(raw, context) {
366
365
  const recordType = String(raw.type ?? '');
367
366
  const sessionId = sessionIdFromContext(context, raw);
368
367
  const seq = context?.sourceLine ?? (typeof raw.seq === 'number' ? raw.seq : 0);
369
- const ts = s(raw.ts, raw.timestamp, raw.createdAt) ?? new Date(0).toISOString();
368
+ const ts = timestampOf(raw.ts, raw.timestamp, raw.createdAt);
370
369
  // Meta lifecycle/custom records — including the current `custom.*` events and non-message
371
370
  // lifecycle types — collapse to one meta row keyed by the source session, never the unique
372
371
  // event id, and never a guessed role.
@@ -520,8 +519,9 @@ export function codexSplit(raw) {
520
519
  const payload = (raw.payload ?? raw);
521
520
  const sessionId = s(raw.session_id, o(raw.session_meta).id, raw.id) ?? 'unknown';
522
521
  const seq = typeof raw.seq === 'number' ? raw.seq : 0;
523
- const ts = s(raw.timestamp, raw.ts, payload.ts) ?? new Date(0).toISOString();
522
+ const ts = timestampOf(raw.timestamp, raw.ts, payload.ts);
524
523
  const recordType = String(raw.type ?? '');
524
+ const payloadType = String(payload.type ?? '');
525
525
  // Check for older short format
526
526
  if (recordType === '' && raw.id && raw.timestamp && raw.instructions) {
527
527
  return [
@@ -539,18 +539,77 @@ export function codexSplit(raw) {
539
539
  },
540
540
  ];
541
541
  }
542
- const role = mapRole(recordType);
542
+ // Codex JSONL is a transport envelope: the top-level `type` (response_item / event_msg /
543
+ // turn_context / ...) is bookkeeping, never a role. Real roles live in
544
+ // `payload.role` for response_item message items (task 0580 D1: passing recordType to
545
+ // mapRole leaked 153k response_item / 91k event_msg rows into the role column).
546
+ let role;
547
+ let disposition;
548
+ let contentText;
549
+ let usage;
550
+ if (recordType === 'response_item') {
551
+ if (payloadType === 'message') {
552
+ role = mapRole(payload.role);
553
+ disposition = 'keep';
554
+ contentText = extractContentText(payload.content) ?? s(payload.text) ?? null;
555
+ }
556
+ else if (payloadType === 'agent_message') {
557
+ role = 'assistant';
558
+ disposition = 'keep';
559
+ contentText = extractContentText(payload.content) ?? s(payload.text, payload.message) ?? null;
560
+ }
561
+ else if (payloadType === 'reasoning') {
562
+ role = 'assistant';
563
+ disposition = 'keep';
564
+ contentText = extractContentText(payload.summary) ?? s(payload.text) ?? null;
565
+ }
566
+ else if (payloadType === 'function_call' || payloadType === 'custom_tool_call') {
567
+ role = 'tool';
568
+ disposition = 'keep';
569
+ contentText = null;
570
+ }
571
+ else {
572
+ role = 'meta';
573
+ disposition = 'meta';
574
+ contentText = s(payload.text) ?? null;
575
+ }
576
+ }
577
+ else if (recordType === 'user' || recordType === 'assistant') {
578
+ // Legacy (pre-rollout) envelope: top-level type carried the role directly.
579
+ role = mapRole(recordType);
580
+ disposition = 'keep';
581
+ contentText = extractContentText(payload.content) ?? s(raw.content, payload.text) ?? null;
582
+ }
583
+ else if (recordType === 'message') {
584
+ role = mapRole(raw.role ?? payload.role);
585
+ disposition = 'keep';
586
+ contentText = extractContentText(payload.content ?? raw.content) ?? s(payload.text, raw.text) ?? null;
587
+ }
588
+ else if (recordType === 'event_msg') {
589
+ role = 'meta';
590
+ disposition = 'meta';
591
+ contentText = s(payload.message, payload.text) ?? null;
592
+ // Per-turn usage lives on token_count transport events (task 0580 R2): real files
593
+ // carry `payload.info.last_token_usage`; the legacy `payload.token_count` path is kept.
594
+ usage = o(o(payload.info).last_token_usage);
595
+ }
596
+ else {
597
+ role = 'meta';
598
+ disposition = 'meta';
599
+ contentText = s(payload.text, raw.instructions) ?? null;
600
+ }
543
601
  const model = s(raw.model, payload.model, o(o(raw.turn_context).payload).model);
544
602
  const cwd = s(raw.cwd, raw.dir);
545
603
  const tokenCount = payload.token_count;
546
- const inputTokens = (tokenCount?.input ?? tokenCount?.input_tokens ?? undefined);
547
- const outputTokens = (tokenCount?.output ?? tokenCount?.output_tokens ?? undefined);
604
+ const usageInput = (usage?.input_tokens ?? tokenCount?.input ?? tokenCount?.input_tokens ?? undefined);
605
+ const usageOutput = (usage?.output_tokens ?? tokenCount?.output ?? tokenCount?.output_tokens ?? undefined);
606
+ const cacheRead = (usage?.cached_input_tokens ?? undefined);
548
607
  const turnCtx = raw.turn_context;
549
608
  const turnPayload = turnCtx?.payload;
550
609
  const turnInputTokens = turnPayload?.input_tokens;
551
610
  const turnOutputTokens = turnPayload?.output_tokens;
552
- const finalInput = inputTokens !== undefined ? inputTokens : turnInputTokens;
553
- const finalOutput = outputTokens !== undefined ? outputTokens : turnOutputTokens;
611
+ const finalInput = usageInput !== undefined ? usageInput : turnInputTokens;
612
+ const finalOutput = usageOutput !== undefined ? usageOutput : turnOutputTokens;
554
613
  const costUsd = computeCost(finalInput, finalOutput, model);
555
614
  const messageSplitIndex = entries.length;
556
615
  entries.push({
@@ -560,47 +619,56 @@ export function codexSplit(raw) {
560
619
  seq,
561
620
  role,
562
621
  record_type: recordType,
563
- disposition: 'keep',
622
+ disposition,
564
623
  ts,
565
624
  duration_ms: undefined,
566
625
  model: model ?? null,
567
626
  input_tokens: finalInput ?? null,
568
627
  output_tokens: finalOutput ?? null,
569
- cache_read_tokens: null,
628
+ cache_read_tokens: cacheRead ?? null,
570
629
  cache_write_tokens: null,
571
630
  cost_usd: costUsd ?? null,
572
- content_text: s(payload.content, payload.text, raw.content, raw.text) ?? null,
631
+ content_text: contentText,
573
632
  cwd: cwd ?? null,
574
633
  provenance: 'ambient',
575
634
  },
576
635
  });
577
- // Tool calls: function_call in response_item
578
- const responseItems = payload.response_item
579
- ? [payload.response_item]
580
- : Array.isArray(payload.response_items)
581
- ? payload.response_items
582
- : [];
583
- for (const item of responseItems) {
584
- if (item.function_call) {
585
- const fc = item.function_call;
586
- entries.push({
587
- targetTable: 'history_tool_call',
588
- record: {
589
- _messageSplitIndex: messageSplitIndex,
590
- session_id: sessionId,
591
- seq,
592
- tool_name: String(fc.name ?? ''),
593
- args_digest: argsDigest(fc.arguments),
594
- args_raw: maybeArgsRaw('codex', String(fc.name ?? ''), fc.arguments),
595
- status: 'ok',
596
- started_at: undefined,
597
- completed_at: undefined,
598
- duration_ms: undefined,
599
- result_bytes: undefined,
600
- error_text: undefined,
601
- },
602
- });
603
- }
636
+ // Tool calls. Live rollouts emit each call as its own response_item whose payload IS the
637
+ // item (`payload.type === "function_call" | "custom_tool_call"`, name/call_id/arguments or
638
+ // input at payload level); the legacy nested `payload.response_item[_s]` shape is kept.
639
+ const toolItems = [];
640
+ if ((recordType === 'response_item' || recordType === 'function_call' || recordType === 'custom_tool_call') &&
641
+ (payloadType === 'function_call' || payloadType === 'custom_tool_call')) {
642
+ toolItems.push(payload);
643
+ }
644
+ else if (payload.response_item) {
645
+ toolItems.push(payload.response_item);
646
+ }
647
+ else if (Array.isArray(payload.response_items)) {
648
+ toolItems.push(...payload.response_items);
649
+ }
650
+ for (const item of toolItems) {
651
+ const fc = item.function_call ?? item;
652
+ const toolName = s(fc.name) ?? '';
653
+ const args = fc.arguments ?? fc.input;
654
+ entries.push({
655
+ targetTable: 'history_tool_call',
656
+ record: {
657
+ _messageSplitIndex: messageSplitIndex,
658
+ session_id: sessionId,
659
+ seq,
660
+ tool_name: toolName,
661
+ call_id: s(fc.call_id),
662
+ args_digest: argsDigest(args),
663
+ args_raw: maybeArgsRaw('codex', toolName, args),
664
+ status: 'ok',
665
+ started_at: undefined,
666
+ completed_at: undefined,
667
+ duration_ms: undefined,
668
+ result_bytes: undefined,
669
+ error_text: undefined,
670
+ },
671
+ });
604
672
  }
605
673
  return entries;
606
674
  }
@@ -613,7 +681,7 @@ export function agySplit(raw) {
613
681
  const recordType = String(raw.type ?? '');
614
682
  const sessionId = s(raw.session_id, raw.conversation_id) ?? 'unknown';
615
683
  const seq = typeof raw.seq === 'number' ? raw.seq : typeof raw.step_index === 'number' ? raw.step_index : 0;
616
- const ts = s(raw.created_at, raw.timestamp, raw.ts) ?? new Date(0).toISOString();
684
+ const ts = timestampOf(raw.created_at, raw.timestamp, raw.ts);
617
685
  let role;
618
686
  let disposition;
619
687
  let contentText = null;
@@ -726,7 +794,7 @@ export function geminiSplit(raw, context) {
726
794
  const state = o(raw.$set);
727
795
  const sessionId = sessionIdFromContext(context, raw);
728
796
  const seq = context?.sourceLine ?? 0;
729
- const ts = s(raw.timestamp, raw.startTime, raw.lastUpdated, state.lastUpdated) ?? new Date(0).toISOString();
797
+ const ts = timestampOf(raw.timestamp, raw.startTime, raw.lastUpdated, state.lastUpdated);
730
798
  if (recordType === 'session' || recordType === 'state') {
731
799
  return [
732
800
  {
@@ -861,7 +929,12 @@ function normalizeGrokRecord(raw) {
861
929
  : undefined));
862
930
  const usage = (body.usage ?? raw.usage);
863
931
  const toolMeta = meta?.['x.ai/tool'];
864
- const toolName = s(body.title, body.tool_name, body.name, toolMeta?.name, toolMeta?.label, raw.tool_name, raw.name) ?? '';
932
+ // The tool's real name is `_meta["x.ai/tool"].name` (or `kind`); `title` is a human label
933
+ // like "Read `/path`" — used only as a last resort, stripped at the first backtick so the
934
+ // argument payload never pollutes tool_name (task 0580 D3).
935
+ const titleFallback = s(body.title)?.split('`')[0]?.trim();
936
+ const toolName = s(toolMeta?.name, body.kind, body.tool_name, body.name, raw.tool_name, raw.name) ??
937
+ (titleFallback !== undefined && titleFallback.length > 0 && titleFallback.length <= 40 ? titleFallback : '');
865
938
  const toolArgs = body.rawInput ?? body.input ?? body.arguments ?? toolMeta?.input ?? raw.input ?? raw.arguments;
866
939
  const toolStatus = s(body.status, raw.outcome, raw.status) ?? 'ok';
867
940
  const durationMs = typeof body.duration_ms === 'number'
@@ -891,20 +964,7 @@ function normalizeGrokRecord(raw) {
891
964
  };
892
965
  }
893
966
  function normalizeTs(value) {
894
- if (typeof value === 'number' && Number.isFinite(value)) {
895
- // Grok timestamps are unix seconds (sometimes with fractional).
896
- const ms = value > 1e12 ? value : value * 1000;
897
- return new Date(ms).toISOString();
898
- }
899
- if (typeof value === 'string' && value.length > 0) {
900
- const asNum = Number(value);
901
- if (Number.isFinite(asNum) && /^\d+(\.\d+)?$/.test(value.trim())) {
902
- const ms = asNum > 1e12 ? asNum : asNum * 1000;
903
- return new Date(ms).toISOString();
904
- }
905
- return value;
906
- }
907
- return new Date(0).toISOString();
967
+ return timestampOf(value);
908
968
  }
909
969
  function extractGrokContent(content) {
910
970
  if (typeof content === 'string')
@@ -1268,9 +1328,31 @@ function s(...values) {
1268
1328
  }
1269
1329
  return undefined;
1270
1330
  }
1331
+ /**
1332
+ * First usable timestamp: an ISO-ish string passes through, a finite number (or digit-string)
1333
+ * is converted from epoch seconds/millis (>1e12 treated as ms). Returns undefined when nothing
1334
+ * usable exists — mappers must persist null ts rather than a 1970 sentinel (task 0580 D4).
1335
+ */
1336
+ function timestampOf(...values) {
1337
+ for (const value of values) {
1338
+ if (typeof value === 'number' && Number.isFinite(value)) {
1339
+ const ms = value > 1e12 ? value : value * 1000;
1340
+ return new Date(ms).toISOString();
1341
+ }
1342
+ if (typeof value === 'string' && value.length > 0) {
1343
+ const asNum = Number(value);
1344
+ if (Number.isFinite(asNum) && /^\d+(\.\d+)?$/.test(value.trim())) {
1345
+ const ms = asNum > 1e12 ? asNum : asNum * 1000;
1346
+ return new Date(ms).toISOString();
1347
+ }
1348
+ return value;
1349
+ }
1350
+ }
1351
+ return undefined;
1352
+ }
1271
1353
  function mapRole(type) {
1272
1354
  const t = String(type ?? '').toLowerCase();
1273
- if (t === 'user' || t === 'human')
1355
+ if (t === 'user' || t === 'human' || t === 'developer')
1274
1356
  return 'user';
1275
1357
  if (t === 'assistant' || t === 'ai' || t === 'model')
1276
1358
  return 'assistant';
@@ -1299,7 +1381,9 @@ function extractContentText(content) {
1299
1381
  if (typeof block !== 'object' || block === null)
1300
1382
  continue;
1301
1383
  const b = block;
1302
- if (b.type === 'text' && typeof b.text === 'string') {
1384
+ // Claude emits {type:"text",text}; codex emits {type:"input_text"|"output_text"|
1385
+ // "summary_text",text} — any block carrying a string `text` is text content.
1386
+ if (typeof b.text === 'string') {
1303
1387
  parts.push(b.text);
1304
1388
  }
1305
1389
  }
@@ -37,7 +37,7 @@ CREATE TABLE IF NOT EXISTS history_message (
37
37
  role TEXT NOT NULL,
38
38
  record_type TEXT NOT NULL,
39
39
  disposition TEXT NOT NULL,
40
- ts TEXT NOT NULL,
40
+ ts TEXT,
41
41
  duration_ms INTEGER,
42
42
  model TEXT,
43
43
  input_tokens INTEGER,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/ts-llm-jsonl-importer",
3
- "version": "0.4.37",
3
+ "version": "0.4.38",
4
4
  "description": "@gobing-ai/ts-llm-jsonl-importer — Generic JSONL importer for LLM agent history files.",
5
5
  "keywords": [
6
6
  "typescript",
@@ -47,9 +47,9 @@
47
47
  "release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-llm-jsonl-importer-v<version> && git push --tags' && exit 1"
48
48
  },
49
49
  "dependencies": {
50
- "@gobing-ai/ts-db": "^0.4.37",
51
- "@gobing-ai/ts-runtime": "^0.4.37",
52
- "@gobing-ai/ts-utils": "^0.4.37",
50
+ "@gobing-ai/ts-db": "^0.4.38",
51
+ "@gobing-ai/ts-runtime": "^0.4.38",
52
+ "@gobing-ai/ts-utils": "^0.4.38",
53
53
  "zod": "^4.1.0"
54
54
  },
55
55
  "devDependencies": {
package/src/mappers.ts CHANGED
@@ -132,10 +132,10 @@ export function claudeSplit(raw: Record<string, unknown>): readonly SplitEntry[]
132
132
  const entries: SplitEntry[] = [];
133
133
  const sessionId = s(raw.sessionId, raw.conversation_uuid) ?? 'unknown';
134
134
  const seq = typeof raw.seq === 'number' ? raw.seq : typeof raw.messageIndex === 'number' ? raw.messageIndex : 0;
135
- const ts = s(raw.ts, raw.timestamp, raw.createdAt) ?? new Date(0).toISOString();
135
+ const ts = timestampOf(raw.ts, raw.timestamp, raw.createdAt);
136
136
  const role = mapRole(raw.type ?? raw.role);
137
137
  const recordType = String(raw.type ?? '');
138
- const model = s(raw.model);
138
+ const model = s(raw.model, o(raw.message).model);
139
139
  const cwd = s(raw.cwd, raw.dir);
140
140
 
141
141
  // Check for system/turn-duration records
@@ -174,14 +174,17 @@ export function claudeSplit(raw: Record<string, unknown>): readonly SplitEntry[]
174
174
 
175
175
  // Main message
176
176
  const hasUsage = typeof raw.usage === 'object' && raw.usage !== null;
177
- const usage = hasUsage ? (raw.usage as Record<string, unknown>) : undefined;
178
- const inputTokens = typeof raw.inputTokens === 'number' ? raw.inputTokens : undefined;
179
- const outputTokens = typeof raw.outputTokens === 'number' ? raw.outputTokens : undefined;
180
- const cacheRead = (usage?.cacheReadTokens ?? usage?.cache_read_tokens ?? undefined) as number | undefined;
181
- const cacheWrite = (usage?.cacheWriteTokens ?? usage?.cache_write_tokens ?? undefined) as number | undefined;
177
+ const usage = (hasUsage ? raw.usage : o(raw.message).usage) as Record<string, unknown> | undefined;
178
+ const inputTokens = ((raw.inputTokens as number | undefined) ?? usage?.input_tokens) as number | undefined;
179
+ const outputTokens = ((raw.outputTokens as number | undefined) ?? usage?.output_tokens) as number | undefined;
180
+ const cacheRead = (usage?.cacheReadTokens ?? usage?.cache_read_tokens ?? usage?.cache_read_input_tokens) as
181
+ | number
182
+ | undefined;
183
+ const cacheWrite = (usage?.cacheWriteTokens ?? usage?.cache_write_tokens ?? usage?.cache_creation_input_tokens) as
184
+ | number
185
+ | undefined;
182
186
  const costUsd = computeCost(inputTokens, outputTokens, model);
183
-
184
- const contentBlocks = raw.content;
187
+ const contentBlocks = raw.content ?? o(raw.message).content;
185
188
  const contentText = extractContentText(contentBlocks);
186
189
 
187
190
  const messageSplitIndex = entries.length;
@@ -253,11 +256,9 @@ export function piSplit(raw: Record<string, unknown>, context?: TransformContext
253
256
  const recordType = String(raw.type ?? raw.recordType ?? '');
254
257
  const sessionId = sessionIdFromContext(context, raw);
255
258
  const seq = context?.sourceLine ?? (typeof raw.seq === 'number' ? raw.seq : 0);
256
- // Prefer the ISO `timestamp`; a numeric epoch-ms `ts` is converted, never stringified.
257
- const ts =
258
- s(raw.timestamp, raw.createdAt) ??
259
- (typeof raw.ts === 'number' && Number.isFinite(raw.ts) ? new Date(raw.ts).toISOString() : s(raw.ts)) ??
260
- new Date(0).toISOString();
259
+ // Prefer the ISO `timestamp`; numeric epoch values are converted (task 0580 R6), never
260
+ // stringified, and a missing ts persists as null rather than a 1970 sentinel (D4).
261
+ const ts = timestampOf(raw.timestamp, raw.createdAt, raw.ts);
261
262
 
262
263
  // Meta lifecycle/custom records collapse to one meta row keyed by the source session,
263
264
  // never the unique event id, and never a guessed role.
@@ -390,7 +391,7 @@ export function ompSplit(raw: Record<string, unknown>, context?: TransformContex
390
391
  const recordType = String(raw.type ?? '');
391
392
  const sessionId = sessionIdFromContext(context, raw);
392
393
  const seq = context?.sourceLine ?? (typeof raw.seq === 'number' ? raw.seq : 0);
393
- const ts = s(raw.ts, raw.timestamp, raw.createdAt) ?? new Date(0).toISOString();
394
+ const ts = timestampOf(raw.ts, raw.timestamp, raw.createdAt);
394
395
 
395
396
  // Meta lifecycle/custom records — including the current `custom.*` events and non-message
396
397
  // lifecycle types — collapse to one meta row keyed by the source session, never the unique
@@ -564,8 +565,9 @@ export function codexSplit(raw: Record<string, unknown>): readonly SplitEntry[]
564
565
  const payload = (raw.payload ?? raw) as Record<string, unknown>;
565
566
  const sessionId = s(raw.session_id, o(raw.session_meta).id, raw.id) ?? 'unknown';
566
567
  const seq = typeof raw.seq === 'number' ? raw.seq : 0;
567
- const ts = s(raw.timestamp, raw.ts, payload.ts) ?? new Date(0).toISOString();
568
+ const ts = timestampOf(raw.timestamp, raw.ts, payload.ts);
568
569
  const recordType = String(raw.type ?? '');
570
+ const payloadType = String(payload.type ?? '');
569
571
 
570
572
  // Check for older short format
571
573
  if (recordType === '' && raw.id && raw.timestamp && raw.instructions) {
@@ -585,21 +587,77 @@ export function codexSplit(raw: Record<string, unknown>): readonly SplitEntry[]
585
587
  ];
586
588
  }
587
589
 
588
- const role = mapRole(recordType);
590
+ // Codex JSONL is a transport envelope: the top-level `type` (response_item / event_msg /
591
+ // turn_context / ...) is bookkeeping, never a role. Real roles live in
592
+ // `payload.role` for response_item message items (task 0580 D1: passing recordType to
593
+ // mapRole leaked 153k response_item / 91k event_msg rows into the role column).
594
+ let role: string;
595
+ let disposition: string;
596
+ let contentText: string | null;
597
+ let usage: Record<string, unknown> | undefined;
598
+ if (recordType === 'response_item') {
599
+ if (payloadType === 'message') {
600
+ role = mapRole(payload.role);
601
+ disposition = 'keep';
602
+ contentText = extractContentText(payload.content) ?? s(payload.text) ?? null;
603
+ } else if (payloadType === 'agent_message') {
604
+ role = 'assistant';
605
+ disposition = 'keep';
606
+ contentText = extractContentText(payload.content) ?? s(payload.text, payload.message) ?? null;
607
+ } else if (payloadType === 'reasoning') {
608
+ role = 'assistant';
609
+ disposition = 'keep';
610
+ contentText = extractContentText(payload.summary) ?? s(payload.text) ?? null;
611
+ } else if (payloadType === 'function_call' || payloadType === 'custom_tool_call') {
612
+ role = 'tool';
613
+ disposition = 'keep';
614
+ contentText = null;
615
+ } else {
616
+ role = 'meta';
617
+ disposition = 'meta';
618
+ contentText = s(payload.text) ?? null;
619
+ }
620
+ } else if (recordType === 'user' || recordType === 'assistant') {
621
+ // Legacy (pre-rollout) envelope: top-level type carried the role directly.
622
+ role = mapRole(recordType);
623
+ disposition = 'keep';
624
+ contentText = extractContentText(payload.content) ?? s(raw.content, payload.text) ?? null;
625
+ } else if (recordType === 'message') {
626
+ role = mapRole(raw.role ?? payload.role);
627
+ disposition = 'keep';
628
+ contentText = extractContentText(payload.content ?? raw.content) ?? s(payload.text, raw.text) ?? null;
629
+ } else if (recordType === 'event_msg') {
630
+ role = 'meta';
631
+ disposition = 'meta';
632
+ contentText = s(payload.message, payload.text) ?? null;
633
+ // Per-turn usage lives on token_count transport events (task 0580 R2): real files
634
+ // carry `payload.info.last_token_usage`; the legacy `payload.token_count` path is kept.
635
+ usage = o(o(payload.info).last_token_usage);
636
+ } else {
637
+ role = 'meta';
638
+ disposition = 'meta';
639
+ contentText = s(payload.text, raw.instructions) ?? null;
640
+ }
641
+
589
642
  const model = s(raw.model, payload.model, o(o(raw.turn_context).payload).model);
590
643
  const cwd = s(raw.cwd, raw.dir);
591
644
 
592
645
  const tokenCount = payload.token_count as Record<string, unknown> | undefined;
593
- const inputTokens = (tokenCount?.input ?? tokenCount?.input_tokens ?? undefined) as number | undefined;
594
- const outputTokens = (tokenCount?.output ?? tokenCount?.output_tokens ?? undefined) as number | undefined;
646
+ const usageInput = (usage?.input_tokens ?? tokenCount?.input ?? tokenCount?.input_tokens ?? undefined) as
647
+ | number
648
+ | undefined;
649
+ const usageOutput = (usage?.output_tokens ?? tokenCount?.output ?? tokenCount?.output_tokens ?? undefined) as
650
+ | number
651
+ | undefined;
652
+ const cacheRead = (usage?.cached_input_tokens ?? undefined) as number | undefined;
595
653
 
596
654
  const turnCtx = raw.turn_context as Record<string, unknown> | undefined;
597
655
  const turnPayload = turnCtx?.payload as Record<string, unknown> | undefined;
598
656
  const turnInputTokens = turnPayload?.input_tokens as number | undefined;
599
657
  const turnOutputTokens = turnPayload?.output_tokens as number | undefined;
600
658
 
601
- const finalInput = inputTokens !== undefined ? inputTokens : turnInputTokens;
602
- const finalOutput = outputTokens !== undefined ? outputTokens : turnOutputTokens;
659
+ const finalInput = usageInput !== undefined ? usageInput : turnInputTokens;
660
+ const finalOutput = usageOutput !== undefined ? usageOutput : turnOutputTokens;
603
661
  const costUsd = computeCost(finalInput, finalOutput, model);
604
662
 
605
663
  const messageSplitIndex = entries.length;
@@ -611,48 +669,57 @@ export function codexSplit(raw: Record<string, unknown>): readonly SplitEntry[]
611
669
  seq,
612
670
  role,
613
671
  record_type: recordType,
614
- disposition: 'keep',
672
+ disposition,
615
673
  ts,
616
674
  duration_ms: undefined,
617
675
  model: model ?? null,
618
676
  input_tokens: finalInput ?? null,
619
677
  output_tokens: finalOutput ?? null,
620
- cache_read_tokens: null,
678
+ cache_read_tokens: cacheRead ?? null,
621
679
  cache_write_tokens: null,
622
680
  cost_usd: costUsd ?? null,
623
- content_text: s(payload.content, payload.text, raw.content, raw.text) ?? null,
681
+ content_text: contentText,
624
682
  cwd: cwd ?? null,
625
683
  provenance: 'ambient',
626
684
  },
627
685
  });
628
686
 
629
- // Tool calls: function_call in response_item
630
- const responseItems = payload.response_item
631
- ? [payload.response_item as Record<string, unknown>]
632
- : Array.isArray(payload.response_items)
633
- ? (payload.response_items as Record<string, unknown>[])
634
- : [];
635
- for (const item of responseItems) {
636
- if (item.function_call) {
637
- const fc = item.function_call as Record<string, unknown>;
638
- entries.push({
639
- targetTable: 'history_tool_call',
640
- record: {
641
- _messageSplitIndex: messageSplitIndex,
642
- session_id: sessionId,
643
- seq,
644
- tool_name: String(fc.name ?? ''),
645
- args_digest: argsDigest(fc.arguments),
646
- args_raw: maybeArgsRaw('codex', String(fc.name ?? ''), fc.arguments),
647
- status: 'ok',
648
- started_at: undefined,
649
- completed_at: undefined,
650
- duration_ms: undefined,
651
- result_bytes: undefined,
652
- error_text: undefined,
653
- },
654
- });
655
- }
687
+ // Tool calls. Live rollouts emit each call as its own response_item whose payload IS the
688
+ // item (`payload.type === "function_call" | "custom_tool_call"`, name/call_id/arguments or
689
+ // input at payload level); the legacy nested `payload.response_item[_s]` shape is kept.
690
+ const toolItems: Record<string, unknown>[] = [];
691
+ if (
692
+ (recordType === 'response_item' || recordType === 'function_call' || recordType === 'custom_tool_call') &&
693
+ (payloadType === 'function_call' || payloadType === 'custom_tool_call')
694
+ ) {
695
+ toolItems.push(payload);
696
+ } else if (payload.response_item) {
697
+ toolItems.push(payload.response_item as Record<string, unknown>);
698
+ } else if (Array.isArray(payload.response_items)) {
699
+ toolItems.push(...(payload.response_items as Record<string, unknown>[]));
700
+ }
701
+ for (const item of toolItems) {
702
+ const fc = (item.function_call as Record<string, unknown> | undefined) ?? item;
703
+ const toolName = s(fc.name) ?? '';
704
+ const args = fc.arguments ?? fc.input;
705
+ entries.push({
706
+ targetTable: 'history_tool_call',
707
+ record: {
708
+ _messageSplitIndex: messageSplitIndex,
709
+ session_id: sessionId,
710
+ seq,
711
+ tool_name: toolName,
712
+ call_id: s(fc.call_id),
713
+ args_digest: argsDigest(args),
714
+ args_raw: maybeArgsRaw('codex', toolName, args),
715
+ status: 'ok',
716
+ started_at: undefined,
717
+ completed_at: undefined,
718
+ duration_ms: undefined,
719
+ result_bytes: undefined,
720
+ error_text: undefined,
721
+ },
722
+ });
656
723
  }
657
724
 
658
725
  return entries;
@@ -668,7 +735,7 @@ export function agySplit(raw: Record<string, unknown>): readonly SplitEntry[] {
668
735
  const recordType = String(raw.type ?? '');
669
736
  const sessionId = s(raw.session_id, raw.conversation_id) ?? 'unknown';
670
737
  const seq = typeof raw.seq === 'number' ? raw.seq : typeof raw.step_index === 'number' ? raw.step_index : 0;
671
- const ts = s(raw.created_at, raw.timestamp, raw.ts) ?? new Date(0).toISOString();
738
+ const ts = timestampOf(raw.created_at, raw.timestamp, raw.ts);
672
739
 
673
740
  let role: string;
674
741
  let disposition: string;
@@ -788,7 +855,7 @@ export function geminiSplit(raw: Record<string, unknown>, context?: TransformCon
788
855
  const state = o(raw.$set);
789
856
  const sessionId = sessionIdFromContext(context, raw);
790
857
  const seq = context?.sourceLine ?? 0;
791
- const ts = s(raw.timestamp, raw.startTime, raw.lastUpdated, state.lastUpdated) ?? new Date(0).toISOString();
858
+ const ts = timestampOf(raw.timestamp, raw.startTime, raw.lastUpdated, state.lastUpdated);
792
859
 
793
860
  if (recordType === 'session' || recordType === 'state') {
794
861
  return [
@@ -906,7 +973,7 @@ function numberOrNull(value: unknown): number | null {
906
973
  function normalizeGrokRecord(raw: Record<string, unknown>): {
907
974
  recordType: string;
908
975
  sessionId: string;
909
- ts: string;
976
+ ts: string | undefined;
910
977
  seq: number;
911
978
  body: Record<string, unknown>;
912
979
  model: string | undefined;
@@ -961,8 +1028,13 @@ function normalizeGrokRecord(raw: Record<string, unknown>): {
961
1028
  );
962
1029
  const usage = (body.usage ?? raw.usage) as Record<string, unknown> | undefined;
963
1030
  const toolMeta = meta?.['x.ai/tool'] as Record<string, unknown> | undefined;
1031
+ // The tool's real name is `_meta["x.ai/tool"].name` (or `kind`); `title` is a human label
1032
+ // like "Read `/path`" — used only as a last resort, stripped at the first backtick so the
1033
+ // argument payload never pollutes tool_name (task 0580 D3).
1034
+ const titleFallback = s(body.title)?.split('`')[0]?.trim();
964
1035
  const toolName =
965
- s(body.title, body.tool_name, body.name, toolMeta?.name, toolMeta?.label, raw.tool_name, raw.name) ?? '';
1036
+ s(toolMeta?.name, body.kind, body.tool_name, body.name, raw.tool_name, raw.name) ??
1037
+ (titleFallback !== undefined && titleFallback.length > 0 && titleFallback.length <= 40 ? titleFallback : '');
966
1038
  const toolArgs = body.rawInput ?? body.input ?? body.arguments ?? toolMeta?.input ?? raw.input ?? raw.arguments;
967
1039
  const toolStatus = s(body.status, raw.outcome, raw.status) ?? 'ok';
968
1040
  const durationMs =
@@ -995,21 +1067,8 @@ function normalizeGrokRecord(raw: Record<string, unknown>): {
995
1067
  };
996
1068
  }
997
1069
 
998
- function normalizeTs(value: unknown): string {
999
- if (typeof value === 'number' && Number.isFinite(value)) {
1000
- // Grok timestamps are unix seconds (sometimes with fractional).
1001
- const ms = value > 1e12 ? value : value * 1000;
1002
- return new Date(ms).toISOString();
1003
- }
1004
- if (typeof value === 'string' && value.length > 0) {
1005
- const asNum = Number(value);
1006
- if (Number.isFinite(asNum) && /^\d+(\.\d+)?$/.test(value.trim())) {
1007
- const ms = asNum > 1e12 ? asNum : asNum * 1000;
1008
- return new Date(ms).toISOString();
1009
- }
1010
- return value;
1011
- }
1012
- return new Date(0).toISOString();
1070
+ function normalizeTs(value: unknown): string | undefined {
1071
+ return timestampOf(value);
1013
1072
  }
1014
1073
 
1015
1074
  function extractGrokContent(content: unknown): string | null {
@@ -1385,10 +1444,32 @@ function s(...values: readonly unknown[]): string | undefined {
1385
1444
  }
1386
1445
  return undefined;
1387
1446
  }
1447
+ /**
1448
+ * First usable timestamp: an ISO-ish string passes through, a finite number (or digit-string)
1449
+ * is converted from epoch seconds/millis (>1e12 treated as ms). Returns undefined when nothing
1450
+ * usable exists — mappers must persist null ts rather than a 1970 sentinel (task 0580 D4).
1451
+ */
1452
+ function timestampOf(...values: readonly unknown[]): string | undefined {
1453
+ for (const value of values) {
1454
+ if (typeof value === 'number' && Number.isFinite(value)) {
1455
+ const ms = value > 1e12 ? value : value * 1000;
1456
+ return new Date(ms).toISOString();
1457
+ }
1458
+ if (typeof value === 'string' && value.length > 0) {
1459
+ const asNum = Number(value);
1460
+ if (Number.isFinite(asNum) && /^\d+(\.\d+)?$/.test(value.trim())) {
1461
+ const ms = asNum > 1e12 ? asNum : asNum * 1000;
1462
+ return new Date(ms).toISOString();
1463
+ }
1464
+ return value;
1465
+ }
1466
+ }
1467
+ return undefined;
1468
+ }
1388
1469
 
1389
1470
  function mapRole(type: unknown): string {
1390
1471
  const t = String(type ?? '').toLowerCase();
1391
- if (t === 'user' || t === 'human') return 'user';
1472
+ if (t === 'user' || t === 'human' || t === 'developer') return 'user';
1392
1473
  if (t === 'assistant' || t === 'ai' || t === 'model') return 'assistant';
1393
1474
  if (t === 'system' || t === 'error') return 'system';
1394
1475
  return t || 'unknown';
@@ -1415,7 +1496,9 @@ function extractContentText(content: unknown): string | undefined {
1415
1496
  for (const block of content) {
1416
1497
  if (typeof block !== 'object' || block === null) continue;
1417
1498
  const b = block as Record<string, unknown>;
1418
- if (b.type === 'text' && typeof b.text === 'string') {
1499
+ // Claude emits {type:"text",text}; codex emits {type:"input_text"|"output_text"|
1500
+ // "summary_text",text} — any block carrying a string `text` is text content.
1501
+ if (typeof b.text === 'string') {
1419
1502
  parts.push(b.text);
1420
1503
  }
1421
1504
  }
package/src/schema-sql.ts CHANGED
@@ -37,7 +37,7 @@ CREATE TABLE IF NOT EXISTS history_message (
37
37
  role TEXT NOT NULL,
38
38
  record_type TEXT NOT NULL,
39
39
  disposition TEXT NOT NULL,
40
- ts TEXT NOT NULL,
40
+ ts TEXT,
41
41
  duration_ms INTEGER,
42
42
  model TEXT,
43
43
  input_tokens INTEGER,