@gobing-ai/ts-llm-jsonl-importer 0.4.25 → 0.4.26

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
@@ -1,11 +1,13 @@
1
1
  import { z } from 'zod';
2
2
  import type { JsonObject, SplitEntry, TransformContext } from './types';
3
+ /** Compute args_digest: sha256 of stable-jsonified, key-sorted, redacted args. */
4
+ export declare function argsDigest(args: unknown): string;
3
5
  /** Map a Claude Code JSONL record into one or more ETL split entries. */
4
6
  export declare function claudeSplit(raw: Record<string, unknown>): readonly SplitEntry[];
5
7
  /** Map a Pi JSONL record into one or more ETL split entries. */
6
8
  export declare function piSplit(raw: unknown): readonly SplitEntry[];
7
9
  /** Map an OMP JSONL record into one or more ETL split entries. */
8
- export declare function ompSplit(raw: Record<string, unknown>): readonly SplitEntry[];
10
+ export declare function ompSplit(raw: Record<string, unknown>, context?: TransformContext): readonly SplitEntry[];
9
11
  /** Map a Codex JSONL record into one or more ETL split entries. */
10
12
  export declare function codexSplit(raw: Record<string, unknown>): readonly SplitEntry[];
11
13
  /** Map an Antigravity (AGY) JSONL record into one or more ETL split entries. */
@@ -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;AA2FxE,yEAAyE;AACzE,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,UAAU,EAAE,CA4G/E;AAMD,gEAAgE;AAChE,wBAAgB,OAAO,CAAC,GAAG,EAAE,OAAO,GAAG,SAAS,UAAU,EAAE,CAsE3D;AAMD,kEAAkE;AAClE,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,UAAU,EAAE,CA4F5E;AAMD,mEAAmE;AACnE,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,UAAU,EAAE,CAiG9E;AAMD,gFAAgF;AAChF,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,UAAU,EAAE,CAgH5E;AAMD,gGAAgG;AAChG,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,SAAS,UAAU,EAAE,CAgF3G;AAyMD,kEAAkE;AAClE,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,UAAU,EAAE,CAyS7E;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"}
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;AA+DxE,kFAAkF;AAClF,wBAAgB,UAAU,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAGhD;AAwBD,yEAAyE;AACzE,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,UAAU,EAAE,CA4G/E;AAMD,gEAAgE;AAChE,wBAAgB,OAAO,CAAC,GAAG,EAAE,OAAO,GAAG,SAAS,UAAU,EAAE,CAsE3D;AAMD,kEAAkE;AAClE,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,SAAS,UAAU,EAAE,CA0GxG;AAoBD,mEAAmE;AACnE,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,UAAU,EAAE,CAiG9E;AAMD,gFAAgF;AAChF,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,UAAU,EAAE,CAgH5E;AAMD,gGAAgG;AAChG,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,SAAS,UAAU,EAAE,CAgF3G;AAyMD,kEAAkE;AAClE,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,UAAU,EAAE,CAyS7E;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
@@ -57,7 +57,7 @@ function detectProvenance(cwd) {
57
57
  return cwd.includes('/spur') || cwd.includes('/spur-new') ? 'spur-run' : 'ambient';
58
58
  }
59
59
  /** Compute args_digest: sha256 of stable-jsonified, key-sorted, redacted args. */
