sidekick-shared 0.28.3 → 0.29.0

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.
Files changed (97) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +149 -19
  3. package/dist/authStatus.d.ts +54 -0
  4. package/dist/authStatus.js +91 -0
  5. package/dist/browser.d.ts +12 -6
  6. package/dist/browser.js +17 -1
  7. package/dist/codex.d.ts +23 -0
  8. package/dist/codex.js +44 -0
  9. package/dist/codexProfiles.d.ts +1 -0
  10. package/dist/codexProfiles.js +8 -11
  11. package/dist/esm/authStatus.js +87 -0
  12. package/dist/esm/browser.js +4 -2
  13. package/dist/esm/codex.js +18 -0
  14. package/dist/esm/codexProfiles.js +8 -11
  15. package/dist/esm/formatting.js +10 -1
  16. package/dist/esm/index.js +9 -6
  17. package/dist/esm/modelCatalog.js +2 -1
  18. package/dist/esm/modelInfo.js +422 -45
  19. package/dist/esm/node.js +9 -3
  20. package/dist/esm/parsers/codexParser.js +175 -54
  21. package/dist/esm/parsers/codexRolloutOrigin.js +107 -0
  22. package/dist/esm/parsers/openCodeParser.js +83 -21
  23. package/dist/esm/pricing.js +18 -0
  24. package/dist/esm/pricingCatalog.js +14 -7
  25. package/dist/esm/promptHistory.js +66 -4
  26. package/dist/esm/providerFailure.js +124 -3
  27. package/dist/esm/providers/codex.js +2 -24
  28. package/dist/esm/providers/codexDatabase.js +3 -19
  29. package/dist/esm/providers/codexRollouts.js +24 -1
  30. package/dist/esm/providers/openCode.js +9 -17
  31. package/dist/esm/report/htmlReportGenerator.js +42 -5
  32. package/dist/esm/report/transcriptParser.js +13 -11
  33. package/dist/esm/schemas/providerFailure.js +3 -1
  34. package/dist/esm/schemas/transcript.js +4 -0
  35. package/dist/esm/sessionPromptHistory.js +2 -2
  36. package/dist/esm/statusline/payload.js +9 -0
  37. package/dist/esm/testing/credentialGuard.js +87 -5
  38. package/dist/esm/toolCallOrigins.js +310 -0
  39. package/dist/esm/transcript.js +42 -3
  40. package/dist/esm/usage/usageEvents.js +146 -17
  41. package/dist/esm/usage/usageReports.js +39 -6
  42. package/dist/esm/watchers/jsonlTail.js +378 -59
  43. package/dist/formatting.d.ts +26 -1
  44. package/dist/formatting.js +11 -1
  45. package/dist/index.d.ts +16 -10
  46. package/dist/index.js +35 -12
  47. package/dist/modelCatalog.js +2 -1
  48. package/dist/modelInfo.d.ts +185 -16
  49. package/dist/modelInfo.js +431 -47
  50. package/dist/node.d.ts +16 -5
  51. package/dist/node.js +31 -2
  52. package/dist/parsers/codexParser.d.ts +55 -0
  53. package/dist/parsers/codexParser.js +175 -54
  54. package/dist/parsers/codexRolloutOrigin.d.ts +49 -0
  55. package/dist/parsers/codexRolloutOrigin.js +111 -0
  56. package/dist/parsers/openCodeParser.d.ts +38 -0
  57. package/dist/parsers/openCodeParser.js +86 -21
  58. package/dist/pricing.d.ts +22 -0
  59. package/dist/pricing.js +50 -0
  60. package/dist/pricingCatalog.d.ts +10 -0
  61. package/dist/pricingCatalog.js +13 -6
  62. package/dist/promptHistory.d.ts +65 -4
  63. package/dist/promptHistory.js +68 -5
  64. package/dist/providerFailure.d.ts +44 -0
  65. package/dist/providerFailure.js +126 -4
  66. package/dist/providers/codex.d.ts +3 -22
  67. package/dist/providers/codex.js +3 -26
  68. package/dist/providers/codexDatabase.d.ts +3 -2
  69. package/dist/providers/codexDatabase.js +4 -22
  70. package/dist/providers/codexRollouts.d.ts +22 -0
  71. package/dist/providers/codexRollouts.js +24 -0
  72. package/dist/providers/openCode.js +8 -16
  73. package/dist/report/htmlReportGenerator.js +42 -5
  74. package/dist/report/transcriptParser.d.ts +10 -1
  75. package/dist/report/transcriptParser.js +13 -11
  76. package/dist/schemas/providerFailure.d.ts +39 -0
  77. package/dist/schemas/providerFailure.js +2 -0
  78. package/dist/schemas/transcript.d.ts +2 -0
  79. package/dist/schemas/transcript.js +4 -0
  80. package/dist/sessionPromptHistory.d.ts +7 -1
  81. package/dist/sessionPromptHistory.js +1 -1
  82. package/dist/statusline/payload.d.ts +10 -0
  83. package/dist/statusline/payload.js +14 -0
  84. package/dist/testing/credentialGuard.d.ts +14 -0
  85. package/dist/testing/credentialGuard.js +87 -5
  86. package/dist/toolCallOrigins.d.ts +70 -0
  87. package/dist/toolCallOrigins.js +346 -0
  88. package/dist/transcript.d.ts +12 -0
  89. package/dist/transcript.js +42 -3
  90. package/dist/types/opencode.d.ts +2 -0
  91. package/dist/usage/usageEvents.d.ts +54 -6
  92. package/dist/usage/usageEvents.js +146 -17
  93. package/dist/usage/usageReports.d.ts +35 -4
  94. package/dist/usage/usageReports.js +39 -6
  95. package/dist/watchers/jsonlTail.d.ts +101 -4
  96. package/dist/watchers/jsonlTail.js +380 -60
  97. package/package.json +41 -1
