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