60
- function argsDigest(args) {
60
+ export function argsDigest(args) {
61
61
  const redacted = redactArgs(args);
62
62
  return sha256(redacted);
63
63
  }
@@ -262,44 +262,59 @@ export function piSplit(raw) {
262
262
  // OMP mapper (near-identical to pi)
263
263
  // ---------------------------------------------------------------------------
264
264
  /** Map an OMP JSONL record into one or more ETL split entries. */
265
- export function ompSplit(raw) {
265
+ export function ompSplit(raw, context) {
266
+ // Current OMP files are top-level event envelopes: `{type: "message", id, timestamp, parentId,
267
+ // message: {role, content, model, usage, cost, duration, ...}}`. The unique top-level `id` is
268
+ // an event id, never a session id; the session key and sequence come from the source file
269
+ // (context) when available. The legacy direct shape keeps role/type/content at the top level.
270
+ const msg = raw.message;
266
271
  const recordType = String(raw.type ?? '');
272
+ const sessionId = sessionIdFromContext(context, raw);
273
+ const seq = context?.sourceLine ?? (typeof raw.seq === 'number' ? raw.seq : 0);
274
+ const ts = s(raw.ts, raw.timestamp, raw.createdAt) ?? new Date(0).toISOString();
275
+ // Meta lifecycle/custom records — including the current `custom.*` events and non-message
276
+ // lifecycle types — collapse to one meta row keyed by the source session, never the unique
277
+ // event id, and never a guessed role.
267
278
  if (recordType === 'title' ||
268
279
  recordType === 'title_change' ||
269
280
  recordType === 'service_tier_change' ||
270
281
  recordType === 'ttsr_injection' ||
271
282
  recordType === 'session_init' ||
272
- recordType === 'compaction') {
283
+ recordType === 'session' ||
284
+ recordType === 'model_change' ||
285
+ recordType === 'thinking_level_change' ||
286
+ recordType === 'compaction' ||
287
+ recordType === 'custom' ||
288
+ recordType === 'custom_message' ||
289
+ recordType.startsWith('custom.')) {
273
290
  return [
274
291
  {
275
292
  targetTable: 'history_message',
276
293
  record: {
277
- session_id: s(raw.id, o(raw.session).id) ?? 'unknown',
278
- seq: typeof raw.seq === 'number' ? raw.seq : 0,
294
+ session_id: sessionId,
295
+ seq,
279
296
  role: 'meta',
280
297
  record_type: recordType,
281
298
  disposition: 'meta',
282
- ts: s(raw.ts, raw.timestamp) ?? new Date(0).toISOString(),
299
+ ts,
283
300
  provenance: 'ambient',
284
301
  },
285
302
  },
286
303
  ];
287
304
  }
288
305
  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);
306
+ const role = mapRole(msg?.role ?? raw.type ?? raw.role);
307
+ const model = s(raw.model, o(msg).model);
294
308
  const cwd = s(raw.cwd, raw.dir);
295
- const msg = raw.message;
296
- const usage = msg?.usage;
309
+ const usage = (msg?.usage ?? raw.usage);
297
310
  const inputTokens = (usage?.input ?? usage?.input_tokens ?? undefined);
298
311
  const outputTokens = (usage?.output ?? usage?.output_tokens ?? undefined);
299
312
  const cacheRead = (usage?.cacheRead ?? usage?.cache_read_tokens ?? undefined);
300
313
  const cacheWrite = (usage?.cacheWrite ?? usage?.cache_write_tokens ?? undefined);
301
314
  const costObj = (msg?.cost ?? raw.cost);
302
315
  const costUsd = typeof costObj?.total === 'number' ? costObj.total : computeCost(inputTokens, outputTokens, model);
316
+ const contentBlocks = msg?.content ?? raw.content;
317
+ const durationMs = typeof msg?.duration === 'number' && Number.isFinite(msg.duration) ? msg.duration : undefined;
303
318
  const messageSplitIndex = entries.length;
304
319
  entries.push({
305
320
  targetTable: 'history_message',
@@ -310,43 +325,59 @@ export function ompSplit(raw) {
310
325
  record_type: recordType,
311
326
  disposition: 'keep',
312
327
  ts,
313
- duration_ms: undefined,
328
+ duration_ms: durationMs,
314
329
  model: model ?? null,
315
330
  input_tokens: inputTokens ?? null,
316
331
  output_tokens: outputTokens ?? null,
317
332
  cache_read_tokens: cacheRead ?? null,
318
333
  cache_write_tokens: cacheWrite ?? null,
319
334
  cost_usd: costUsd ?? null,
320
- content_text: s(raw.content, raw.text, msg?.content) ?? null,
335
+ content_text: extractContentText(contentBlocks) ?? s(raw.content, raw.text, msg?.content) ?? null,
321
336
  cwd: cwd ?? null,
322
337
  provenance: detectProvenance(cwd),
323
338
  },
324
339
  });
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
- }
340
+ if (Array.isArray(contentBlocks) && role === 'assistant') {
341
+ for (const block of contentBlocks) {
342
+ const call = normalizeOmpToolCall(block);
343
+ if (call === null)
344
+ continue;
345
+ entries.push({
346
+ targetTable: 'history_tool_call',
347
+ record: {
348
+ _messageSplitIndex: messageSplitIndex,
349
+ session_id: sessionId,
350
+ seq,
351
+ tool_name: String(call.name ?? ''),
352
+ args_digest: argsDigest(call.input ?? call.arguments),
353
+ status: 'ok',
354
+ started_at: undefined,
355
+ completed_at: undefined,
356
+ duration_ms: undefined,
357
+ result_bytes: undefined,
358
+ error_text: undefined,
359
+ },
360
+ });
346
361
  }
347
362
  }
348
363
  return entries;
349
364
  }
365
+ /**
366
+ * Normalize an OMP assistant content block to a tool-call object, or null when the block is not a
367
+ * call. Supports both the legacy nested `{toolCall: {...}}` block and the current flat
368
+ * `{type: "toolCall", id, name, arguments}` block. Exactly one history_tool_call row is emitted
369
+ * per call block.
370
+ */
371
+ function normalizeOmpToolCall(block) {
372
+ if (typeof block !== 'object' || block === null)
373
+ return null;
374
+ const nested = o(block.toolCall);
375
+ if (Object.keys(nested).length > 0)
376
+ return nested;
377
+ if (block.type === 'toolCall')
378
+ return block;
379
+ return null;
380
+ }
350
381
  // ---------------------------------------------------------------------------
351
382
  // Codex mapper
352
383
  // ---------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/ts-llm-jsonl-importer",
3
- "version": "0.4.25",
3
+ "version": "0.4.26",
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.25",
51
- "@gobing-ai/ts-runtime": "^0.4.25",
52
- "@gobing-ai/ts-utils": "^0.4.25",
50
+ "@gobing-ai/ts-db": "^0.4.26",
51
+ "@gobing-ai/ts-runtime": "^0.4.26",
52
+ "@gobing-ai/ts-utils": "^0.4.26",
53
53
  "zod": "^4.1.0"
54
54
  },