@@ -171,6 +171,10 @@ export function extractPatchFilePaths(input) {
171
171
  }
172
172
  /** Default cap on each piece of per-call correlation state the parser keeps. */
173
173
  export const DEFAULT_MAX_TRACKED_TOOL_CALLS = 4096;
174
+ /** Fixed per-entry cost added to each pending call's byte estimate. */
175
+ const PENDING_ENTRY_OVERHEAD_BYTES = 64;
176
+ /** Prefix of the bookkeeping keys the parser stashes inside tool inputs. */
177
+ const SIDEKICK_INPUT_KEY_PREFIX = '_sidekick';
174
178
  /** Adds `key`, refreshing its position, and evicts the oldest past `max`. */
175
179
  function rememberBounded(set, key, max) {
176
180
  set.delete(key);
@@ -178,12 +182,31 @@ function rememberBounded(set, key, max) {
178
182
  while (set.size > max)
179
183
  set.delete(set.values().next().value);
180
184
  }
181
- /** Sets `key`, refreshing its position, and evicts the oldest past `max`. */
182
- function setBounded(map, key, value, max) {
183
- map.delete(key);
184
- map.set(key, value);
185
- while (map.size > max)
186
- map.delete(map.keys().next().value);
185
+ function stringBytes(...values) {
186
+ let total = 0;
187
+ for (const value of values)
188
+ if (typeof value === 'string')
189
+ total += value.length;
190
+ return total;
191
+ }
192
+ function approxJsonBytes(value) {
193
+ if (value === undefined)
194
+ return 0;
195
+ try {
196
+ return JSON.stringify(value)?.length ?? 0;
197
+ }
198
+ catch {
199
+ return 0;
200
+ }
201
+ }
202
+ /** Only the `_sidekick*` bookkeeping keys of a tool input. */
203
+ function sidekickKeysOnly(input) {
204
+ const kept = {};
205
+ for (const key of Object.keys(input)) {
206
+ if (key.startsWith(SIDEKICK_INPUT_KEY_PREFIX))
207
+ kept[key] = input[key];
208
+ }
209
+ return kept;
187
210
  }
188
211
  /**
189
212
  * Stateful parser for Codex JSONL rollout lines.
@@ -209,10 +232,35 @@ export class CodexRolloutParser {
209
232
  emittedToolUseIds = new Set();
210
233
  patchExpandedToolUseIds = new Map();
211
234
  maxTrackedToolCalls;
235
+ /** Byte budget for pending calls; `null` = unlimited (no accounting). */
236
+ maxPendingBytes;
237
+ metadataOnly;
238
+ /** Pending calls in insertion order, for the byte budget. */
239
+ pendingLedger = new Map();
240
+ pendingBytes = 0;
241
+ evictedByCount = 0;
242
+ evictedByBytes = 0;
212
243
  constructor(options = {}) {
213
244
  const max = options.maxTrackedToolCalls;
214
245
  this.maxTrackedToolCalls =
215
246
  typeof max === 'number' && max >= 1 ? Math.floor(max) : DEFAULT_MAX_TRACKED_TOOL_CALLS;
247
+ const maxBytes = options.maxPendingBytes;
248
+ this.maxPendingBytes =
249
+ typeof maxBytes === 'number' && Number.isFinite(maxBytes) && maxBytes >= 1
250
+ ? Math.floor(maxBytes)
251
+ : null;
252
+ this.metadataOnly = options.metadataOnly === true;
253
+ }
254
+ /** Counters for the pending tool-call state and what the caps evicted. */
255
+ getPendingStats() {
256
+ return {
257
+ pendingCalls: this.pendingExecCommands.size +
258
+ this.pendingMcpToolCalls.size +
259
+ this.patchExpandedToolUseIds.size,
260
+ pendingBytes: this.maxPendingBytes === null ? null : this.pendingBytes,
261
+ evictedByCount: this.evictedByCount,
262
+ evictedByBytes: this.evictedByBytes,
263
+ };
216
264
  }
217
265
  /** Get stored session metadata. */
218
266
  getSessionMeta() {
@@ -267,6 +315,72 @@ export class CodexRolloutParser {
267
315
  this.inPlanMode = false;
268
316
  this.emittedToolUseIds.clear();
269
317
  this.patchExpandedToolUseIds.clear();
318
+ this.pendingLedger.clear();
319
+ this.pendingBytes = 0;
320
+ this.evictedByCount = 0;
321
+ this.evictedByBytes = 0;
322
+ }
323
+ // --- Pending call state ---
324
+ /**
325
+ * Stores a pending call, refreshing its position, then enforces the count
326
+ * cap on its table and the byte budget across all pending tables, oldest
327
+ * first.
328
+ */
329
+ putPending(table, callId, value, estimateBytes) {
330
+ this.forgetPendingBytes(table, callId);
331
+ table.delete(callId);
332
+ table.set(callId, value);
333
+ while (table.size > this.maxTrackedToolCalls) {
334
+ const oldest = table.keys().next().value;
335
+ table.delete(oldest);
336
+ this.forgetPendingBytes(table, oldest);
337
+ this.evictedByCount += 1;
338
+ }
339
+ if (this.maxPendingBytes === null)
340
+ return;
341
+ const bytes = PENDING_ENTRY_OVERHEAD_BYTES + estimateBytes();
342
+ this.pendingLedger.set(this.ledgerKey(table, callId), {
343
+ table: table,
344
+ callId,
345
+ bytes,
346
+ });
347
+ this.pendingBytes += bytes;
348
+ while (this.pendingBytes > this.maxPendingBytes && this.pendingLedger.size > 0) {
349
+ const [key, oldest] = this.pendingLedger.entries().next().value;
350
+ this.pendingLedger.delete(key);
351
+ this.pendingBytes -= oldest.bytes;
352
+ oldest.table.delete(oldest.callId);
353
+ this.evictedByBytes += 1;
354
+ }
355
+ }
356
+ /** Removes and returns a pending call. */
357
+ takePending(table, callId) {
358
+ const value = table.get(callId);
359
+ table.delete(callId);
360
+ this.forgetPendingBytes(table, callId);
361
+ return value;
362
+ }
363
+ forgetPendingBytes(table, callId) {
364
+ if (this.maxPendingBytes === null)
365
+ return;
366
+ const key = this.ledgerKey(table, callId);
367
+ const entry = this.pendingLedger.get(key);
368
+ if (!entry)
369
+ return;
370
+ this.pendingLedger.delete(key);
371
+ this.pendingBytes -= entry.bytes;
372
+ }
373
+ ledgerKey(table, callId) {
374
+ const kind = table === this.pendingExecCommands ? 'e' : table === this.pendingMcpToolCalls ? 'm' : 'p';
375
+ return `${kind}\u0000${callId}`;
376
+ }
377
+ /** The emitted tool input: unchanged, or only its `_sidekick*` keys in metadata-only mode. */
378
+ toolInput(input) {
379
+ return this.metadataOnly ? sidekickKeysOnly(input) : input;
380
+ }
381
+ /** The emitted tool-result content: unchanged, or empty in metadata-only mode. */
382
+ toolOutput(content) {
383
+ return this.metadataOnly ? '' : content;
270
384
  }
271
385
  // --- Handlers ---
272
386
  handleSessionMeta(timestamp, payload) {
@@ -373,7 +487,9 @@ export class CodexRolloutParser {
373
487
  role: 'assistant',
374
488
  id: item.id,
375
489
  model: this.currentModel || undefined,
376
- content: [{ type: 'thinking', thinking: summaryTexts.join('\n') }],
490
+ content: [
491
+ { type: 'thinking', thinking: this.metadataOnly ? '' : summaryTexts.join('\n') },
492
+ ],
377
493
  },
378
494
  timestamp,
379
495
  },
@@ -381,11 +497,13 @@ export class CodexRolloutParser {
381
497
  }
382
498
  handleFunctionCall(timestamp, item) {
383
499
  let parsedArgs = {};
384
- try {
385
- parsedArgs = JSON.parse(item.arguments);
386
- }
387
- catch {
388
- parsedArgs = { raw: item.arguments };
500
+ if (!this.metadataOnly) {
501
+ try {
502
+ parsedArgs = JSON.parse(item.arguments);
503
+ }
504
+ catch {
505
+ parsedArgs = { raw: item.arguments };
506
+ }
389
507
  }
390
508
  const canonicalName = normalizeCodexToolName(item.name);
391
509
  const normalizedInput = normalizeCodexToolInput(canonicalName, item.name, parsedArgs);
@@ -402,7 +520,7 @@ export class CodexRolloutParser {
402
520
  type: 'tool_use',
403
521
  id: item.call_id,
404
522
  name: canonicalName,
405
- input: normalizedInput,
523
+ input: this.toolInput(normalizedInput),
406
524
  },
407
525
  ],
408
526
  },
@@ -422,7 +540,7 @@ export class CodexRolloutParser {
422
540
  {
423
541
  type: 'tool_result',
424
542
  tool_use_id: item.call_id,
425
- content: item.output,
543
+ content: this.toolOutput(item.output),
426
544
  is_error: isError,
427
545
  },
428
546
  ],
@@ -446,11 +564,11 @@ export class CodexRolloutParser {
446
564
  type: 'tool_use',
447
565
  id: item.call_id,
448
566
  name: 'Bash',
449
- input: {
567
+ input: this.toolInput({
450
568
  command,
451
569
  workdir: item.action?.workdir,
452
570
  _sidekickRawToolName: 'local_shell_call',
453
- },
571
+ }),
454
572
  },
455
573
  ],
456
574
  },
@@ -464,7 +582,7 @@ export class CodexRolloutParser {
464
582
  if (filePaths.length === 0)
465
583
  return [];
466
584
  const toolUseIds = filePaths.map((fp) => `${item.call_id}-${fp}`);
467
- setBounded(this.patchExpandedToolUseIds, item.call_id, toolUseIds, this.maxTrackedToolCalls);
585
+ this.putPending(this.patchExpandedToolUseIds, item.call_id, toolUseIds, () => stringBytes(item.call_id, ...toolUseIds));
468
586
  for (const id of toolUseIds) {
469
587
  rememberBounded(this.emittedToolUseIds, id, this.maxTrackedToolCalls);
470
588
  }
@@ -479,10 +597,10 @@ export class CodexRolloutParser {
479
597
  type: 'tool_use',
480
598
  id: `${item.call_id}-${fp}`,
481
599
  name: 'Edit',
482
- input: {
600
+ input: this.toolInput({
483
601
  file_path: fp,
484
602
  _sidekickRawToolName: 'apply_patch',
485
- },
603
+ }),
486
604
  },
487
605
  ],
488
606
  },
