@gobing-ai/ts-llm-jsonl-importer 0.4.35 → 0.4.37
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 +6 -1
- package/dist/mappers.d.ts.map +1 -1
- package/dist/mappers.js +101 -41
- package/dist/opencode-importer.d.ts.map +1 -1
- package/dist/opencode-importer.js +2 -0
- package/package.json +4 -4
- package/src/mappers.ts +104 -42
- package/src/opencode-importer.ts +2 -0
package/dist/mappers.d.ts
CHANGED
|
@@ -2,10 +2,15 @@ import { z } from 'zod';
|
|
|
2
2
|
import type { JsonObject, SplitEntry, TransformContext } from './types';
|
|
3
3
|
/** Compute args_digest: sha256 of stable-jsonified, key-sorted, redacted args. */
|
|
4
4
|
export declare function argsDigest(args: unknown): string;
|
|
5
|
+
/**
|
|
6
|
+
* Return JSON-stringified raw args when the tool is a todo-writing tool for its source,
|
|
7
|
+
* otherwise undefined. Codex `arguments` arrives as a JSON string already — store as-is.
|
|
8
|
+
*/
|
|
9
|
+
export declare function maybeArgsRaw(source: string, toolName: string, args: unknown): string | undefined;
|
|
5
10
|
/** Map a Claude Code JSONL record into one or more ETL split entries. */
|
|
6
11
|
export declare function claudeSplit(raw: Record<string, unknown>): readonly SplitEntry[];
|
|
7
12
|
/** Map a Pi JSONL record into one or more ETL split entries. */
|
|
8
|
-
export declare function piSplit(raw: unknown): readonly SplitEntry[];
|
|
13
|
+
export declare function piSplit(raw: Record<string, unknown>, context?: TransformContext): readonly SplitEntry[];
|
|
9
14
|
/** Map an OMP JSONL record into one or more ETL split entries. */
|
|
10
15
|
export declare function ompSplit(raw: Record<string, unknown>, context?: TransformContext): readonly SplitEntry[];
|
|
11
16
|
/** Timing signals carried by an OMP toolResult message envelope (task 0564 R1). */
|
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;
|
|
1
|
+
{"version":3,"file":"mappers.d.ts","sourceRoot":"","sources":["../src/mappers.ts"],"names":[],"mappings":"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"}
|
package/dist/mappers.js
CHANGED
|
@@ -89,14 +89,17 @@ function redactArgs(args) {
|
|
|
89
89
|
*
|
|
90
90
|
* Evidence: 0489 R4 confirmed omp/pi/claude; codex/grok/agy probed task 0553 R3 from real
|
|
91
91
|
* session JSONL. agy has no on-disk session format (VS Code fork); gemini deferred by the
|
|
92
|
-
* 2026-08-06 source-support ruling.
|
|
92
|
+
* 2026-08-06 source-support ruling. Task 0578 R3: omp `todo_write`, pi `manage_todo_list`,
|
|
93
|
+
* opencode `todowrite`/`todoread` measured live in .spur/spur.db (3,237/21, 1,043, 379/2
|
|
94
|
+
* rows with NULL args_raw).
|
|
93
95
|
*/
|
|
94
96
|
const TODO_TOOL_ALLOWLIST = {
|
|
95
97
|
claude: ['TodoWrite'],
|
|
96
|
-
|
|
97
|
-
omp: ['TodoWrite', 'todo'],
|
|
98
|
+
opencode: ['todowrite', 'todoread'],
|
|
98
99
|
codex: ['update_plan'],
|
|
100
|
+
omp: ['TodoWrite', 'todo', 'todo_write'],
|
|
99
101
|
grok: ['todo_write'],
|
|
102
|
+
pi: ['todo', 'manage_todo_list'],
|
|
100
103
|
agy: [],
|
|
101
104
|
gemini: [],
|
|
102
105
|
};
|
|
@@ -104,7 +107,7 @@ const TODO_TOOL_ALLOWLIST = {
|
|
|
104
107
|
* Return JSON-stringified raw args when the tool is a todo-writing tool for its source,
|
|
105
108
|
* otherwise undefined. Codex `arguments` arrives as a JSON string already — store as-is.
|
|
106
109
|
*/
|
|
107
|
-
function maybeArgsRaw(source, toolName, args) {
|
|
110
|
+
export function maybeArgsRaw(source, toolName, args) {
|
|
108
111
|
const allow = TODO_TOOL_ALLOWLIST[source];
|
|
109
112
|
if (allow === undefined || !allow.includes(toolName))
|
|
110
113
|
return undefined;
|
|
@@ -226,24 +229,62 @@ export function claudeSplit(raw) {
|
|
|
226
229
|
// Pi mapper
|
|
227
230
|
// ---------------------------------------------------------------------------
|
|
228
231
|
/** Map a Pi JSONL record into one or more ETL split entries. */
|
|
229
|
-
export function piSplit(raw) {
|
|
232
|
+
export function piSplit(raw, context) {
|
|
233
|
+
// Pi session files are top-level event envelopes: `{type, id, parentId, timestamp,
|
|
234
|
+
// message: {role, content, model, usage, provider, ...}}`. Newer records carry `recordType`
|
|
235
|
+
// instead of `type` with `role`/`text` at the top level. The top-level `id` is an event id,
|
|
236
|
+
// never a session id; the session key and sequence come from the source file (context).
|
|
237
|
+
const msg = raw.message;
|
|
238
|
+
const recordType = String(raw.type ?? raw.recordType ?? '');
|
|
239
|
+
const sessionId = sessionIdFromContext(context, raw);
|
|
240
|
+
const seq = context?.sourceLine ?? (typeof raw.seq === 'number' ? raw.seq : 0);
|
|
241
|
+
// Prefer the ISO `timestamp`; a numeric epoch-ms `ts` is converted, never stringified.
|
|
242
|
+
const ts = s(raw.timestamp, raw.createdAt) ??
|
|
243
|
+
(typeof raw.ts === 'number' && Number.isFinite(raw.ts) ? new Date(raw.ts).toISOString() : s(raw.ts)) ??
|
|
244
|
+
new Date(0).toISOString();
|
|
245
|
+
// Meta lifecycle/custom records collapse to one meta row keyed by the source session,
|
|
246
|
+
// never the unique event id, and never a guessed role.
|
|
247
|
+
if (recordType === 'title' ||
|
|
248
|
+
recordType === 'title_change' ||
|
|
249
|
+
recordType === 'service_tier_change' ||
|
|
250
|
+
recordType === 'session_info' ||
|
|
251
|
+
recordType === 'ttsr_injection' ||
|
|
252
|
+
recordType === 'session_init' ||
|
|
253
|
+
recordType === 'session' ||
|
|
254
|
+
recordType === 'model_change' ||
|
|
255
|
+
recordType === 'thinking_level_change' ||
|
|
256
|
+
recordType === 'compaction' ||
|
|
257
|
+
recordType === 'custom' ||
|
|
258
|
+
recordType === 'custom_message' ||
|
|
259
|
+
recordType.startsWith('custom.')) {
|
|
260
|
+
return [
|
|
261
|
+
{
|
|
262
|
+
targetTable: 'history_message',
|
|
263
|
+
record: {
|
|
264
|
+
session_id: sessionId,
|
|
265
|
+
seq,
|
|
266
|
+
role: 'meta',
|
|
267
|
+
record_type: recordType,
|
|
268
|
+
disposition: 'meta',
|
|
269
|
+
ts,
|
|
270
|
+
provenance: 'ambient',
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
];
|
|
274
|
+
}
|
|
230
275
|
const entries = [];
|
|
231
|
-
const
|
|
232
|
-
const
|
|
233
|
-
const
|
|
234
|
-
const
|
|
235
|
-
const role = mapRole(r.type ?? r.role);
|
|
236
|
-
const recordType = String(r.type ?? '');
|
|
237
|
-
const model = s(r.model, o(r.message).model);
|
|
238
|
-
const cwd = s(r.cwd, r.dir);
|
|
239
|
-
const msg = r.message;
|
|
240
|
-
const usage = msg?.usage;
|
|
276
|
+
const role = piRole(msg?.role ?? raw.role ?? recordType);
|
|
277
|
+
const model = s(raw.model, o(msg).model);
|
|
278
|
+
const cwd = s(raw.cwd, raw.dir);
|
|
279
|
+
const usage = (msg?.usage ?? raw.usage);
|
|
241
280
|
const inputTokens = (usage?.input ?? usage?.input_tokens ?? undefined);
|
|
242
281
|
const outputTokens = (usage?.output ?? usage?.output_tokens ?? undefined);
|
|
243
282
|
const cacheRead = (usage?.cacheRead ?? usage?.cache_read_tokens ?? undefined);
|
|
244
283
|
const cacheWrite = (usage?.cacheWrite ?? usage?.cache_write_tokens ?? undefined);
|
|
245
|
-
const costObj = (msg?.cost ??
|
|
284
|
+
const costObj = (msg?.cost ?? raw.cost);
|
|
246
285
|
const costUsd = typeof costObj?.total === 'number' ? costObj.total : computeCost(inputTokens, outputTokens, model);
|
|
286
|
+
const contentBlocks = msg?.content ?? raw.content;
|
|
287
|
+
const durationMs = typeof msg?.duration === 'number' && Number.isFinite(msg.duration) ? msg.duration : undefined;
|
|
247
288
|
const messageSplitIndex = entries.length;
|
|
248
289
|
entries.push({
|
|
249
290
|
targetTable: 'history_message',
|
|
@@ -254,45 +295,64 @@ export function piSplit(raw) {
|
|
|
254
295
|
record_type: recordType,
|
|
255
296
|
disposition: 'keep',
|
|
256
297
|
ts,
|
|
257
|
-
duration_ms:
|
|
298
|
+
duration_ms: durationMs,
|
|
258
299
|
model: model ?? null,
|
|
259
300
|
input_tokens: inputTokens ?? null,
|
|
260
301
|
output_tokens: outputTokens ?? null,
|
|
261
302
|
cache_read_tokens: cacheRead ?? null,
|
|
262
303
|
cache_write_tokens: cacheWrite ?? null,
|
|
263
304
|
cost_usd: costUsd ?? null,
|
|
264
|
-
content_text: s(
|
|
305
|
+
content_text: extractContentText(contentBlocks) ?? s(raw.content, raw.text, msg?.content) ?? null,
|
|
265
306
|
cwd: cwd ?? null,
|
|
266
307
|
provenance: 'ambient',
|
|
267
308
|
},
|
|
268
309
|
});
|
|
269
|
-
// Tool calls from
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
310
|
+
// Tool calls from content blocks in assistant messages. normalizeOmpToolCall handles both
|
|
311
|
+
// the legacy nested `{toolCall: {...}}` block and pi's flat `{type: "toolCall", id, name,
|
|
312
|
+
// arguments}` block.
|
|
313
|
+
if (Array.isArray(contentBlocks) && role === 'assistant') {
|
|
314
|
+
for (const block of contentBlocks) {
|
|
315
|
+
const call = normalizeOmpToolCall(block);
|
|
316
|
+
if (call === null)
|
|
317
|
+
continue;
|
|
318
|
+
entries.push({
|
|
319
|
+
targetTable: 'history_tool_call',
|
|
320
|
+
record: {
|
|
321
|
+
_messageSplitIndex: messageSplitIndex,
|
|
322
|
+
session_id: sessionId,
|
|
323
|
+
seq,
|
|
324
|
+
tool_name: String(call.name ?? ''),
|
|
325
|
+
// The tool's own call id is the exact join key a toolResult's `toolCallId`
|
|
326
|
+
// matches (see ompSplit / task 0564 R1).
|
|
327
|
+
call_id: s(call.id),
|
|
328
|
+
args_digest: argsDigest(call.input ?? call.arguments),
|
|
329
|
+
args_raw: maybeArgsRaw('pi', String(call.name ?? ''), call.input ?? call.arguments),
|
|
330
|
+
status: 'ok',
|
|
331
|
+
started_at: undefined,
|
|
332
|
+
completed_at: undefined,
|
|
333
|
+
duration_ms: undefined,
|
|
334
|
+
result_bytes: undefined,
|
|
335
|
+
error_text: undefined,
|
|
336
|
+
},
|
|
337
|
+
});
|
|
292
338
|
}
|
|
293
339
|
}
|
|
294
340
|
return entries;
|
|
295
341
|
}
|
|
342
|
+
/**
|
|
343
|
+
* Canonicalize a pi message role. pi tool results and bash executions ride user turns (Claude
|
|
344
|
+
* protocol shape); everything unrecognized clamps to `unknown` so pi record types never leak
|
|
345
|
+
* into the role column (task 0577 AC2).
|
|
346
|
+
*/
|
|
347
|
+
function piRole(type) {
|
|
348
|
+
const t = String(type ?? '').toLowerCase();
|
|
349
|
+
if (t === 'toolresult' || t === 'tool_result' || t === 'bashexecution' || t === 'tool')
|
|
350
|
+
return 'user';
|
|
351
|
+
const mapped = mapRole(t);
|
|
352
|
+
if (mapped === 'user' || mapped === 'assistant' || mapped === 'system')
|
|
353
|
+
return mapped;
|
|
354
|
+
return 'unknown';
|
|
355
|
+
}
|
|
296
356
|
// ---------------------------------------------------------------------------
|
|
297
357
|
// OMP mapper (near-identical to pi)
|
|
298
358
|
// ---------------------------------------------------------------------------
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"opencode-importer.d.ts","sourceRoot":"","sources":["../src/opencode-importer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,SAAS,EAAkB,MAAM,kBAAkB,CAAC;AACnF,OAAO,EAGH,KAAK,UAAU,EAEf,KAAK,YAAY,EACpB,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"opencode-importer.d.ts","sourceRoot":"","sources":["../src/opencode-importer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,SAAS,EAAkB,MAAM,kBAAkB,CAAC;AACnF,OAAO,EAGH,KAAK,UAAU,EAEf,KAAK,YAAY,EACpB,MAAM,uBAAuB,CAAC;AAgB/B,OAAO,KAAK,EAAe,UAAU,EAAE,YAAY,EAAgC,aAAa,EAAE,MAAM,SAAS,CAAC;AAYlH,8EAA8E;AAC9E,MAAM,WAAW,qBAAqB;IAClC,EAAE,EAAE,SAAS,CAAC;IACd,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,cAAc,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;IAC1C,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CACpB;AAED,wFAAwF;AACxF,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,YAAY,CAAC,CAoI7F"}
|
|
@@ -2,6 +2,7 @@ import { createDbAdapter } from '@gobing-ai/ts-db';
|
|
|
2
2
|
import { ambientRuntimePaths, createNodeFileSystem, joinPath, } from '@gobing-ai/ts-runtime';
|
|
3
3
|
import { sha256 } from './hash.js';
|
|
4
4
|
import { applyHistoryImportSchema, deleteCheckpointOperation, openCodeBulkWriteOperations, openCodeDeleteOperations, readOpenCodeExistingEntries, readOpenCodeMessages, readOpenCodeParts, } from './jsonl-importer-dao.js';
|
|
5
|
+
import { maybeArgsRaw } from './mappers.js';
|
|
5
6
|
import { redactRecord } from './redaction.js';
|
|
6
7
|
const SOURCE = 'opencode';
|
|
7
8
|
const PAGE_SIZE = 250;
|
|
@@ -202,6 +203,7 @@ function prepareEntries(row, data, parts, sourceFile, redactionRules) {
|
|
|
202
203
|
session_id: row.session_id,
|
|
203
204
|
seq: started ?? created,
|
|
204
205
|
tool_name: stringValue(part.tool) ?? 'unknown',
|
|
206
|
+
args_raw: maybeArgsRaw(SOURCE, stringValue(part.tool) ?? 'unknown', state.input),
|
|
205
207
|
args_digest: sha256(state.input ?? null),
|
|
206
208
|
status,
|
|
207
209
|
started_at: started === undefined ? null : isoTime(started),
|
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.37",
|
|
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.37",
|
|
51
|
+
"@gobing-ai/ts-runtime": "^0.4.37",
|
|
52
|
+
"@gobing-ai/ts-utils": "^0.4.37",
|
|
53
53
|
"zod": "^4.1.0"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
package/src/mappers.ts
CHANGED
|
@@ -96,14 +96,17 @@ function redactArgs(args: unknown): unknown {
|
|
|
96
96
|
*
|
|
97
97
|
* Evidence: 0489 R4 confirmed omp/pi/claude; codex/grok/agy probed task 0553 R3 from real
|
|
98
98
|
* session JSONL. agy has no on-disk session format (VS Code fork); gemini deferred by the
|
|
99
|
-
* 2026-08-06 source-support ruling.
|
|
99
|
+
* 2026-08-06 source-support ruling. Task 0578 R3: omp `todo_write`, pi `manage_todo_list`,
|
|
100
|
+
* opencode `todowrite`/`todoread` measured live in .spur/spur.db (3,237/21, 1,043, 379/2
|
|
101
|
+
* rows with NULL args_raw).
|
|
100
102
|
*/
|
|
101
103
|
const TODO_TOOL_ALLOWLIST: Readonly<Record<string, readonly string[]>> = {
|
|
102
104
|
claude: ['TodoWrite'],
|
|
103
|
-
|
|
104
|
-
omp: ['TodoWrite', 'todo'],
|
|
105
|
+
opencode: ['todowrite', 'todoread'],
|
|
105
106
|
codex: ['update_plan'],
|
|
107
|
+
omp: ['TodoWrite', 'todo', 'todo_write'],
|
|
106
108
|
grok: ['todo_write'],
|
|
109
|
+
pi: ['todo', 'manage_todo_list'],
|
|
107
110
|
agy: [],
|
|
108
111
|
gemini: [],
|
|
109
112
|
};
|
|
@@ -112,7 +115,7 @@ const TODO_TOOL_ALLOWLIST: Readonly<Record<string, readonly string[]>> = {
|
|
|
112
115
|
* Return JSON-stringified raw args when the tool is a todo-writing tool for its source,
|
|
113
116
|
* otherwise undefined. Codex `arguments` arrives as a JSON string already — store as-is.
|
|
114
117
|
*/
|
|
115
|
-
function maybeArgsRaw(source: string, toolName: string, args: unknown): string | undefined {
|
|
118
|
+
export function maybeArgsRaw(source: string, toolName: string, args: unknown): string | undefined {
|
|
116
119
|
const allow = TODO_TOOL_ALLOWLIST[source];
|
|
117
120
|
if (allow === undefined || !allow.includes(toolName)) return undefined;
|
|
118
121
|
if (typeof args === 'string') return args;
|
|
@@ -241,25 +244,67 @@ export function claudeSplit(raw: Record<string, unknown>): readonly SplitEntry[]
|
|
|
241
244
|
// ---------------------------------------------------------------------------
|
|
242
245
|
|
|
243
246
|
/** Map a Pi JSONL record into one or more ETL split entries. */
|
|
244
|
-
export function piSplit(raw: unknown): readonly SplitEntry[] {
|
|
247
|
+
export function piSplit(raw: Record<string, unknown>, context?: TransformContext): readonly SplitEntry[] {
|
|
248
|
+
// Pi session files are top-level event envelopes: `{type, id, parentId, timestamp,
|
|
249
|
+
// message: {role, content, model, usage, provider, ...}}`. Newer records carry `recordType`
|
|
250
|
+
// instead of `type` with `role`/`text` at the top level. The top-level `id` is an event id,
|
|
251
|
+
// never a session id; the session key and sequence come from the source file (context).
|
|
252
|
+
const msg = raw.message as Record<string, unknown> | undefined;
|
|
253
|
+
const recordType = String(raw.type ?? raw.recordType ?? '');
|
|
254
|
+
const sessionId = sessionIdFromContext(context, raw);
|
|
255
|
+
const seq = context?.sourceLine ?? (typeof raw.seq === 'number' ? raw.seq : 0);
|
|
256
|
+
// Prefer the ISO `timestamp`; a numeric epoch-ms `ts` is converted, never stringified.
|
|
257
|
+
const ts =
|
|
258
|
+
s(raw.timestamp, raw.createdAt) ??
|
|
259
|
+
(typeof raw.ts === 'number' && Number.isFinite(raw.ts) ? new Date(raw.ts).toISOString() : s(raw.ts)) ??
|
|
260
|
+
new Date(0).toISOString();
|
|
261
|
+
|
|
262
|
+
// Meta lifecycle/custom records collapse to one meta row keyed by the source session,
|
|
263
|
+
// never the unique event id, and never a guessed role.
|
|
264
|
+
if (
|
|
265
|
+
recordType === 'title' ||
|
|
266
|
+
recordType === 'title_change' ||
|
|
267
|
+
recordType === 'service_tier_change' ||
|
|
268
|
+
recordType === 'session_info' ||
|
|
269
|
+
recordType === 'ttsr_injection' ||
|
|
270
|
+
recordType === 'session_init' ||
|
|
271
|
+
recordType === 'session' ||
|
|
272
|
+
recordType === 'model_change' ||
|
|
273
|
+
recordType === 'thinking_level_change' ||
|
|
274
|
+
recordType === 'compaction' ||
|
|
275
|
+
recordType === 'custom' ||
|
|
276
|
+
recordType === 'custom_message' ||
|
|
277
|
+
recordType.startsWith('custom.')
|
|
278
|
+
) {
|
|
279
|
+
return [
|
|
280
|
+
{
|
|
281
|
+
targetTable: 'history_message',
|
|
282
|
+
record: {
|
|
283
|
+
session_id: sessionId,
|
|
284
|
+
seq,
|
|
285
|
+
role: 'meta',
|
|
286
|
+
record_type: recordType,
|
|
287
|
+
disposition: 'meta',
|
|
288
|
+
ts,
|
|
289
|
+
provenance: 'ambient',
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
];
|
|
293
|
+
}
|
|
294
|
+
|
|
245
295
|
const entries: SplitEntry[] = [];
|
|
246
|
-
const
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
const
|
|
250
|
-
const role = mapRole(r.type ?? r.role);
|
|
251
|
-
const recordType = String(r.type ?? '');
|
|
252
|
-
const model = s(r.model, o(r.message).model);
|
|
253
|
-
const cwd = s(r.cwd, r.dir);
|
|
254
|
-
|
|
255
|
-
const msg = r.message as Record<string, unknown> | undefined;
|
|
256
|
-
const usage = msg?.usage as Record<string, unknown> | undefined;
|
|
296
|
+
const role = piRole(msg?.role ?? raw.role ?? recordType);
|
|
297
|
+
const model = s(raw.model, o(msg).model);
|
|
298
|
+
const cwd = s(raw.cwd, raw.dir);
|
|
299
|
+
const usage = (msg?.usage ?? raw.usage) as Record<string, unknown> | undefined;
|
|
257
300
|
const inputTokens = (usage?.input ?? usage?.input_tokens ?? undefined) as number | undefined;
|
|
258
301
|
const outputTokens = (usage?.output ?? usage?.output_tokens ?? undefined) as number | undefined;
|
|
259
302
|
const cacheRead = (usage?.cacheRead ?? usage?.cache_read_tokens ?? undefined) as number | undefined;
|
|
260
303
|
const cacheWrite = (usage?.cacheWrite ?? usage?.cache_write_tokens ?? undefined) as number | undefined;
|
|
261
|
-
const costObj = (msg?.cost ??
|
|
304
|
+
const costObj = (msg?.cost ?? raw.cost) as Record<string, unknown> | undefined;
|
|
262
305
|
const costUsd = typeof costObj?.total === 'number' ? costObj.total : computeCost(inputTokens, outputTokens, model);
|
|
306
|
+
const contentBlocks = msg?.content ?? raw.content;
|
|
307
|
+
const durationMs = typeof msg?.duration === 'number' && Number.isFinite(msg.duration) ? msg.duration : undefined;
|
|
263
308
|
|
|
264
309
|
const messageSplitIndex = entries.length;
|
|
265
310
|
|
|
@@ -272,48 +317,65 @@ export function piSplit(raw: unknown): readonly SplitEntry[] {
|
|
|
272
317
|
record_type: recordType,
|
|
273
318
|
disposition: 'keep',
|
|
274
319
|
ts,
|
|
275
|
-
duration_ms:
|
|
320
|
+
duration_ms: durationMs,
|
|
276
321
|
model: model ?? null,
|
|
277
322
|
input_tokens: inputTokens ?? null,
|
|
278
323
|
output_tokens: outputTokens ?? null,
|
|
279
324
|
cache_read_tokens: cacheRead ?? null,
|
|
280
325
|
cache_write_tokens: cacheWrite ?? null,
|
|
281
326
|
cost_usd: costUsd ?? null,
|
|
282
|
-
content_text: s(
|
|
327
|
+
content_text: extractContentText(contentBlocks) ?? s(raw.content, raw.text, msg?.content) ?? null,
|
|
283
328
|
cwd: cwd ?? null,
|
|
284
329
|
provenance: 'ambient',
|
|
285
330
|
},
|
|
286
331
|
});
|
|
287
332
|
|
|
288
|
-
// Tool calls from
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
333
|
+
// Tool calls from content blocks in assistant messages. normalizeOmpToolCall handles both
|
|
334
|
+
// the legacy nested `{toolCall: {...}}` block and pi's flat `{type: "toolCall", id, name,
|
|
335
|
+
// arguments}` block.
|
|
336
|
+
if (Array.isArray(contentBlocks) && role === 'assistant') {
|
|
337
|
+
for (const block of contentBlocks as Record<string, unknown>[]) {
|
|
338
|
+
const call = normalizeOmpToolCall(block);
|
|
339
|
+
if (call === null) continue;
|
|
340
|
+
entries.push({
|
|
341
|
+
targetTable: 'history_tool_call',
|
|
342
|
+
record: {
|
|
343
|
+
_messageSplitIndex: messageSplitIndex,
|
|
344
|
+
session_id: sessionId,
|
|
345
|
+
seq,
|
|
346
|
+
tool_name: String(call.name ?? ''),
|
|
347
|
+
// The tool's own call id is the exact join key a toolResult's `toolCallId`
|
|
348
|
+
// matches (see ompSplit / task 0564 R1).
|
|
349
|
+
call_id: s(call.id),
|
|
350
|
+
args_digest: argsDigest(call.input ?? call.arguments),
|
|
351
|
+
args_raw: maybeArgsRaw('pi', String(call.name ?? ''), call.input ?? call.arguments),
|
|
352
|
+
status: 'ok',
|
|
353
|
+
started_at: undefined,
|
|
354
|
+
completed_at: undefined,
|
|
355
|
+
duration_ms: undefined,
|
|
356
|
+
result_bytes: undefined,
|
|
357
|
+
error_text: undefined,
|
|
358
|
+
},
|
|
359
|
+
});
|
|
311
360
|
}
|
|
312
361
|
}
|
|
313
362
|
|
|
314
363
|
return entries;
|
|
315
364
|
}
|
|
316
365
|
|
|
366
|
+
/**
|
|
367
|
+
* Canonicalize a pi message role. pi tool results and bash executions ride user turns (Claude
|
|
368
|
+
* protocol shape); everything unrecognized clamps to `unknown` so pi record types never leak
|
|
369
|
+
* into the role column (task 0577 AC2).
|
|
370
|
+
*/
|
|
371
|
+
function piRole(type: unknown): string {
|
|
372
|
+
const t = String(type ?? '').toLowerCase();
|
|
373
|
+
if (t === 'toolresult' || t === 'tool_result' || t === 'bashexecution' || t === 'tool') return 'user';
|
|
374
|
+
const mapped = mapRole(t);
|
|
375
|
+
if (mapped === 'user' || mapped === 'assistant' || mapped === 'system') return mapped;
|
|
376
|
+
return 'unknown';
|
|
377
|
+
}
|
|
378
|
+
|
|
317
379
|
// ---------------------------------------------------------------------------
|
|
318
380
|
// OMP mapper (near-identical to pi)
|
|
319
381
|
// ---------------------------------------------------------------------------
|
package/src/opencode-importer.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
readOpenCodeMessages,
|
|
20
20
|
readOpenCodeParts,
|
|
21
21
|
} from './jsonl-importer-dao';
|
|
22
|
+
import { maybeArgsRaw } from './mappers';
|
|
22
23
|
import { redactRecord } from './redaction';
|
|
23
24
|
import type { ImportIssue, ImportMode, ImportResult, JsonObject, ReconcileSummary, RedactionRule } from './types';
|
|
24
25
|
|
|
@@ -262,6 +263,7 @@ function prepareEntries(
|
|
|
262
263
|
session_id: row.session_id,
|
|
263
264
|
seq: started ?? created,
|
|
264
265
|
tool_name: stringValue(part.tool) ?? 'unknown',
|
|
266
|
+
args_raw: maybeArgsRaw(SOURCE, stringValue(part.tool) ?? 'unknown', state.input),
|
|
265
267
|
args_digest: sha256(state.input ?? null),
|
|
266
268
|
status,
|
|
267
269
|
started_at: started === undefined ? null : isoTime(started),
|