55
55
  "devDependencies": {
package/src/mappers.ts CHANGED
@@ -64,7 +64,7 @@ function detectProvenance(cwd?: string): 'ambient' | 'spur-run' {
64
64
  }
65
65
 
66
66
  /** Compute args_digest: sha256 of stable-jsonified, key-sorted, redacted args. */
67
- function argsDigest(args: unknown): string {
67
+ export function argsDigest(args: unknown): string {
68
68
  const redacted = redactArgs(args);
69
69
  return sha256(redacted);
70
70
  }
@@ -284,26 +284,44 @@ export function piSplit(raw: unknown): readonly SplitEntry[] {
284
284
  // ---------------------------------------------------------------------------
285
285
 
286
286
  /** Map an OMP JSONL record into one or more ETL split entries. */
287
- export function ompSplit(raw: Record<string, unknown>): readonly SplitEntry[] {
287
+ export function ompSplit(raw: Record<string, unknown>, context?: TransformContext): readonly SplitEntry[] {
288
+ // Current OMP files are top-level event envelopes: `{type: "message", id, timestamp, parentId,
289
+ // message: {role, content, model, usage, cost, duration, ...}}`. The unique top-level `id` is
290
+ // an event id, never a session id; the session key and sequence come from the source file
291
+ // (context) when available. The legacy direct shape keeps role/type/content at the top level.
292
+ const msg = raw.message as Record<string, unknown> | undefined;
288
293
  const recordType = String(raw.type ?? '');
294
+ const sessionId = sessionIdFromContext(context, raw);
295
+ const seq = context?.sourceLine ?? (typeof raw.seq === 'number' ? raw.seq : 0);
296
+ const ts = s(raw.ts, raw.timestamp, raw.createdAt) ?? new Date(0).toISOString();
297
+
298
+ // Meta lifecycle/custom records — including the current `custom.*` events and non-message
299
+ // lifecycle types — collapse to one meta row keyed by the source session, never the unique
300
+ // event id, and never a guessed role.
289
301
  if (
290
302
  recordType === 'title' ||
291
303
  recordType === 'title_change' ||
292
304
  recordType === 'service_tier_change' ||
293
305
  recordType === 'ttsr_injection' ||
294
306
  recordType === 'session_init' ||
295
- recordType === 'compaction'
307
+ recordType === 'session' ||
308
+ recordType === 'model_change' ||
309
+ recordType === 'thinking_level_change' ||
310
+ recordType === 'compaction' ||
311
+ recordType === 'custom' ||
312
+ recordType === 'custom_message' ||
313
+ recordType.startsWith('custom.')
296
314
  ) {
297
315
  return [
298
316
  {
299
317
  targetTable: 'history_message',
300
318
  record: {
301
- session_id: s(raw.id, o(raw.session).id) ?? 'unknown',
302
- seq: typeof raw.seq === 'number' ? raw.seq : 0,
319
+ session_id: sessionId,
320
+ seq,
303
321
  role: 'meta',
304
322
  record_type: recordType,
305
323
  disposition: 'meta',
306
- ts: s(raw.ts, raw.timestamp) ?? new Date(0).toISOString(),
324
+ ts,
307
325
  provenance: 'ambient',
308
326
  },
309
327
  },
@@ -311,21 +329,18 @@ export function ompSplit(raw: Record<string, unknown>): readonly SplitEntry[] {
311
329
  }
312
330
 
313
331
  const entries: SplitEntry[] = [];
314
- const sessionId = s(raw.id, o(raw.session).id) ?? 'unknown';
315
- const seq = typeof raw.seq === 'number' ? raw.seq : 0;
316
- const ts = s(raw.ts, raw.timestamp, raw.createdAt) ?? new Date(0).toISOString();
317
- const role = mapRole(raw.type ?? raw.role);
318
- const model = s(raw.model, o(raw.message).model);
332
+ const role = mapRole(msg?.role ?? raw.type ?? raw.role);
333
+ const model = s(raw.model, o(msg).model);
319
334
  const cwd = s(raw.cwd, raw.dir);
320
-
321
- const msg = raw.message as Record<string, unknown> | undefined;
322
- const usage = msg?.usage as Record<string, unknown> | undefined;
335
+ const usage = (msg?.usage ?? raw.usage) as Record<string, unknown> | undefined;
323
336
  const inputTokens = (usage?.input ?? usage?.input_tokens ?? undefined) as number | undefined;
324
337
  const outputTokens = (usage?.output ?? usage?.output_tokens ?? undefined) as number | undefined;
325
338
  const cacheRead = (usage?.cacheRead ?? usage?.cache_read_tokens ?? undefined) as number | undefined;
326
339
  const cacheWrite = (usage?.cacheWrite ?? usage?.cache_write_tokens ?? undefined) as number | undefined;
327
340
  const costObj = (msg?.cost ?? raw.cost) as Record<string, unknown> | undefined;
328
341
  const costUsd = typeof costObj?.total === 'number' ? costObj.total : computeCost(inputTokens, outputTokens, model);
342
+ const contentBlocks = msg?.content ?? raw.content;
343
+ const durationMs = typeof msg?.duration === 'number' && Number.isFinite(msg.duration) ? msg.duration : undefined;
329
344
 
330
345
  const messageSplitIndex = entries.length;
331
346
 
@@ -338,46 +353,59 @@ export function ompSplit(raw: Record<string, unknown>): readonly SplitEntry[] {
338
353
  record_type: recordType,
339
354
  disposition: 'keep',
340
355
  ts,
341
- duration_ms: undefined,
356
+ duration_ms: durationMs,
342
357
  model: model ?? null,
343
358
  input_tokens: inputTokens ?? null,
344
359
  output_tokens: outputTokens ?? null,
345
360
  cache_read_tokens: cacheRead ?? null,
346
361
  cache_write_tokens: cacheWrite ?? null,
347
362
  cost_usd: costUsd ?? null,
348
- content_text: s(raw.content, raw.text, msg?.content) ?? null,
363
+ content_text: extractContentText(contentBlocks) ?? s(raw.content, raw.text, msg?.content) ?? null,
349
364
  cwd: cwd ?? null,
350
365
  provenance: detectProvenance(cwd),
351
366
  },
352
367
  });
353
368
 
354
- if (Array.isArray(raw.content) && role === 'assistant') {
355
- for (const block of raw.content as Record<string, unknown>[]) {
356
- if (block?.toolCall) {
357
- const tc = block.toolCall as Record<string, unknown>;
358
- entries.push({
359
- targetTable: 'history_tool_call',
360
- record: {
361
- _messageSplitIndex: messageSplitIndex,
362
- session_id: sessionId,
363
- seq,
364
- tool_name: String(tc.name ?? ''),
365
- args_digest: argsDigest(tc.input ?? tc.arguments),
366
- status: 'ok',
367
- started_at: undefined,
368
- completed_at: undefined,
369
- duration_ms: undefined,
370
- result_bytes: undefined,
371
- error_text: undefined,
372
- },
373
- });
374
- }
369
+ if (Array.isArray(contentBlocks) && role === 'assistant') {
370
+ for (const block of contentBlocks as Record<string, unknown>[]) {
371
+ const call = normalizeOmpToolCall(block);
372
+ if (call === null) continue;
373
+ entries.push({
374
+ targetTable: 'history_tool_call',
375
+ record: {
376
+ _messageSplitIndex: messageSplitIndex,
377
+ session_id: sessionId,
378
+ seq,
379
+ tool_name: String(call.name ?? ''),
380
+ args_digest: argsDigest(call.input ?? call.arguments),
381
+ status: 'ok',
382
+ started_at: undefined,
383
+ completed_at: undefined,
384
+ duration_ms: undefined,
385
+ result_bytes: undefined,
386
+ error_text: undefined,
387
+ },
388
+ });
375
389
  }
376
390
  }
377
391
 
378
392
  return entries;
379
393
  }
380
394
 
395
+ /**
396
+ * Normalize an OMP assistant content block to a tool-call object, or null when the block is not a
397
+ * call. Supports both the legacy nested `{toolCall: {...}}` block and the current flat
398
+ * `{type: "toolCall", id, name, arguments}` block. Exactly one history_tool_call row is emitted
399
+ * per call block.
400
+ */
401
+ function normalizeOmpToolCall(block: Record<string, unknown>): Record<string, unknown> | null {
402
+ if (typeof block !== 'object' || block === null) return null;
403
+ const nested = o(block.toolCall);
404
+ if (Object.keys(nested).length > 0) return nested;
405
+ if (block.type === 'toolCall') return block;
406
+ return null;
407
+ }
408
+
381
409
  // ---------------------------------------------------------------------------
382
410
  // Codex mapper
383
411
  // ---------------------------------------------------------------------------