@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 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). */
@@ -1 +1 @@
1
- {"version":3,"file":"mappers.d.ts","sourceRoot":"","sources":["../src/mappers.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"mappers.d.ts","sourceRoot":"","sources":["../src/mappers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAmExE,kFAAkF;AAClF,wBAAgB,UAAU,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAGhD;AAwCD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAMhG;AAMD,yEAAyE;AACzE,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,UAAU,EAAE,CA6G/E;AAMD,gEAAgE;AAChE,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,SAAS,UAAU,EAAE,CAqHvG;AAmBD,kEAAkE;AAClE,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,SAAS,UAAU,EAAE,CA+GxG;AAoBD,mFAAmF;AACnF,MAAM,WAAW,mBAAmB;IAChC,yEAAyE;IACzE,UAAU,EAAE,MAAM,CAAC;IACnB,wEAAwE;IACxE,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,yDAAyD;IACzD,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;CACnC;AAED,+FAA+F;AAC/F,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CASrE;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,mBAAmB,GAAG,IAAI,CAS5F;AAMD,mEAAmE;AACnE,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,UAAU,EAAE,CAkG9E;AAMD,gFAAgF;AAChF,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,UAAU,EAAE,CAiH5E;AAMD,gGAAgG;AAChG,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,SAAS,UAAU,EAAE,CAiF3G;AAyMD,kEAAkE;AAClE,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,UAAU,EAAE,CA4S7E;AAwDD,wGAAwG;AACxG,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAKxD;AAMD,gDAAgD;AAChD,eAAO,MAAM,gBAAgB,wBAAsE,CAAC;AACpG,4CAA4C;AAC5C,eAAO,MAAM,YAAY,wBAAsE,CAAC;AAChG,6CAA6C;AAC7C,eAAO,MAAM,aAAa,wBAAsE,CAAC;AACjG,+CAA+C;AAC/C,eAAO,MAAM,eAAe,wBAAsE,CAAC;AACnG,6CAA6C;AAC7C,eAAO,MAAM,aAAa,wBAAsE,CAAC;AACjG,8CAA8C;AAC9C,eAAO,MAAM,cAAc,wBAAsE,CAAC;AAClG,gDAAgD;AAChD,eAAO,MAAM,gBAAgB,wBAAsE,CAAC;AAQpG,oDAAoD;AACpD,eAAO,MAAM,aAAa,gCAAoB,CAAC;AAC/C,gDAAgD;AAChD,eAAO,MAAM,SAAS,gCAAoB,CAAC;AAC3C,iDAAiD;AACjD,eAAO,MAAM,UAAU,gCAAoB,CAAC;AAC5C,mDAAmD;AACnD,eAAO,MAAM,YAAY,gCAAoB,CAAC;AAC9C,iDAAiD;AACjD,eAAO,MAAM,UAAU,gCAAoB,CAAC;AAC5C,kDAAkD;AAClD,eAAO,MAAM,WAAW,gCAAoB,CAAC;AAC7C,oDAAoD;AACpD,eAAO,MAAM,aAAa,gCAAoB,CAAC"}
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
- pi: ['todo'],
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 r = raw;
232
- const sessionId = s(r.id, o(r.session).id) ?? 'unknown';
233
- const seq = typeof r.seq === 'number' ? r.seq : 0;
234
- const ts = s(r.ts, r.timestamp, r.createdAt) ?? new Date(0).toISOString();
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 ?? r.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: undefined,
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(r.content, r.text, msg?.content) ?? null,
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 toolCall blocks in assistant content
270
- if (Array.isArray(r.content) && role === 'assistant') {
271
- for (const block of r.content) {
272
- if (block?.toolCall) {
273
- const tc = block.toolCall;
274
- entries.push({
275
- targetTable: 'history_tool_call',
276
- record: {
277
- _messageSplitIndex: messageSplitIndex,
278
- session_id: sessionId,
279
- seq,
280
- tool_name: String(tc.name ?? ''),
281
- args_digest: argsDigest(tc.input ?? tc.arguments),
282
- args_raw: maybeArgsRaw('pi', String(tc.name ?? ''), tc.input ?? tc.arguments),
283
- status: 'ok',
284
- started_at: undefined,
285
- completed_at: undefined,
286
- duration_ms: undefined,
287
- result_bytes: undefined,
288
- error_text: undefined,
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;AAe/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"}
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.35",
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.35",
51
- "@gobing-ai/ts-runtime": "^0.4.35",
52
- "@gobing-ai/ts-utils": "^0.4.35",
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
- pi: ['todo'],
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 r = raw as Record<string, unknown>;
247
- const sessionId = s(r.id, o(r.session).id) ?? 'unknown';
248
- const seq = typeof r.seq === 'number' ? r.seq : 0;
249
- const ts = s(r.ts, r.timestamp, r.createdAt) ?? new Date(0).toISOString();
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 ?? r.cost) as Record<string, unknown> | undefined;
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: undefined,
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(r.content, r.text, msg?.content) ?? null,
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 toolCall blocks in assistant content
289
- if (Array.isArray(r.content) && role === 'assistant') {
290
- for (const block of r.content as Record<string, unknown>[]) {
291
- if (block?.toolCall) {
292
- const tc = block.toolCall as Record<string, unknown>;
293
- entries.push({
294
- targetTable: 'history_tool_call',
295
- record: {
296
- _messageSplitIndex: messageSplitIndex,
297
- session_id: sessionId,
298
- seq,
299
- tool_name: String(tc.name ?? ''),
300
- args_digest: argsDigest(tc.input ?? tc.arguments),
301
- args_raw: maybeArgsRaw('pi', String(tc.name ?? ''), tc.input ?? tc.arguments),
302
- status: 'ok',
303
- started_at: undefined,
304
- completed_at: undefined,
305
- duration_ms: undefined,
306
- result_bytes: undefined,
307
- error_text: undefined,
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
  // ---------------------------------------------------------------------------
@@ -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),