@gobing-ai/ts-llm-jsonl-importer 0.4.19 → 0.4.21
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/importer.d.ts.map +1 -1
- package/dist/importer.js +32 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/jsonl-importer-dao.d.ts +6 -6
- package/dist/jsonl-importer-dao.d.ts.map +1 -1
- package/dist/jsonl-importer-dao.js +95 -9
- package/dist/mappers.d.ts +41 -0
- package/dist/mappers.d.ts.map +1 -0
- package/dist/mappers.js +1035 -0
- package/dist/schema-sql.d.ts.map +1 -1
- package/dist/schema-sql.js +57 -0
- package/dist/sources.d.ts.map +1 -1
- package/dist/sources.js +23 -5
- package/dist/types.d.ts +8 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/importer.ts +41 -5
- package/src/index.ts +1 -0
- package/src/jsonl-importer-dao.ts +108 -17
- package/src/mappers.ts +1130 -0
- package/src/schema-sql.ts +57 -0
- package/src/sources.ts +92 -5
- package/src/types.ts +19 -2
package/src/mappers.ts
ADDED
|
@@ -0,0 +1,1130 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { sha256 } from './hash';
|
|
3
|
+
import type { JsonObject, SplitEntry } from './types';
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Helper: identity field map for typed columns
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
/** Columns the mapper may produce for history_message (excluding framework-managed fields). */
|
|
10
|
+
const MESSAGE_MAPPER_KEYS: readonly string[] = [
|
|
11
|
+
'session_id',
|
|
12
|
+
'seq',
|
|
13
|
+
'turn_index',
|
|
14
|
+
'role',
|
|
15
|
+
'record_type',
|
|
16
|
+
'disposition',
|
|
17
|
+
'ts',
|
|
18
|
+
'duration_ms',
|
|
19
|
+
'model',
|
|
20
|
+
'input_tokens',
|
|
21
|
+
'output_tokens',
|
|
22
|
+
'cache_read_tokens',
|
|
23
|
+
'cache_write_tokens',
|
|
24
|
+
'cost_usd',
|
|
25
|
+
'content_text',
|
|
26
|
+
'cwd',
|
|
27
|
+
'provenance',
|
|
28
|
+
'run_id',
|
|
29
|
+
'task_wbs',
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/** Columns the mapper may produce for history_tool_call. */
|
|
33
|
+
const TOOL_CALL_MAPPER_KEYS: readonly string[] = [
|
|
34
|
+
'_messageSplitIndex',
|
|
35
|
+
'session_id',
|
|
36
|
+
'seq',
|
|
37
|
+
'tool_name',
|
|
38
|
+
'args_digest',
|
|
39
|
+
'status',
|
|
40
|
+
'started_at',
|
|
41
|
+
'completed_at',
|
|
42
|
+
'duration_ms',
|
|
43
|
+
'result_bytes',
|
|
44
|
+
'error_text',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/** Build an identity fieldMap for the given column list. */
|
|
48
|
+
function identityFieldMap(keys: readonly string[]): Record<string, string> {
|
|
49
|
+
const map: Record<string, string> = {};
|
|
50
|
+
for (const key of keys) {
|
|
51
|
+
map[key] = key;
|
|
52
|
+
}
|
|
53
|
+
return map;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Source-specific helpers
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
/** Build a provenance string: 'ambient' unless cwd implies a spur workspace. */
|
|
61
|
+
function detectProvenance(cwd?: string): 'ambient' | 'spur-run' {
|
|
62
|
+
if (cwd === undefined) return 'ambient';
|
|
63
|
+
return cwd.includes('/spur') || cwd.includes('/spur-new') ? 'spur-run' : 'ambient';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Compute args_digest: sha256 of stable-jsonified, key-sorted, redacted args. */
|
|
67
|
+
function argsDigest(args: unknown): string {
|
|
68
|
+
const redacted = redactArgs(args);
|
|
69
|
+
return sha256(redacted);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Redact tool arguments for digest: replace string values > 80 chars or containing secrets. */
|
|
73
|
+
function redactArgs(args: unknown): unknown {
|
|
74
|
+
if (typeof args === 'string') {
|
|
75
|
+
if (args.length > 80) return '[REDACTED:long]';
|
|
76
|
+
if (/[A-Za-z0-9+/]{40,}=*|[A-Za-z0-9_-]{20,}/.test(args)) return '[REDACTED:secret]';
|
|
77
|
+
return args;
|
|
78
|
+
}
|
|
79
|
+
if (Array.isArray(args)) return args.map(redactArgs);
|
|
80
|
+
if (args !== null && typeof args === 'object') {
|
|
81
|
+
const result: Record<string, unknown> = {};
|
|
82
|
+
for (const [key, value] of Object.entries(args as Record<string, unknown>)) {
|
|
83
|
+
result[key] = redactArgs(value);
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
return args;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Claude mapper
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
/** Map a Claude Code JSONL record into one or more ETL split entries. */
|
|
95
|
+
export function claudeSplit(raw: Record<string, unknown>): readonly SplitEntry[] {
|
|
96
|
+
const entries: SplitEntry[] = [];
|
|
97
|
+
const sessionId = s(raw.sessionId, raw.conversation_uuid) ?? 'unknown';
|
|
98
|
+
const seq = typeof raw.seq === 'number' ? raw.seq : typeof raw.messageIndex === 'number' ? raw.messageIndex : 0;
|
|
99
|
+
const ts = s(raw.ts, raw.timestamp, raw.createdAt) ?? new Date(0).toISOString();
|
|
100
|
+
const role = mapRole(raw.type ?? raw.role);
|
|
101
|
+
const recordType = String(raw.type ?? '');
|
|
102
|
+
const model = s(raw.model);
|
|
103
|
+
const cwd = s(raw.cwd, raw.dir);
|
|
104
|
+
|
|
105
|
+
// Check for system/turn-duration records
|
|
106
|
+
if (raw.subtype === 'turn_duration' && typeof raw.durationMs === 'number') {
|
|
107
|
+
entries.push({
|
|
108
|
+
targetTable: 'history_message',
|
|
109
|
+
record: {
|
|
110
|
+
session_id: sessionId,
|
|
111
|
+
seq,
|
|
112
|
+
role: 'system',
|
|
113
|
+
record_type: 'turn_duration',
|
|
114
|
+
disposition: 'meta',
|
|
115
|
+
ts,
|
|
116
|
+
duration_ms: raw.durationMs,
|
|
117
|
+
content_text: null,
|
|
118
|
+
provenance: detectProvenance(cwd),
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
return entries;
|
|
122
|
+
}
|
|
123
|
+
if (recordType === 'attachment' || recordType.startsWith('file-history')) {
|
|
124
|
+
entries.push({
|
|
125
|
+
targetTable: 'history_message',
|
|
126
|
+
record: {
|
|
127
|
+
session_id: sessionId,
|
|
128
|
+
seq,
|
|
129
|
+
role: 'meta',
|
|
130
|
+
record_type: recordType,
|
|
131
|
+
disposition: 'meta',
|
|
132
|
+
ts,
|
|
133
|
+
provenance: detectProvenance(cwd),
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
return entries;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Main message
|
|
140
|
+
const hasUsage = typeof raw.usage === 'object' && raw.usage !== null;
|
|
141
|
+
const usage = hasUsage ? (raw.usage as Record<string, unknown>) : undefined;
|
|
142
|
+
const inputTokens = typeof raw.inputTokens === 'number' ? raw.inputTokens : undefined;
|
|
143
|
+
const outputTokens = typeof raw.outputTokens === 'number' ? raw.outputTokens : undefined;
|
|
144
|
+
const cacheRead = (usage?.cacheReadTokens ?? usage?.cache_read_tokens ?? undefined) as number | undefined;
|
|
145
|
+
const cacheWrite = (usage?.cacheWriteTokens ?? usage?.cache_write_tokens ?? undefined) as number | undefined;
|
|
146
|
+
const costUsd = computeCost(inputTokens, outputTokens, model);
|
|
147
|
+
|
|
148
|
+
const contentBlocks = raw.content;
|
|
149
|
+
const contentText = extractContentText(contentBlocks);
|
|
150
|
+
|
|
151
|
+
const messageSplitIndex = entries.length;
|
|
152
|
+
|
|
153
|
+
entries.push({
|
|
154
|
+
targetTable: 'history_message',
|
|
155
|
+
record: {
|
|
156
|
+
session_id: sessionId,
|
|
157
|
+
seq,
|
|
158
|
+
turn_index: typeof raw.turnIndex === 'number' ? raw.turnIndex : undefined,
|
|
159
|
+
role,
|
|
160
|
+
record_type: recordType,
|
|
161
|
+
disposition: 'keep',
|
|
162
|
+
ts,
|
|
163
|
+
duration_ms: undefined,
|
|
164
|
+
model: model ?? null,
|
|
165
|
+
input_tokens: inputTokens ?? null,
|
|
166
|
+
output_tokens: outputTokens ?? null,
|
|
167
|
+
cache_read_tokens: cacheRead ?? null,
|
|
168
|
+
cache_write_tokens: cacheWrite ?? null,
|
|
169
|
+
cost_usd: costUsd ?? null,
|
|
170
|
+
content_text: contentText ?? null,
|
|
171
|
+
cwd: cwd ?? null,
|
|
172
|
+
provenance: detectProvenance(cwd),
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Tool calls from content blocks
|
|
177
|
+
if (Array.isArray(contentBlocks) && role === 'assistant') {
|
|
178
|
+
for (const block of contentBlocks) {
|
|
179
|
+
if (typeof block !== 'object' || block === null) continue;
|
|
180
|
+
const b = block as Record<string, unknown>;
|
|
181
|
+
if (b.type === 'tool_use') {
|
|
182
|
+
entries.push({
|
|
183
|
+
targetTable: 'history_tool_call',
|
|
184
|
+
record: {
|
|
185
|
+
_messageSplitIndex: messageSplitIndex,
|
|
186
|
+
session_id: sessionId,
|
|
187
|
+
seq,
|
|
188
|
+
tool_name: String(b.name ?? ''),
|
|
189
|
+
args_digest: argsDigest(b.input),
|
|
190
|
+
status: 'ok',
|
|
191
|
+
started_at: undefined,
|
|
192
|
+
completed_at: undefined,
|
|
193
|
+
duration_ms: undefined,
|
|
194
|
+
result_bytes: undefined,
|
|
195
|
+
error_text: undefined,
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return entries;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
// Pi mapper
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
/** Map a Pi JSONL record into one or more ETL split entries. */
|
|
210
|
+
export function piSplit(raw: unknown): readonly SplitEntry[] {
|
|
211
|
+
const entries: SplitEntry[] = [];
|
|
212
|
+
const r = raw as Record<string, unknown>;
|
|
213
|
+
const sessionId = s(r.id, o(r.session).id) ?? 'unknown';
|
|
214
|
+
const seq = typeof r.seq === 'number' ? r.seq : 0;
|
|
215
|
+
const ts = s(r.ts, r.timestamp, r.createdAt) ?? new Date(0).toISOString();
|
|
216
|
+
const role = mapRole(r.type ?? r.role);
|
|
217
|
+
const recordType = String(r.type ?? '');
|
|
218
|
+
const model = s(r.model, o(r.message).model);
|
|
219
|
+
const cwd = s(r.cwd, r.dir);
|
|
220
|
+
|
|
221
|
+
const msg = r.message as Record<string, unknown> | undefined;
|
|
222
|
+
const usage = msg?.usage as Record<string, unknown> | undefined;
|
|
223
|
+
const inputTokens = (usage?.input ?? usage?.input_tokens ?? undefined) as number | undefined;
|
|
224
|
+
const outputTokens = (usage?.output ?? usage?.output_tokens ?? undefined) as number | undefined;
|
|
225
|
+
const cacheRead = (usage?.cacheRead ?? usage?.cache_read_tokens ?? undefined) as number | undefined;
|
|
226
|
+
const cacheWrite = (usage?.cacheWrite ?? usage?.cache_write_tokens ?? undefined) as number | undefined;
|
|
227
|
+
const costObj = (msg?.cost ?? r.cost) as Record<string, unknown> | undefined;
|
|
228
|
+
const costUsd = typeof costObj?.total === 'number' ? costObj.total : computeCost(inputTokens, outputTokens, model);
|
|
229
|
+
|
|
230
|
+
const messageSplitIndex = entries.length;
|
|
231
|
+
|
|
232
|
+
entries.push({
|
|
233
|
+
targetTable: 'history_message',
|
|
234
|
+
record: {
|
|
235
|
+
session_id: sessionId,
|
|
236
|
+
seq,
|
|
237
|
+
role,
|
|
238
|
+
record_type: recordType,
|
|
239
|
+
disposition: 'keep',
|
|
240
|
+
ts,
|
|
241
|
+
duration_ms: undefined,
|
|
242
|
+
model: model ?? null,
|
|
243
|
+
input_tokens: inputTokens ?? null,
|
|
244
|
+
output_tokens: outputTokens ?? null,
|
|
245
|
+
cache_read_tokens: cacheRead ?? null,
|
|
246
|
+
cache_write_tokens: cacheWrite ?? null,
|
|
247
|
+
cost_usd: costUsd ?? null,
|
|
248
|
+
content_text: s(r.content, r.text, msg?.content) ?? null,
|
|
249
|
+
cwd: cwd ?? null,
|
|
250
|
+
provenance: detectProvenance(cwd),
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Tool calls from toolCall blocks in assistant content
|
|
255
|
+
if (Array.isArray(r.content) && role === 'assistant') {
|
|
256
|
+
for (const block of r.content as Record<string, unknown>[]) {
|
|
257
|
+
if (block?.toolCall) {
|
|
258
|
+
const tc = block.toolCall as Record<string, unknown>;
|
|
259
|
+
entries.push({
|
|
260
|
+
targetTable: 'history_tool_call',
|
|
261
|
+
record: {
|
|
262
|
+
_messageSplitIndex: messageSplitIndex,
|
|
263
|
+
session_id: sessionId,
|
|
264
|
+
seq,
|
|
265
|
+
tool_name: String(tc.name ?? ''),
|
|
266
|
+
args_digest: argsDigest(tc.input ?? tc.arguments),
|
|
267
|
+
status: 'ok',
|
|
268
|
+
started_at: undefined,
|
|
269
|
+
completed_at: undefined,
|
|
270
|
+
duration_ms: undefined,
|
|
271
|
+
result_bytes: undefined,
|
|
272
|
+
error_text: undefined,
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return entries;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
// OMP mapper (near-identical to pi)
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
/** Map an OMP JSONL record into one or more ETL split entries. */
|
|
287
|
+
export function ompSplit(raw: Record<string, unknown>): readonly SplitEntry[] {
|
|
288
|
+
const recordType = String(raw.type ?? '');
|
|
289
|
+
if (
|
|
290
|
+
recordType === 'title' ||
|
|
291
|
+
recordType === 'title_change' ||
|
|
292
|
+
recordType === 'service_tier_change' ||
|
|
293
|
+
recordType === 'ttsr_injection' ||
|
|
294
|
+
recordType === 'session_init' ||
|
|
295
|
+
recordType === 'compaction'
|
|
296
|
+
) {
|
|
297
|
+
return [
|
|
298
|
+
{
|
|
299
|
+
targetTable: 'history_message',
|
|
300
|
+
record: {
|
|
301
|
+
session_id: s(raw.id, o(raw.session).id) ?? 'unknown',
|
|
302
|
+
seq: typeof raw.seq === 'number' ? raw.seq : 0,
|
|
303
|
+
role: 'meta',
|
|
304
|
+
record_type: recordType,
|
|
305
|
+
disposition: 'meta',
|
|
306
|
+
ts: s(raw.ts, raw.timestamp) ?? new Date(0).toISOString(),
|
|
307
|
+
provenance: 'ambient',
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const entries: SplitEntry[] = [];
|
|
314
|
+
const sessionId = s(raw.id, o(raw.session).id) ?? 'unknown';
|
|
315
|
+
const seq = typeof raw.seq === 'number' ? raw.seq : 0;
|
|
316
|
+
const ts = s(raw.ts, raw.timestamp, raw.createdAt) ?? new Date(0).toISOString();
|
|
317
|
+
const role = mapRole(raw.type ?? raw.role);
|
|
318
|
+
const model = s(raw.model, o(raw.message).model);
|
|
319
|
+
const cwd = s(raw.cwd, raw.dir);
|
|
320
|
+
|
|
321
|
+
const msg = raw.message as Record<string, unknown> | undefined;
|
|
322
|
+
const usage = msg?.usage as Record<string, unknown> | undefined;
|
|
323
|
+
const inputTokens = (usage?.input ?? usage?.input_tokens ?? undefined) as number | undefined;
|
|
324
|
+
const outputTokens = (usage?.output ?? usage?.output_tokens ?? undefined) as number | undefined;
|
|
325
|
+
const cacheRead = (usage?.cacheRead ?? usage?.cache_read_tokens ?? undefined) as number | undefined;
|
|
326
|
+
const cacheWrite = (usage?.cacheWrite ?? usage?.cache_write_tokens ?? undefined) as number | undefined;
|
|
327
|
+
const costObj = (msg?.cost ?? raw.cost) as Record<string, unknown> | undefined;
|
|
328
|
+
const costUsd = typeof costObj?.total === 'number' ? costObj.total : computeCost(inputTokens, outputTokens, model);
|
|
329
|
+
|
|
330
|
+
const messageSplitIndex = entries.length;
|
|
331
|
+
|
|
332
|
+
entries.push({
|
|
333
|
+
targetTable: 'history_message',
|
|
334
|
+
record: {
|
|
335
|
+
session_id: sessionId,
|
|
336
|
+
seq,
|
|
337
|
+
role,
|
|
338
|
+
record_type: recordType,
|
|
339
|
+
disposition: 'keep',
|
|
340
|
+
ts,
|
|
341
|
+
duration_ms: undefined,
|
|
342
|
+
model: model ?? null,
|
|
343
|
+
input_tokens: inputTokens ?? null,
|
|
344
|
+
output_tokens: outputTokens ?? null,
|
|
345
|
+
cache_read_tokens: cacheRead ?? null,
|
|
346
|
+
cache_write_tokens: cacheWrite ?? null,
|
|
347
|
+
cost_usd: costUsd ?? null,
|
|
348
|
+
content_text: s(raw.content, raw.text, msg?.content) ?? null,
|
|
349
|
+
cwd: cwd ?? null,
|
|
350
|
+
provenance: detectProvenance(cwd),
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
if (Array.isArray(raw.content) && role === 'assistant') {
|
|
355
|
+
for (const block of raw.content as Record<string, unknown>[]) {
|
|
356
|
+
if (block?.toolCall) {
|
|
357
|
+
const tc = block.toolCall as Record<string, unknown>;
|
|
358
|
+
entries.push({
|
|
359
|
+
targetTable: 'history_tool_call',
|
|
360
|
+
record: {
|
|
361
|
+
_messageSplitIndex: messageSplitIndex,
|
|
362
|
+
session_id: sessionId,
|
|
363
|
+
seq,
|
|
364
|
+
tool_name: String(tc.name ?? ''),
|
|
365
|
+
args_digest: argsDigest(tc.input ?? tc.arguments),
|
|
366
|
+
status: 'ok',
|
|
367
|
+
started_at: undefined,
|
|
368
|
+
completed_at: undefined,
|
|
369
|
+
duration_ms: undefined,
|
|
370
|
+
result_bytes: undefined,
|
|
371
|
+
error_text: undefined,
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return entries;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ---------------------------------------------------------------------------
|
|
382
|
+
// Codex mapper
|
|
383
|
+
// ---------------------------------------------------------------------------
|
|
384
|
+
|
|
385
|
+
/** Map a Codex JSONL record into one or more ETL split entries. */
|
|
386
|
+
export function codexSplit(raw: Record<string, unknown>): readonly SplitEntry[] {
|
|
387
|
+
const entries: SplitEntry[] = [];
|
|
388
|
+
|
|
389
|
+
const payload = (raw.payload ?? raw) as Record<string, unknown>;
|
|
390
|
+
const sessionId = s(raw.session_id, o(raw.session_meta).id, raw.id) ?? 'unknown';
|
|
391
|
+
const seq = typeof raw.seq === 'number' ? raw.seq : 0;
|
|
392
|
+
const ts = s(raw.timestamp, raw.ts, payload.ts) ?? new Date(0).toISOString();
|
|
393
|
+
const recordType = String(raw.type ?? '');
|
|
394
|
+
|
|
395
|
+
// Check for older short format
|
|
396
|
+
if (recordType === '' && raw.id && raw.timestamp && raw.instructions) {
|
|
397
|
+
return [
|
|
398
|
+
{
|
|
399
|
+
targetTable: 'history_message',
|
|
400
|
+
record: {
|
|
401
|
+
session_id: sessionId,
|
|
402
|
+
seq,
|
|
403
|
+
role: 'unknown',
|
|
404
|
+
record_type: 'short_format',
|
|
405
|
+
disposition: 'unknown',
|
|
406
|
+
ts,
|
|
407
|
+
provenance: 'ambient',
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
];
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const role = mapRole(recordType);
|
|
414
|
+
const model = s(raw.model, payload.model, o(o(raw.turn_context).payload).model);
|
|
415
|
+
const cwd = s(raw.cwd, raw.dir);
|
|
416
|
+
|
|
417
|
+
const tokenCount = payload.token_count as Record<string, unknown> | undefined;
|
|
418
|
+
const inputTokens = (tokenCount?.input ?? tokenCount?.input_tokens ?? undefined) as number | undefined;
|
|
419
|
+
const outputTokens = (tokenCount?.output ?? tokenCount?.output_tokens ?? undefined) as number | undefined;
|
|
420
|
+
|
|
421
|
+
const turnCtx = raw.turn_context as Record<string, unknown> | undefined;
|
|
422
|
+
const turnPayload = turnCtx?.payload as Record<string, unknown> | undefined;
|
|
423
|
+
const turnInputTokens = turnPayload?.input_tokens as number | undefined;
|
|
424
|
+
const turnOutputTokens = turnPayload?.output_tokens as number | undefined;
|
|
425
|
+
|
|
426
|
+
const finalInput = inputTokens !== undefined ? inputTokens : turnInputTokens;
|
|
427
|
+
const finalOutput = outputTokens !== undefined ? outputTokens : turnOutputTokens;
|
|
428
|
+
const costUsd = computeCost(finalInput, finalOutput, model);
|
|
429
|
+
|
|
430
|
+
const messageSplitIndex = entries.length;
|
|
431
|
+
|
|
432
|
+
entries.push({
|
|
433
|
+
targetTable: 'history_message',
|
|
434
|
+
record: {
|
|
435
|
+
session_id: sessionId,
|
|
436
|
+
seq,
|
|
437
|
+
role,
|
|
438
|
+
record_type: recordType,
|
|
439
|
+
disposition: 'keep',
|
|
440
|
+
ts,
|
|
441
|
+
duration_ms: undefined,
|
|
442
|
+
model: model ?? null,
|
|
443
|
+
input_tokens: finalInput ?? null,
|
|
444
|
+
output_tokens: finalOutput ?? null,
|
|
445
|
+
cache_read_tokens: null,
|
|
446
|
+
cache_write_tokens: null,
|
|
447
|
+
cost_usd: costUsd ?? null,
|
|
448
|
+
content_text: s(payload.content, payload.text, raw.content, raw.text) ?? null,
|
|
449
|
+
cwd: cwd ?? null,
|
|
450
|
+
provenance: detectProvenance(cwd),
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// Tool calls: function_call in response_item
|
|
455
|
+
const responseItems = payload.response_item
|
|
456
|
+
? [payload.response_item as Record<string, unknown>]
|
|
457
|
+
: Array.isArray(payload.response_items)
|
|
458
|
+
? (payload.response_items as Record<string, unknown>[])
|
|
459
|
+
: [];
|
|
460
|
+
for (const item of responseItems) {
|
|
461
|
+
if (item.function_call) {
|
|
462
|
+
const fc = item.function_call as Record<string, unknown>;
|
|
463
|
+
entries.push({
|
|
464
|
+
targetTable: 'history_tool_call',
|
|
465
|
+
record: {
|
|
466
|
+
_messageSplitIndex: messageSplitIndex,
|
|
467
|
+
session_id: sessionId,
|
|
468
|
+
seq,
|
|
469
|
+
tool_name: String(fc.name ?? ''),
|
|
470
|
+
args_digest: argsDigest(fc.arguments),
|
|
471
|
+
status: 'ok',
|
|
472
|
+
started_at: undefined,
|
|
473
|
+
completed_at: undefined,
|
|
474
|
+
duration_ms: undefined,
|
|
475
|
+
result_bytes: undefined,
|
|
476
|
+
error_text: undefined,
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return entries;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// ---------------------------------------------------------------------------
|
|
486
|
+
// AGY mapper (Antigravity CLI)
|
|
487
|
+
// ---------------------------------------------------------------------------
|
|
488
|
+
|
|
489
|
+
/** Map an Antigravity (AGY) JSONL record into one or more ETL split entries. */
|
|
490
|
+
export function agySplit(raw: Record<string, unknown>): readonly SplitEntry[] {
|
|
491
|
+
const entries: SplitEntry[] = [];
|
|
492
|
+
const recordType = String(raw.type ?? '');
|
|
493
|
+
const sessionId = s(raw.session_id, raw.conversation_id) ?? 'unknown';
|
|
494
|
+
const seq = typeof raw.seq === 'number' ? raw.seq : typeof raw.step_index === 'number' ? raw.step_index : 0;
|
|
495
|
+
const ts = s(raw.created_at, raw.timestamp, raw.ts) ?? new Date(0).toISOString();
|
|
496
|
+
|
|
497
|
+
let role: string;
|
|
498
|
+
let disposition: string;
|
|
499
|
+
let contentText: string | null = null;
|
|
500
|
+
|
|
501
|
+
// Classification from task 0463 field map (agy transcript types).
|
|
502
|
+
switch (recordType) {
|
|
503
|
+
case 'USER_INPUT':
|
|
504
|
+
case 'ASK_QUESTION':
|
|
505
|
+
role = 'user';
|
|
506
|
+
disposition = 'keep';
|
|
507
|
+
contentText = s(raw.content, raw.text, raw.message) ?? null;
|
|
508
|
+
break;
|
|
509
|
+
case 'PLANNER_RESPONSE':
|
|
510
|
+
role = 'assistant';
|
|
511
|
+
disposition = 'keep';
|
|
512
|
+
contentText = s(raw.content, raw.text, raw.message, raw.plan) ?? null;
|
|
513
|
+
break;
|
|
514
|
+
case 'ERROR_MESSAGE':
|
|
515
|
+
role = 'system';
|
|
516
|
+
disposition = 'keep';
|
|
517
|
+
contentText = s(raw.content, raw.text, raw.message) ?? null;
|
|
518
|
+
break;
|
|
519
|
+
case 'RUN_COMMAND':
|
|
520
|
+
case 'VIEW_FILE':
|
|
521
|
+
case 'GREP_SEARCH':
|
|
522
|
+
case 'LIST_DIRECTORY':
|
|
523
|
+
case 'READ_URL_CONTENT':
|
|
524
|
+
role = 'tool';
|
|
525
|
+
disposition = 'keep';
|
|
526
|
+
contentText = s(raw.content, raw.command, raw.file_path, raw.pattern) ?? null;
|
|
527
|
+
break;
|
|
528
|
+
case 'CODE_ACTION':
|
|
529
|
+
case 'INVOKE_SUBAGENT':
|
|
530
|
+
role = 'tool';
|
|
531
|
+
disposition = 'keep';
|
|
532
|
+
contentText = s(raw.content, raw.text) ?? null;
|
|
533
|
+
break;
|
|
534
|
+
case 'CONVERSATION_HISTORY':
|
|
535
|
+
case 'CHECKPOINT':
|
|
536
|
+
case 'GENERIC':
|
|
537
|
+
role = 'meta';
|
|
538
|
+
disposition = 'meta';
|
|
539
|
+
contentText = s(raw.content, raw.text) ?? null;
|
|
540
|
+
break;
|
|
541
|
+
default:
|
|
542
|
+
if (recordType.length === 0) {
|
|
543
|
+
role = 'unknown';
|
|
544
|
+
disposition = 'unknown';
|
|
545
|
+
} else {
|
|
546
|
+
// Known discriminator present but not in the frozen 0463 keep/meta table —
|
|
547
|
+
// still a determined type; treat as meta bookkeeping rather than unknown.
|
|
548
|
+
role = 'meta';
|
|
549
|
+
disposition = 'meta';
|
|
550
|
+
contentText = s(raw.content, raw.text) ?? null;
|
|
551
|
+
}
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const messageSplitIndex = entries.length;
|
|
556
|
+
|
|
557
|
+
entries.push({
|
|
558
|
+
targetTable: 'history_message',
|
|
559
|
+
record: {
|
|
560
|
+
session_id: sessionId,
|
|
561
|
+
seq,
|
|
562
|
+
role,
|
|
563
|
+
record_type: recordType,
|
|
564
|
+
disposition,
|
|
565
|
+
ts,
|
|
566
|
+
duration_ms: undefined,
|
|
567
|
+
model: null,
|
|
568
|
+
input_tokens: null,
|
|
569
|
+
output_tokens: null,
|
|
570
|
+
cache_read_tokens: null,
|
|
571
|
+
cache_write_tokens: null,
|
|
572
|
+
cost_usd: null,
|
|
573
|
+
content_text: contentText,
|
|
574
|
+
cwd: null,
|
|
575
|
+
provenance: 'ambient',
|
|
576
|
+
},
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// Tool calls from PLANNER_RESPONSE.tool_calls[]
|
|
580
|
+
if (recordType === 'PLANNER_RESPONSE' && Array.isArray(raw.tool_calls)) {
|
|
581
|
+
for (const tc of raw.tool_calls as Record<string, unknown>[]) {
|
|
582
|
+
entries.push({
|
|
583
|
+
targetTable: 'history_tool_call',
|
|
584
|
+
record: {
|
|
585
|
+
_messageSplitIndex: messageSplitIndex,
|
|
586
|
+
session_id: sessionId,
|
|
587
|
+
seq: typeof tc.step_index === 'number' ? tc.step_index : seq,
|
|
588
|
+
tool_name: String(tc.name ?? tc.tool_name ?? ''),
|
|
589
|
+
args_digest: argsDigest(tc.input ?? tc.arguments),
|
|
590
|
+
status: 'ok',
|
|
591
|
+
started_at: undefined,
|
|
592
|
+
completed_at: undefined,
|
|
593
|
+
duration_ms: undefined,
|
|
594
|
+
result_bytes: undefined,
|
|
595
|
+
error_text: undefined,
|
|
596
|
+
},
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return entries;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// ---------------------------------------------------------------------------
|
|
605
|
+
// Grok mapper
|
|
606
|
+
// ---------------------------------------------------------------------------
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Grok emits two JSONL shapes per session (task 0463):
|
|
610
|
+
* - `events.jsonl`: flat `{ts, type, ...}`
|
|
611
|
+
* - `updates.jsonl`: JSON-RPC-like `{timestamp, method, params:{sessionId, update:{sessionUpdate,...}}}`
|
|
612
|
+
*
|
|
613
|
+
* Normalize both into a single view before disposition mapping.
|
|
614
|
+
*/
|
|
615
|
+
function normalizeGrokRecord(raw: Record<string, unknown>): {
|
|
616
|
+
recordType: string;
|
|
617
|
+
sessionId: string;
|
|
618
|
+
ts: string;
|
|
619
|
+
seq: number;
|
|
620
|
+
body: Record<string, unknown>;
|
|
621
|
+
model: string | undefined;
|
|
622
|
+
contentText: string | null;
|
|
623
|
+
usage: Record<string, unknown> | undefined;
|
|
624
|
+
toolName: string;
|
|
625
|
+
toolArgs: unknown;
|
|
626
|
+
toolStatus: string;
|
|
627
|
+
durationMs: number | undefined;
|
|
628
|
+
errorText: string | undefined;
|
|
629
|
+
hasTypeDiscriminator: boolean;
|
|
630
|
+
} {
|
|
631
|
+
const params = raw.params as Record<string, unknown> | undefined;
|
|
632
|
+
const update = params?.update as Record<string, unknown> | undefined;
|
|
633
|
+
const isUpdatesShape = typeof raw.method === 'string' && params !== undefined && update !== undefined;
|
|
634
|
+
|
|
635
|
+
const body = isUpdatesShape ? update : raw;
|
|
636
|
+
const recordType = String((isUpdatesShape ? update?.sessionUpdate : raw.type) ?? raw.sessionUpdate ?? '');
|
|
637
|
+
const sessionId =
|
|
638
|
+
s(
|
|
639
|
+
params?.sessionId,
|
|
640
|
+
raw.session_id,
|
|
641
|
+
raw.sessionId,
|
|
642
|
+
(raw.params as Record<string, unknown> | undefined)?.sessionId,
|
|
643
|
+
) ?? 'unknown';
|
|
644
|
+
const ts = normalizeTs(raw.ts ?? raw.timestamp ?? body.ts ?? body.timestamp);
|
|
645
|
+
const seq = typeof raw.seq === 'number' ? raw.seq : typeof body.seq === 'number' ? body.seq : 0;
|
|
646
|
+
const meta = (body._meta ?? params?._meta ?? raw._meta) as Record<string, unknown> | undefined;
|
|
647
|
+
const model = s(meta?.modelId, body.model, raw.model);
|
|
648
|
+
const contentText = extractGrokContent(body.content ?? body.text ?? raw.content ?? raw.text);
|
|
649
|
+
const usage = (body.usage ?? raw.usage) as Record<string, unknown> | undefined;
|
|
650
|
+
const toolMeta = meta?.['x.ai/tool'] as Record<string, unknown> | undefined;
|
|
651
|
+
const toolName =
|
|
652
|
+
s(body.title, body.tool_name, body.name, toolMeta?.name, toolMeta?.label, raw.tool_name, raw.name) ?? '';
|
|
653
|
+
const toolArgs = body.rawInput ?? body.input ?? body.arguments ?? toolMeta?.input ?? raw.input ?? raw.arguments;
|
|
654
|
+
const toolStatus = s(body.status, raw.outcome, raw.status) ?? 'ok';
|
|
655
|
+
const durationMs =
|
|
656
|
+
typeof body.duration_ms === 'number'
|
|
657
|
+
? body.duration_ms
|
|
658
|
+
: typeof raw.duration_ms === 'number'
|
|
659
|
+
? raw.duration_ms
|
|
660
|
+
: typeof usage?.apiDurationMs === 'number'
|
|
661
|
+
? (usage.apiDurationMs as number)
|
|
662
|
+
: undefined;
|
|
663
|
+
const errorText =
|
|
664
|
+
body.error !== undefined ? String(body.error) : raw.error !== undefined ? String(raw.error) : undefined;
|
|
665
|
+
const hasTypeDiscriminator = recordType.length > 0 || isUpdatesShape;
|
|
666
|
+
|
|
667
|
+
return {
|
|
668
|
+
recordType,
|
|
669
|
+
sessionId,
|
|
670
|
+
ts,
|
|
671
|
+
seq,
|
|
672
|
+
body,
|
|
673
|
+
model,
|
|
674
|
+
contentText,
|
|
675
|
+
usage,
|
|
676
|
+
toolName,
|
|
677
|
+
toolArgs,
|
|
678
|
+
toolStatus,
|
|
679
|
+
durationMs,
|
|
680
|
+
errorText,
|
|
681
|
+
hasTypeDiscriminator,
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function normalizeTs(value: unknown): string {
|
|
686
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
687
|
+
// Grok timestamps are unix seconds (sometimes with fractional).
|
|
688
|
+
const ms = value > 1e12 ? value : value * 1000;
|
|
689
|
+
return new Date(ms).toISOString();
|
|
690
|
+
}
|
|
691
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
692
|
+
const asNum = Number(value);
|
|
693
|
+
if (Number.isFinite(asNum) && /^\d+(\.\d+)?$/.test(value.trim())) {
|
|
694
|
+
const ms = asNum > 1e12 ? asNum : asNum * 1000;
|
|
695
|
+
return new Date(ms).toISOString();
|
|
696
|
+
}
|
|
697
|
+
return value;
|
|
698
|
+
}
|
|
699
|
+
return new Date(0).toISOString();
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function extractGrokContent(content: unknown): string | null {
|
|
703
|
+
if (typeof content === 'string') return content;
|
|
704
|
+
if (content !== null && typeof content === 'object' && !Array.isArray(content)) {
|
|
705
|
+
const c = content as Record<string, unknown>;
|
|
706
|
+
if (typeof c.text === 'string') return c.text;
|
|
707
|
+
if (typeof c.content === 'string') return c.content;
|
|
708
|
+
}
|
|
709
|
+
if (Array.isArray(content)) {
|
|
710
|
+
const parts: string[] = [];
|
|
711
|
+
for (const block of content) {
|
|
712
|
+
if (typeof block === 'string') parts.push(block);
|
|
713
|
+
else if (block !== null && typeof block === 'object') {
|
|
714
|
+
const b = block as Record<string, unknown>;
|
|
715
|
+
if (typeof b.text === 'string') parts.push(b.text);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return parts.length > 0 ? parts.join('\n') : null;
|
|
719
|
+
}
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/** Bookkeeping event types from 0463 grok field map → disposition meta. */
|
|
724
|
+
const GROK_META_TYPES = new Set([
|
|
725
|
+
'phase_changed',
|
|
726
|
+
'permission_requested',
|
|
727
|
+
'permission_resolved',
|
|
728
|
+
'mcp_server_starting',
|
|
729
|
+
'mcp_server_connected',
|
|
730
|
+
'mcp_config_resolved',
|
|
731
|
+
'mcp_managed_config_result',
|
|
732
|
+
'mcp_transport_decode_error',
|
|
733
|
+
'mcp_init_completed',
|
|
734
|
+
'mcp_health_check',
|
|
735
|
+
'loop_started',
|
|
736
|
+
'first_token',
|
|
737
|
+
'agent_thought_chunk',
|
|
738
|
+
'hook_execution',
|
|
739
|
+
'plan',
|
|
740
|
+
'session_recap',
|
|
741
|
+
'task_backgrounded',
|
|
742
|
+
'task_completed',
|
|
743
|
+
'retry_state',
|
|
744
|
+
'auto_compact_started',
|
|
745
|
+
'auto_compact_completed',
|
|
746
|
+
'compaction_checkpoint',
|
|
747
|
+
'image_compressed',
|
|
748
|
+
'reasoning',
|
|
749
|
+
'system',
|
|
750
|
+
'turn_started',
|
|
751
|
+
'turn_ended',
|
|
752
|
+
'yolo_toggled',
|
|
753
|
+
]);
|
|
754
|
+
|
|
755
|
+
/** Map a Grok JSONL record into one or more ETL split entries. */
|
|
756
|
+
export function grokSplit(raw: Record<string, unknown>): readonly SplitEntry[] {
|
|
757
|
+
const entries: SplitEntry[] = [];
|
|
758
|
+
const n = normalizeGrokRecord(raw);
|
|
759
|
+
const { recordType, sessionId, ts, seq } = n;
|
|
760
|
+
|
|
761
|
+
if (recordType === 'turn_completed') {
|
|
762
|
+
const usage = n.usage;
|
|
763
|
+
const modelUsage = usage?.modelUsage;
|
|
764
|
+
const modelFromUsage =
|
|
765
|
+
modelUsage !== null && typeof modelUsage === 'object'
|
|
766
|
+
? Object.keys(modelUsage as Record<string, unknown>)[0]
|
|
767
|
+
: undefined;
|
|
768
|
+
const model = s(n.model, modelFromUsage);
|
|
769
|
+
const inputTokens = (usage?.inputTokens ?? usage?.input ?? usage?.input_tokens) as number | undefined;
|
|
770
|
+
const outputTokens = (usage?.outputTokens ?? usage?.output ?? usage?.output_tokens) as number | undefined;
|
|
771
|
+
const cacheRead = (usage?.cachedReadTokens ?? usage?.cache_read_tokens) as number | undefined;
|
|
772
|
+
|
|
773
|
+
entries.push({
|
|
774
|
+
targetTable: 'history_message',
|
|
775
|
+
record: {
|
|
776
|
+
session_id: sessionId,
|
|
777
|
+
seq,
|
|
778
|
+
role: 'assistant',
|
|
779
|
+
record_type: recordType,
|
|
780
|
+
disposition: 'keep',
|
|
781
|
+
ts,
|
|
782
|
+
duration_ms: n.durationMs ?? null,
|
|
783
|
+
model: model ?? null,
|
|
784
|
+
input_tokens: inputTokens ?? null,
|
|
785
|
+
output_tokens: outputTokens ?? null,
|
|
786
|
+
cache_read_tokens: cacheRead ?? null,
|
|
787
|
+
cache_write_tokens: null,
|
|
788
|
+
cost_usd: null,
|
|
789
|
+
content_text: n.contentText,
|
|
790
|
+
cwd: null,
|
|
791
|
+
provenance: 'ambient',
|
|
792
|
+
},
|
|
793
|
+
});
|
|
794
|
+
return entries;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
if (recordType === 'tool_started') {
|
|
798
|
+
const messageSplitIndex = 0;
|
|
799
|
+
entries.push({
|
|
800
|
+
targetTable: 'history_message',
|
|
801
|
+
record: {
|
|
802
|
+
session_id: sessionId,
|
|
803
|
+
seq,
|
|
804
|
+
role: 'assistant',
|
|
805
|
+
record_type: recordType,
|
|
806
|
+
disposition: 'keep',
|
|
807
|
+
ts,
|
|
808
|
+
duration_ms: undefined,
|
|
809
|
+
model: n.model ?? null,
|
|
810
|
+
input_tokens: null,
|
|
811
|
+
output_tokens: null,
|
|
812
|
+
cache_read_tokens: null,
|
|
813
|
+
cache_write_tokens: null,
|
|
814
|
+
cost_usd: null,
|
|
815
|
+
content_text: n.contentText,
|
|
816
|
+
cwd: null,
|
|
817
|
+
provenance: 'ambient',
|
|
818
|
+
},
|
|
819
|
+
});
|
|
820
|
+
entries.push({
|
|
821
|
+
targetTable: 'history_tool_call',
|
|
822
|
+
record: {
|
|
823
|
+
_messageSplitIndex: messageSplitIndex,
|
|
824
|
+
session_id: sessionId,
|
|
825
|
+
seq,
|
|
826
|
+
tool_name: n.toolName,
|
|
827
|
+
args_digest: argsDigest(n.toolArgs),
|
|
828
|
+
status: n.toolStatus || 'ok',
|
|
829
|
+
started_at: ts,
|
|
830
|
+
completed_at: undefined,
|
|
831
|
+
duration_ms: undefined,
|
|
832
|
+
result_bytes: undefined,
|
|
833
|
+
error_text: n.errorText,
|
|
834
|
+
},
|
|
835
|
+
});
|
|
836
|
+
return entries;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
if (recordType === 'tool_completed') {
|
|
840
|
+
entries.push({
|
|
841
|
+
targetTable: 'history_tool_call',
|
|
842
|
+
record: {
|
|
843
|
+
_messageSplitIndex: 0,
|
|
844
|
+
session_id: sessionId,
|
|
845
|
+
seq,
|
|
846
|
+
tool_name: n.toolName,
|
|
847
|
+
args_digest: null,
|
|
848
|
+
status: n.toolStatus || 'ok',
|
|
849
|
+
started_at: undefined,
|
|
850
|
+
completed_at: ts,
|
|
851
|
+
duration_ms: n.durationMs,
|
|
852
|
+
result_bytes: undefined,
|
|
853
|
+
error_text: n.errorText,
|
|
854
|
+
},
|
|
855
|
+
});
|
|
856
|
+
return entries;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
if (recordType === 'tool_call' || recordType === 'tool_call_update') {
|
|
860
|
+
// Pair tool_call with a parent message row so message_hash resolution works.
|
|
861
|
+
const messageSplitIndex = 0;
|
|
862
|
+
entries.push({
|
|
863
|
+
targetTable: 'history_message',
|
|
864
|
+
record: {
|
|
865
|
+
session_id: sessionId,
|
|
866
|
+
seq,
|
|
867
|
+
role: 'assistant',
|
|
868
|
+
record_type: recordType,
|
|
869
|
+
disposition: 'keep',
|
|
870
|
+
ts,
|
|
871
|
+
duration_ms: undefined,
|
|
872
|
+
model: n.model ?? null,
|
|
873
|
+
input_tokens: null,
|
|
874
|
+
output_tokens: null,
|
|
875
|
+
cache_read_tokens: null,
|
|
876
|
+
cache_write_tokens: null,
|
|
877
|
+
cost_usd: null,
|
|
878
|
+
content_text: n.contentText,
|
|
879
|
+
cwd: null,
|
|
880
|
+
provenance: 'ambient',
|
|
881
|
+
},
|
|
882
|
+
});
|
|
883
|
+
entries.push({
|
|
884
|
+
targetTable: 'history_tool_call',
|
|
885
|
+
record: {
|
|
886
|
+
_messageSplitIndex: messageSplitIndex,
|
|
887
|
+
session_id: sessionId,
|
|
888
|
+
seq,
|
|
889
|
+
tool_name: n.toolName,
|
|
890
|
+
args_digest: argsDigest(n.toolArgs),
|
|
891
|
+
status: n.toolStatus || 'ok',
|
|
892
|
+
started_at: recordType === 'tool_call' ? ts : undefined,
|
|
893
|
+
completed_at: recordType === 'tool_call_update' ? ts : undefined,
|
|
894
|
+
duration_ms: n.durationMs,
|
|
895
|
+
result_bytes: undefined,
|
|
896
|
+
error_text: n.errorText,
|
|
897
|
+
},
|
|
898
|
+
});
|
|
899
|
+
return entries;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
if (
|
|
903
|
+
recordType === 'user' ||
|
|
904
|
+
recordType === 'user_message' ||
|
|
905
|
+
recordType === 'user_message_chunk' ||
|
|
906
|
+
recordType === 'message'
|
|
907
|
+
) {
|
|
908
|
+
entries.push({
|
|
909
|
+
targetTable: 'history_message',
|
|
910
|
+
record: {
|
|
911
|
+
session_id: sessionId,
|
|
912
|
+
seq,
|
|
913
|
+
role: 'user',
|
|
914
|
+
record_type: recordType || 'user_message',
|
|
915
|
+
disposition: 'keep',
|
|
916
|
+
ts,
|
|
917
|
+
duration_ms: undefined,
|
|
918
|
+
model: null,
|
|
919
|
+
input_tokens: null,
|
|
920
|
+
output_tokens: null,
|
|
921
|
+
cache_read_tokens: null,
|
|
922
|
+
cache_write_tokens: null,
|
|
923
|
+
cost_usd: null,
|
|
924
|
+
content_text: n.contentText,
|
|
925
|
+
cwd: null,
|
|
926
|
+
provenance: 'ambient',
|
|
927
|
+
},
|
|
928
|
+
});
|
|
929
|
+
return entries;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
if (recordType === 'assistant' || recordType === 'agent_message_chunk') {
|
|
933
|
+
entries.push({
|
|
934
|
+
targetTable: 'history_message',
|
|
935
|
+
record: {
|
|
936
|
+
session_id: sessionId,
|
|
937
|
+
seq,
|
|
938
|
+
role: 'assistant',
|
|
939
|
+
record_type: recordType,
|
|
940
|
+
disposition: 'keep',
|
|
941
|
+
ts,
|
|
942
|
+
duration_ms: undefined,
|
|
943
|
+
model: n.model ?? null,
|
|
944
|
+
input_tokens: null,
|
|
945
|
+
output_tokens: null,
|
|
946
|
+
cache_read_tokens: null,
|
|
947
|
+
cache_write_tokens: null,
|
|
948
|
+
cost_usd: null,
|
|
949
|
+
content_text: n.contentText,
|
|
950
|
+
cwd: null,
|
|
951
|
+
provenance: 'ambient',
|
|
952
|
+
},
|
|
953
|
+
});
|
|
954
|
+
return entries;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
if (GROK_META_TYPES.has(recordType)) {
|
|
958
|
+
entries.push({
|
|
959
|
+
targetTable: 'history_message',
|
|
960
|
+
record: {
|
|
961
|
+
session_id: sessionId,
|
|
962
|
+
seq,
|
|
963
|
+
role: 'meta',
|
|
964
|
+
record_type: recordType,
|
|
965
|
+
disposition: 'meta',
|
|
966
|
+
ts,
|
|
967
|
+
duration_ms: n.durationMs ?? null,
|
|
968
|
+
model: n.model ?? null,
|
|
969
|
+
input_tokens: null,
|
|
970
|
+
output_tokens: null,
|
|
971
|
+
cache_read_tokens: null,
|
|
972
|
+
cache_write_tokens: null,
|
|
973
|
+
cost_usd: null,
|
|
974
|
+
content_text: n.contentText,
|
|
975
|
+
cwd: null,
|
|
976
|
+
provenance: 'ambient',
|
|
977
|
+
},
|
|
978
|
+
});
|
|
979
|
+
return entries;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// True unknown: no type discriminator (or an unlisted type with empty discriminator).
|
|
983
|
+
// Capture loudly with a stable field-shape key (R6).
|
|
984
|
+
if (!n.hasTypeDiscriminator || recordType.length === 0) {
|
|
985
|
+
entries.push({
|
|
986
|
+
targetTable: 'history_message',
|
|
987
|
+
record: {
|
|
988
|
+
session_id: sessionId,
|
|
989
|
+
seq,
|
|
990
|
+
role: 'unknown',
|
|
991
|
+
record_type: stableFieldShape(raw as JsonObject),
|
|
992
|
+
disposition: 'unknown',
|
|
993
|
+
ts,
|
|
994
|
+
duration_ms: undefined,
|
|
995
|
+
model: null,
|
|
996
|
+
input_tokens: null,
|
|
997
|
+
output_tokens: null,
|
|
998
|
+
cache_read_tokens: null,
|
|
999
|
+
cache_write_tokens: null,
|
|
1000
|
+
cost_usd: null,
|
|
1001
|
+
content_text: n.contentText,
|
|
1002
|
+
cwd: null,
|
|
1003
|
+
provenance: 'ambient',
|
|
1004
|
+
},
|
|
1005
|
+
});
|
|
1006
|
+
return entries;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// Determined but unlisted type — keep as meta bookkeeping, not unknown.
|
|
1010
|
+
entries.push({
|
|
1011
|
+
targetTable: 'history_message',
|
|
1012
|
+
record: {
|
|
1013
|
+
session_id: sessionId,
|
|
1014
|
+
seq,
|
|
1015
|
+
role: 'meta',
|
|
1016
|
+
record_type: recordType,
|
|
1017
|
+
disposition: 'meta',
|
|
1018
|
+
ts,
|
|
1019
|
+
duration_ms: n.durationMs ?? null,
|
|
1020
|
+
model: n.model ?? null,
|
|
1021
|
+
input_tokens: null,
|
|
1022
|
+
output_tokens: null,
|
|
1023
|
+
cache_read_tokens: null,
|
|
1024
|
+
cache_write_tokens: null,
|
|
1025
|
+
cost_usd: null,
|
|
1026
|
+
content_text: n.contentText,
|
|
1027
|
+
cwd: null,
|
|
1028
|
+
provenance: 'ambient',
|
|
1029
|
+
},
|
|
1030
|
+
});
|
|
1031
|
+
return entries;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// ---------------------------------------------------------------------------
|
|
1035
|
+
// Shared utilities
|
|
1036
|
+
// ---------------------------------------------------------------------------
|
|
1037
|
+
|
|
1038
|
+
/** Narrow an unknown JSON value to an object bag so nested lookups type-check. */
|
|
1039
|
+
function o(value: unknown): Record<string, unknown> {
|
|
1040
|
+
return typeof value === 'object' && value !== null ? (value as Record<string, unknown>) : {};
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
/** Safe string accessor: returns the first non-empty string value. */
|
|
1044
|
+
function s(...values: readonly unknown[]): string | undefined {
|
|
1045
|
+
for (const value of values) {
|
|
1046
|
+
if (typeof value === 'string' && value.length > 0) return value;
|
|
1047
|
+
if (typeof value === 'number' && Number.isFinite(value)) return String(value);
|
|
1048
|
+
}
|
|
1049
|
+
return undefined;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function mapRole(type: unknown): string {
|
|
1053
|
+
const t = String(type ?? '').toLowerCase();
|
|
1054
|
+
if (t === 'user' || t === 'human') return 'user';
|
|
1055
|
+
if (t === 'assistant' || t === 'ai' || t === 'model') return 'assistant';
|
|
1056
|
+
if (t === 'system' || t === 'error') return 'system';
|
|
1057
|
+
return t || 'unknown';
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/** Compute cost_usd from token counts and model. Conservative estimate using known rates. */
|
|
1061
|
+
function computeCost(
|
|
1062
|
+
inputTokens?: number | null,
|
|
1063
|
+
outputTokens?: number | null,
|
|
1064
|
+
model?: string | null,
|
|
1065
|
+
): number | undefined {
|
|
1066
|
+
if (inputTokens === undefined || inputTokens === null) return undefined;
|
|
1067
|
+
if (outputTokens === undefined || outputTokens === null) return undefined;
|
|
1068
|
+
const inputRate = model?.toLowerCase().includes('claude') ? 3 : 1;
|
|
1069
|
+
const outputRate = model?.toLowerCase().includes('claude') ? 15 : 5;
|
|
1070
|
+
return (inputTokens * inputRate + outputTokens * outputRate) / 1_000_000;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
/** Extract text content from Claude-style content blocks. */
|
|
1074
|
+
function extractContentText(content: unknown): string | undefined {
|
|
1075
|
+
if (typeof content === 'string') return content;
|
|
1076
|
+
if (!Array.isArray(content)) return undefined;
|
|
1077
|
+
const parts: string[] = [];
|
|
1078
|
+
for (const block of content) {
|
|
1079
|
+
if (typeof block !== 'object' || block === null) continue;
|
|
1080
|
+
const b = block as Record<string, unknown>;
|
|
1081
|
+
if (b.type === 'text' && typeof b.text === 'string') {
|
|
1082
|
+
parts.push(b.text);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
return parts.length > 0 ? parts.join('\n') : undefined;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
/** Compute a stable field-shape key from a JSON object: sorted, lowercased, +-joined top-level keys. */
|
|
1089
|
+
export function stableFieldShape(raw: JsonObject): string {
|
|
1090
|
+
return Object.keys(raw)
|
|
1091
|
+
.map((k) => k.toLowerCase())
|
|
1092
|
+
.sort()
|
|
1093
|
+
.join('+');
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// ---------------------------------------------------------------------------
|
|
1097
|
+
// Identity field maps for the six source mappers
|
|
1098
|
+
// ---------------------------------------------------------------------------
|
|
1099
|
+
|
|
1100
|
+
/** Identity field map for the Claude mapper. */
|
|
1101
|
+
export const CLAUDE_FIELD_MAP = identityFieldMap(MESSAGE_MAPPER_KEYS.concat(TOOL_CALL_MAPPER_KEYS));
|
|
1102
|
+
/** Identity field map for the Pi mapper. */
|
|
1103
|
+
export const PI_FIELD_MAP = identityFieldMap(MESSAGE_MAPPER_KEYS.concat(TOOL_CALL_MAPPER_KEYS));
|
|
1104
|
+
/** Identity field map for the OMP mapper. */
|
|
1105
|
+
export const OMP_FIELD_MAP = identityFieldMap(MESSAGE_MAPPER_KEYS.concat(TOOL_CALL_MAPPER_KEYS));
|
|
1106
|
+
/** Identity field map for the Codex mapper. */
|
|
1107
|
+
export const CODEX_FIELD_MAP = identityFieldMap(MESSAGE_MAPPER_KEYS.concat(TOOL_CALL_MAPPER_KEYS));
|
|
1108
|
+
/** Identity field map for the AGY mapper. */
|
|
1109
|
+
export const AGY_FIELD_MAP = identityFieldMap(MESSAGE_MAPPER_KEYS.concat(TOOL_CALL_MAPPER_KEYS));
|
|
1110
|
+
/** Identity field map for the Grok mapper. */
|
|
1111
|
+
export const GROK_FIELD_MAP = identityFieldMap(MESSAGE_MAPPER_KEYS.concat(TOOL_CALL_MAPPER_KEYS));
|
|
1112
|
+
|
|
1113
|
+
// ---------------------------------------------------------------------------
|
|
1114
|
+
// Zod schemas for the six source mappers
|
|
1115
|
+
// ---------------------------------------------------------------------------
|
|
1116
|
+
|
|
1117
|
+
const passthroughSchema = z.object({}).passthrough();
|
|
1118
|
+
|
|
1119
|
+
/** Passthrough Zod schema for the Claude mapper. */
|
|
1120
|
+
export const CLAUDE_SCHEMA = passthroughSchema;
|
|
1121
|
+
/** Passthrough Zod schema for the Pi mapper. */
|
|
1122
|
+
export const PI_SCHEMA = passthroughSchema;
|
|
1123
|
+
/** Passthrough Zod schema for the OMP mapper. */
|
|
1124
|
+
export const OMP_SCHEMA = passthroughSchema;
|
|
1125
|
+
/** Passthrough Zod schema for the Codex mapper. */
|
|
1126
|
+
export const CODEX_SCHEMA = passthroughSchema;
|
|
1127
|
+
/** Passthrough Zod schema for the AGY mapper. */
|
|
1128
|
+
export const AGY_SCHEMA = passthroughSchema;
|
|
1129
|
+
/** Passthrough Zod schema for the Grok mapper. */
|
|
1130
|
+
export const GROK_SCHEMA = passthroughSchema;
|