@gobing-ai/ts-llm-jsonl-importer 0.4.37 → 0.4.39
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.
- package/dist/mappers.d.ts.map +1 -1
- package/dist/mappers.js +152 -68
- package/dist/schema-sql.js +1 -1
- package/package.json +4 -4
- package/src/mappers.ts +155 -72
- package/src/schema-sql.ts +1 -1
package/dist/mappers.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mappers.d.ts","sourceRoot":"","sources":["../src/mappers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAmExE,kFAAkF;AAClF,wBAAgB,UAAU,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAGhD;AAwCD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAMhG;AAMD,yEAAyE;AACzE,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,UAAU,EAAE,
|
|
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 =
|
|
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 :
|
|
169
|
-
const inputTokens =
|
|
170
|
-
const outputTokens =
|
|
171
|
-
const cacheRead = (usage?.cacheReadTokens ?? usage?.cache_read_tokens ??
|
|
172
|
-
const cacheWrite = (usage?.cacheWriteTokens ?? usage?.cache_write_tokens ??
|
|
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`;
|
|
242
|
-
|
|
243
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
547
|
-
const
|
|
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 =
|
|
553
|
-
const finalOutput =
|
|
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
|
|
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:
|
|
631
|
+
content_text: contentText,
|
|
573
632
|
cwd: cwd ?? null,
|
|
574
633
|
provenance: 'ambient',
|
|
575
634
|
},
|
|
576
635
|
});
|
|
577
|
-
// Tool calls
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/schema-sql.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobing-ai/ts-llm-jsonl-importer",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.39",
|
|
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.
|
|
51
|
-
"@gobing-ai/ts-runtime": "^0.4.
|
|
52
|
-
"@gobing-ai/ts-utils": "^0.4.
|
|
50
|
+
"@gobing-ai/ts-db": "^0.4.39",
|
|
51
|
+
"@gobing-ai/ts-runtime": "^0.4.39",
|
|
52
|
+
"@gobing-ai/ts-utils": "^0.4.39",
|
|
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 =
|
|
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>
|
|
178
|
-
const inputTokens =
|
|
179
|
-
const outputTokens =
|
|
180
|
-
const cacheRead = (usage?.cacheReadTokens ?? usage?.cache_read_tokens ??
|
|
181
|
-
|
|
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`;
|
|
257
|
-
|
|
258
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
594
|
-
|
|
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 =
|
|
602
|
-
const finalOutput =
|
|
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
|
|
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:
|
|
681
|
+
content_text: contentText,
|
|
624
682
|
cwd: cwd ?? null,
|
|
625
683
|
provenance: 'ambient',
|
|
626
684
|
},
|
|
627
685
|
});
|
|
628
686
|
|
|
629
|
-
// Tool calls
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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 =
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|