@@ -490,12 +608,14 @@ export class CodexRolloutParser {
490
608
  }));
491
609
  }
492
610
  // Generic custom tool
493
- let parsedInput;
494
- try {
495
- parsedInput = JSON.parse(item.input);
496
- }
497
- catch {
498
- parsedInput = { raw: item.input };
611
+ let parsedInput = {};
612
+ if (!this.metadataOnly) {
613
+ try {
614
+ parsedInput = JSON.parse(item.input);
615
+ }
616
+ catch {
617
+ parsedInput = { raw: item.input };
618
+ }
499
619
  }
500
620
  const canonicalName = normalizeCodexToolName(item.name);
501
621
  const normalizedInput = normalizeCodexToolInput(canonicalName, item.name, parsedInput);
@@ -512,7 +632,7 @@ export class CodexRolloutParser {
512
632
  type: 'tool_use',
513
633
  id: item.call_id,
514
634
  name: canonicalName,
515
- input: normalizedInput,
635
+ input: this.toolInput(normalizedInput),
516
636
  },
517
637
  ],
518
638
  },
@@ -533,8 +653,9 @@ export class CodexRolloutParser {
533
653
  catch {
534
654
  isError = parsePlainTextFailure(item.output);
535
655
  }
536
- const toolUseIds = this.patchExpandedToolUseIds.get(item.call_id) ?? [item.call_id];
537
- this.patchExpandedToolUseIds.delete(item.call_id);
656
+ const toolUseIds = this.takePending(this.patchExpandedToolUseIds, item.call_id) ?? [
657
+ item.call_id,
658
+ ];
538
659
  return toolUseIds.map((toolUseId) => ({
539
660
  type: 'user',
540
661
  message: {
@@ -544,7 +665,7 @@ export class CodexRolloutParser {
544
665
  {
545
666
  type: 'tool_result',
546
667
  tool_use_id: toolUseId,
547
- content: item.output,
668
+ content: this.toolOutput(item.output),
548
669
  is_error: isError,
549
670
  duration,
550
671
  },
@@ -608,18 +729,15 @@ export class CodexRolloutParser {
608
729
  // response_item/message events which carry richer metadata (id, content parts, role).
609
730
  case 'exec_command_begin': {
610
731
  const e = event;
611
- setBounded(this.pendingExecCommands, e.call_id, {
612
- call_id: e.call_id,
613
- command: e.command,
614
- workdir: e.workdir,
615
- timestamp,
616
- }, this.maxTrackedToolCalls);
732
+ const pending = this.metadataOnly
733
+ ? { call_id: e.call_id, command: [], timestamp }
734
+ : { call_id: e.call_id, command: e.command, workdir: e.workdir, timestamp };
735
+ this.putPending(this.pendingExecCommands, e.call_id, pending, () => stringBytes(pending.call_id, pending.workdir, pending.timestamp, ...(Array.isArray(pending.command) ? pending.command : [])));
617
736
  return [];
618
737
  }
619
738
  case 'exec_command_end': {
620
739
  const e = event;
621
- const pending = this.pendingExecCommands.get(e.call_id);
622
- this.pendingExecCommands.delete(e.call_id);
740
+ const pending = this.takePending(this.pendingExecCommands, e.call_id);
623
741
  const command = pending?.command?.join(' ') || '';
624
742
  const events = [];
625
743
  if (!this.emittedToolUseIds.has(e.call_id)) {
@@ -635,18 +753,20 @@ export class CodexRolloutParser {
635
753
  type: 'tool_use',
636
754
  id: e.call_id,
637
755
  name: 'Bash',
638
- input: {
756
+ input: this.toolInput({
639
757
  command,
640
758
  workdir: pending?.workdir,
641
759
  _sidekickRawToolName: 'exec_command',
642
- },
760
+ }),
643
761
  },
644
762
  ],
645
763
  },
646
764
  timestamp: pending?.timestamp || timestamp,
647
765
  });
648
766
  }
649
- const output = [e.stdout, e.stderr].filter(Boolean).join('\n') || '';
767
+ const output = this.metadataOnly
768
+ ? ''
769
+ : [e.stdout, e.stderr].filter(Boolean).join('\n') || '';
650
770
  events.push({
651
771
  type: 'user',
652
772
  message: {
@@ -668,19 +788,20 @@ export class CodexRolloutParser {
668
788
  }
669
789
  case 'mcp_tool_call_begin': {
670
790
  const e = event;
671
- setBounded(this.pendingMcpToolCalls, e.call_id, {
791
+ const pending = {
672
792
  call_id: e.call_id,
673
793
  server_name: e.server_name,
674
794
  tool_name: e.tool_name,
675
- arguments: e.arguments,
795
+ arguments: this.metadataOnly ? undefined : e.arguments,
676
796
  timestamp,
677
- }, this.maxTrackedToolCalls);
797
+ };
798
+ this.putPending(this.pendingMcpToolCalls, e.call_id, pending, () => stringBytes(pending.call_id, pending.server_name, pending.tool_name, timestamp) +
799
+ approxJsonBytes(pending.arguments));
678
800
  return [];
679
801
  }
680
802
  case 'mcp_tool_call_end': {
681
803
  const e = event;
682
- const pendingMcp = this.pendingMcpToolCalls.get(e.call_id);
683
- this.pendingMcpToolCalls.delete(e.call_id);
804
+ const pendingMcp = this.takePending(this.pendingMcpToolCalls, e.call_id);
684
805
  const toolName = pendingMcp?.tool_name || 'McpTool';
685
806
  const events = [];
686
807
  if (!this.emittedToolUseIds.has(e.call_id)) {
@@ -696,11 +817,11 @@ export class CodexRolloutParser {
696
817
  type: 'tool_use',
697
818
  id: e.call_id,
698
819
  name: normalizeCodexToolName(toolName),
699
- input: {
820
+ input: this.toolInput({
700
821
  ...(pendingMcp?.arguments || {}),
701
822
  _sidekickRawToolName: toolName,
702
823
  _sidekickMcpServerName: pendingMcp?.server_name,
703
- },
824
+ }),
704
825
  },
705
826
  ],
706
827
  },
@@ -716,7 +837,7 @@ export class CodexRolloutParser {
716
837
  {
717
838
  type: 'tool_result',
718
839
  tool_use_id: e.call_id,
719
- content: e.result || '',
840
+ content: this.toolOutput(e.result || ''),
720
841
  is_error: e.is_error || false,
721
842
  duration: e.duration_ms,
722
843
  },
@@ -775,12 +896,12 @@ export class CodexRolloutParser {
775
896
  type: 'tool_use',
776
897
  id: patchId,
777
898
  name: 'Edit',
778
- input: {
899
+ input: this.toolInput({
779
900
  file_path: e.file_path,
780
901
  additions: e.additions ?? 0,
781
902
  deletions: e.deletions ?? 0,
782
903
  _sidekickRawToolName: 'patch_applied',
783
- },
904
+ }),
784
905
  },
785
906
  ],
786
907
  },
@@ -807,10 +928,10 @@ export class CodexRolloutParser {
807
928
  type: 'tool_use',
808
929
  id: `plan-enter-${timestamp}`,
809
930
  name: 'EnterPlanMode',
810
- input: {
931
+ input: this.toolInput({
811
932
  source: 'codex_task_started',
812
933
  _sidekickRawToolName: 'task_started',
813
- },
934
+ }),
814
935
  },
815
936
  ],
816
937
  },
@@ -834,10 +955,10 @@ export class CodexRolloutParser {
834
955
  type: 'tool_use',
835
956
  id: `plan-exit-${timestamp}`,
836
957
  name: 'ExitPlanMode',
837
- input: {
958
+ input: this.toolInput({
838
959
  source: 'codex_task_complete',
839
960
  _sidekickRawToolName: 'task_complete',
840
- },
961
+ }),
841
962
  },
842
963
  ],
843
964
  },
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Where a Codex rollout came from, read from its `session_meta` record:
3
+ * a session a person started, a fork of another session, or a subagent.
4
+ *
5
+ * Codex writes `session_meta.source` as a plain string for sessions started
6
+ * from a surface (`cli`, `vscode`, `exec`, …) and as an object for subagents:
7
+ * `{ subagent: { thread_spawn: { parent_thread_id, … } } }` for a spawned
8
+ * agent, `{ subagent: { other: 'guardian' } }` for internal reviewers, or
9
+ * `{ subagent: 'review' }`. The same JSON appears as a string in the state
10
+ * database's `threads.source`. A forked rollout records `forked_from_id`, and
11
+ * embeds its parent's `session_meta` after its own, so only the first counts.
12
+ *
13
+ * Pure: no filesystem access. Browser-safe.
14
+ *
15
+ * @module parsers/codexRolloutOrigin
16
+ */
17
+ function record(value) {
18
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
19
+ ? value
20
+ : undefined;
21
+ }
22
+ function nonEmpty(value) {
23
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
24
+ }
25
+ /** A `source` value as an object: rollouts store it parsed, the database as JSON text. */
26
+ function sourceObject(source) {
27
+ if (typeof source !== 'string')
28
+ return record(source);
29
+ if (!source.startsWith('{'))
30
+ return undefined;
31
+ try {
32
+ return record(JSON.parse(source));
33
+ }
34
+ catch {
35
+ return undefined;
36
+ }
37
+ }
38
+ /**
39
+ * Parent thread id of a spawned subagent's `source` (object, or JSON text as
40
+ * stored in `threads.source`): `subagent.thread_spawn.parent_thread_id`.
41
+ */
42
+ export function codexSpawnParentFromSource(source) {
43
+ const spawn = record(record(sourceObject(source)?.subagent)?.thread_spawn);
44
+ const parent = spawn?.parent_thread_id;
45
+ return typeof parent === 'string' ? parent : undefined;
46
+ }
47
+ function subagentKindOf(subagent) {
48
+ if (typeof subagent === 'string')
49
+ return nonEmpty(subagent);
50
+ const variant = record(subagent);
51
+ if (!variant)
52
+ return undefined;
53
+ const other = nonEmpty(variant.other);
54
+ if (other)
55
+ return other;
56
+ return Object.keys(variant)[0];
57
+ }
58
+ /** The `session_meta` payload of a record, of a bare payload, or of the first in a list. */
59
+ function metaPayload(input) {
60
+ if (Array.isArray(input)) {
61
+ for (const item of input) {
62
+ const row = record(item);
63
+ if (row?.type === 'session_meta')
64
+ return record(row.payload);
65
+ }
66
+ return undefined;
67
+ }
68
+ const row = record(input);
69
+ if (!row)
70
+ return undefined;
71
+ if (row.type === 'session_meta')
72
+ return record(row.payload);
73
+ // A payload on its own, as `readSessionMeta()` returns it.
74
+ if (row.type === undefined && row.payload === undefined)
75
+ return row;
76
+ return undefined;
77
+ }
78
+ /**
79
+ * Origin of a Codex rollout from its `session_meta`. Accepts the parsed
80
+ * `session_meta` record, its payload, or the rollout's records in order (the
81
+ * first `session_meta` wins: a fork embeds its parent's after its own).
82
+ * Anything else yields `{ isSubagent: false }`.
83
+ */
84
+ export function codexRolloutOrigin(input) {
85
+ const payload = metaPayload(input);
86
+ if (!payload)
87
+ return { isSubagent: false };
88
+ const forkedFromId = nonEmpty(payload.forked_from_id);
89
+ const object = sourceObject(payload.source);
90
+ const isSubagent = object !== undefined && 'subagent' in object;
91
+ const source = typeof payload.source === 'string' && object === undefined
92
+ ? nonEmpty(payload.source)
93
+ : isSubagent
94
+ ? 'subagent'
95
+ : undefined;
96
+ const parentId = forkedFromId ??
97
+ nonEmpty(codexSpawnParentFromSource(object)) ??
98
+ (isSubagent ? nonEmpty(payload.parent_thread_id) : undefined);
99
+ const subagentKind = isSubagent ? subagentKindOf(object?.subagent) : undefined;
100
+ return {
101
+ ...(forkedFromId ? { forkedFromId } : {}),
102
+ ...(source ? { source } : {}),
103
+ isSubagent,
104
+ ...(parentId ? { parentId } : {}),
105
+ ...(subagentKind ? { subagentKind } : {}),
106
+ };
107
+ }
@@ -115,6 +115,78 @@ function toISOString(time) {
115
115
  return time;
116
116
  return new Date(time).toISOString();
117
117
  }
118
+ /**
119
+ * Token counts from a message in either storage shape: database rows are
120
+ * mapped to flat `cacheRead` / `cacheWrite`, while file-storage messages keep
121
+ * OpenCode's nested `cache: { read, write }`.
122
+ * @internal
123
+ */
124
+ export function openCodeTokenCounts(tokens) {
125
+ const raw = (tokens ?? {});
126
+ const cache = (raw.cache ?? {});
127
+ const num = (value) => (typeof value === 'number' ? value : undefined);
128
+ return {
129
+ input: num(raw.input),
130
+ output: num(raw.output),
131
+ reasoning: num(raw.reasoning),
132
+ cacheRead: num(raw.cacheRead) ?? num(cache.read),
133
+ cacheWrite: num(raw.cacheWrite) ?? num(cache.write),
134
+ total: num(raw.total),
135
+ };
136
+ }
137
+ /**
138
+ * How a stored OpenCode message splits its tokens. Current OpenCode stores
139
+ * every bucket disjoint for every provider: `input` excludes cache reads and
140
+ * writes (since 2025-11) and `output` excludes `reasoning` (since 2026-04),
141
+ * with `total` equal to their sum. Older messages either counted cached input
142
+ * in `input` (OpenAI-compatible providers) or reasoning in `output`. The
143
+ * stored `total` tells the layouts apart; without it the current layout is
144
+ * assumed.
145
+ * @internal
146
+ */
147
+ export function openCodeTokenLayout(tokens) {
148
+ const n = (value) => typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : 0;
149
+ const input = n(tokens.input);
150
+ const output = n(tokens.output);
151
+ const reasoning = n(tokens.reasoning);
152
+ const cache = n(tokens.cacheRead) + n(tokens.cacheWrite);
153
+ const total = n(tokens.total);
154
+ if (total > 0 && total !== input + cache + output + reasoning) {
155
+ if (reasoning > 0 && total === input + cache + output) {
156
+ return { inputIncludesCache: false, reasoningInOutput: true };
157
+ }
158
+ if (cache > 0 && total === input + output + reasoning) {
159
+ return { inputIncludesCache: true, reasoningInOutput: false };
160
+ }
161
+ if (cache > 0 && reasoning > 0 && total === input + output) {
162
+ return { inputIncludesCache: true, reasoningInOutput: true };
163
+ }
164
+ }
165
+ return { inputIncludesCache: false, reasoningInOutput: false };
166
+ }
167
+ /** Normalizes one OpenCode message's or step's stored tokens. @internal */
168
+ export function normalizeOpenCodeUsage(tokens, providerID, extra = {}) {
169
+ const layout = openCodeTokenLayout(tokens);
170
+ return normalizeProviderUsage({
171
+ // `openai` semantics subtract cache reads from input; only an old
172
+ // cache-inclusive message needs that.
173
+ semantics: layout.inputIncludesCache
174
+ ? 'openai'
175
+ : providerID === 'anthropic'
176
+ ? 'anthropic'
177
+ : 'sidekick',
178
+ provider: providerID ?? 'opencode',
179
+ ...(extra.source ? { source: extra.source } : {}),
180
+ ...(extra.model ? { model: extra.model } : {}),
181
+ inputTokens: tokens.input,
182
+ outputTokens: tokens.output,
183
+ cacheReadTokens: tokens.cacheRead,
184
+ cacheWriteTokens: tokens.cacheWrite,
185
+ reasoningTokens: tokens.reasoning,
186
+ reasoningIncludedInOutput: layout.reasoningInOutput,
187
+ ...(extra.reportedCostUsd !== undefined ? { reportedCostUsd: extra.reportedCostUsd } : {}),
188
+ });
189
+ }
118
190
  /**
119
191
  * Converts an OpenCode message and its parts into SessionEvent array.
120
192
  *
@@ -320,31 +392,20 @@ function convertAssistantMessage(message, parts) {
320
392
  : stepFinishCost > 0
321
393
  ? stepFinishCost
322
394
  : undefined;
323
- const usage = {
324
- input_tokens: message.tokens.input || 0,
325
- output_tokens: message.tokens.output || 0,
326
- cache_creation_input_tokens: message.tokens.cacheWrite || 0,
327
- cache_read_input_tokens: message.tokens.cacheRead || 0,
328
- reported_cost: reportedCost,
329
- reasoning_tokens: message.tokens.reasoning || 0,
330
- };
331
- const normalizedUsage = normalizeProviderUsage({
332
- semantics: message.providerID === 'openai'
333
- ? 'openai'
334
- : message.providerID === 'anthropic'
335
- ? 'anthropic'
336
- : 'sidekick',
337
- provider: message.providerID ?? 'opencode',
395
+ const counts = openCodeTokenCounts(message.tokens);
396
+ const normalizedUsage = normalizeOpenCodeUsage(counts, message.providerID, {
338
397
  source: 'opencode-message',
339
398
  model: message.modelID,
340
- inputTokens: message.tokens.input,
341
- outputTokens: message.tokens.output,
342
- cacheReadTokens: message.tokens.cacheRead,
343
- cacheWriteTokens: message.tokens.cacheWrite,
344
- reasoningTokens: message.tokens.reasoning,
345
- reasoningIncludedInOutput: message.providerID === 'openai',
346
399
  reportedCostUsd: reportedCost,
347
400
  });
401
+ const usage = {
402
+ input_tokens: normalizedUsage.uncachedInputTokens,
403
+ output_tokens: counts.output || 0,
404
+ cache_creation_input_tokens: counts.cacheWrite || 0,
405
+ cache_read_input_tokens: counts.cacheRead || 0,
406
+ reported_cost: reportedCost,
407
+ reasoning_tokens: counts.reasoning || 0,
408
+ };
348
409
  const timestamp = toISOString(message.time.completed || message.time.created);
349
410
  const retryAttempts = parts
350
411
  .filter((part) => part.type === 'retry')
@@ -483,6 +544,7 @@ export function parseDbMessageData(row) {
483
544
  cacheRead: cache?.read || 0,
484
545
  cacheWrite: cache?.write || 0,
485
546
  reasoning: tokens?.reasoning || 0,
547
+ ...(typeof tokens?.total === 'number' ? { total: tokens.total } : {}),
486
548
  },
487
549
  time: {
488
550
  created: time?.created || row.time_created,
@@ -0,0 +1,18 @@
1
+ /**
2
+ * `sidekick-shared/pricing` — model pricing, model-id identity, usage
3
+ * normalization and cost, cost provenance, cost/token formatting, and text
4
+ * token estimates, as one small pure entry (no filesystem, network, or Zod).
5
+ *
6
+ * For hosts that price usage or format costs without session reading: it
7
+ * loads a handful of modules instead of the full browser surface. LiteLLM
8
+ * catalog hydration stays Node-only in `sidekick-shared/node`
9
+ * (`hydratePricingCatalog`); once hydrated, lookups here see the catalog.
10
+ * Everything here is also exported from the package root and
11
+ * `sidekick-shared/browser`.
12
+ *
13
+ * @module pricing
14
+ */
15
+ export { MODEL_PROVIDER_IDS, canonicalModelId, isMovingModelAlias, parseModelId, compareModelIds, sortModelIds, shortModelName, getModelDisplayInfo, getModelInfo, getModelPricing, resolveModelPricing, registerModelPricing, clearRegisteredModelPricing, getPricingCatalogInfo, calculateCost, calculateCostWithPricing, calculateCostWithProvenance, mergeCostSources, costSourceFromPricingProvenance, mergeCostComponents, formatCost, } from './modelInfo';
16
+ export { normalizeProviderUsage, extractNormalizedUsage, calculateNormalizedUsageCost, } from './usageNormalization';
17
+ export { formatTokenCount, formatDurationMs } from './formatting';
18
+ export { ASCII_CHARS_PER_TOKEN, estimateTextTokens, estimateSerializedTokens, } from './tokenEstimation';