sidekick-shared 0.23.1 → 0.24.1

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 (47) hide show
  1. package/README.md +134 -37
  2. package/dist/aggregation/EventAggregator.d.ts +5 -1
  3. package/dist/aggregation/EventAggregator.js +64 -131
  4. package/dist/aggregation/types.d.ts +3 -0
  5. package/dist/browser.d.ts +6 -0
  6. package/dist/browser.js +10 -1
  7. package/dist/context/sessionContext.js +8 -6
  8. package/dist/extractors/tokenUsage.js +1 -1
  9. package/dist/index.d.ts +11 -1
  10. package/dist/index.js +23 -7
  11. package/dist/modelInfo.d.ts +17 -7
  12. package/dist/modelInfo.js +5 -2
  13. package/dist/node.d.ts +4 -0
  14. package/dist/node.js +8 -1
  15. package/dist/observedSessionCollector.d.ts +71 -0
  16. package/dist/observedSessionCollector.js +179 -0
  17. package/dist/parsers/codexParser.js +32 -14
  18. package/dist/parsers/openCodeParser.js +22 -1
  19. package/dist/parsers/subagentTraceParser.js +11 -5
  20. package/dist/providers/claudeCode.js +45 -1
  21. package/dist/providers/codex.js +17 -7
  22. package/dist/report/transcriptParser.d.ts +3 -11
  23. package/dist/report/transcriptParser.js +69 -154
  24. package/dist/schemas/index.d.ts +7 -1
  25. package/dist/schemas/index.js +19 -2
  26. package/dist/schemas/observedSessionV1.d.ts +32 -1
  27. package/dist/schemas/observedSessionV1.js +11 -1
  28. package/dist/schemas/sessionEvent.d.ts +373 -12
  29. package/dist/schemas/sessionEvent.js +101 -16
  30. package/dist/schemas/tokenEstimation.d.ts +14 -0
  31. package/dist/schemas/tokenEstimation.js +10 -0
  32. package/dist/schemas/transcript.d.ts +25 -0
  33. package/dist/schemas/transcript.js +87 -0
  34. package/dist/schemas/usageNormalization.d.ts +55 -0
  35. package/dist/schemas/usageNormalization.js +41 -0
  36. package/dist/sessionTranscripts.d.ts +16 -0
  37. package/dist/sessionTranscripts.js +47 -0
  38. package/dist/tokenEstimation.d.ts +24 -0
  39. package/dist/tokenEstimation.js +62 -0
  40. package/dist/transcript.d.ts +98 -0
  41. package/dist/transcript.js +255 -0
  42. package/dist/types/observedSessionV1.d.ts +3 -1
  43. package/dist/types/observedSessionV1.js +14 -1
  44. package/dist/types/sessionEvent.d.ts +73 -7
  45. package/dist/usageNormalization.d.ts +69 -0
  46. package/dist/usageNormalization.js +100 -0
  47. package/package.json +1 -1
@@ -24,16 +24,20 @@ export interface ModelPricing {
24
24
  cacheWriteCostPerMillion: number;
25
25
  cacheReadCostPerMillion: number;
26
26
  }
27
- /** Token usage for cost calculation. */
27
+ /**
28
+ * Token usage for the legacy cost calculation contract.
29
+ * @deprecated Use NormalizedUsage and calculateNormalizedUsageCost. This shape
30
+ * cannot state whether reasoning is already included in output.
31
+ */
28
32
  export interface CostTokenUsage {
29
33
  inputTokens: number;
30
34
  outputTokens: number;
31
35
  cacheWriteTokens: number;
32
36
  cacheReadTokens: number;
33
37
  /**
34
- * OpenAI o-series / Codex "reasoning" tokens. Billed at the output rate by
35
- * OpenAI, so we multiply by `outputCostPerMillion` inside calculateCost.
36
- * Optional for backward compatibility with existing callers.
38
+ * Legacy disjoint reasoning category. The compatibility calculator always
39
+ * adds it at the output rate, even when a provider already included it in
40
+ * output. Use NormalizedUsage to state inclusion semantics.
37
41
  */
38
42
  reasoningTokens?: number;
39
43
  }
@@ -55,7 +59,10 @@ export interface ModelInfo {
55
59
  }
56
60
  /** Provenance for a displayed cost value. */
57
61
  export type CostSource = 'reported' | 'estimated' | 'unpriced';
58
- /** Input for cost calculation that preserves reported-vs-estimated provenance. */
62
+ /**
63
+ * Input for the legacy cost calculation contract.
64
+ * @deprecated Use NormalizedUsageCostInput.
65
+ */
59
66
  export interface CostProvenanceInput {
60
67
  usage: CostTokenUsage;
61
68
  modelId: string;
@@ -128,8 +135,9 @@ export declare function getModelInfo(modelId: string): ModelInfo;
128
135
  /**
129
136
  * Calculates cost from token usage and an explicit pricing object.
130
137
  *
131
- * Reasoning tokens (OpenAI o-series / Codex) are billed at the output rate,
132
- * matching OpenAI's billing behavior.
138
+ * Legacy behavior treats reasoning as disjoint and adds it at the output rate.
139
+ * @deprecated Use calculateNormalizedUsageCost. This legacy function always
140
+ * adds reasoning to output and cannot represent provider inclusion semantics.
133
141
  */
134
142
  export declare function calculateCostWithPricing(usage: CostTokenUsage, pricing: ModelPricing): number;
135
143
  /**
@@ -137,11 +145,13 @@ export declare function calculateCostWithPricing(usage: CostTokenUsage, pricing:
137
145
  *
138
146
  * Returns `null` for unknown models. Callers should render `—` in that case,
139
147
  * not $0 — so users don't confuse "missing pricing" with "free".
148
+ * @deprecated Use calculateNormalizedUsageCost.
140
149
  */
141
150
  export declare function calculateCost(usage: CostTokenUsage, modelId: string): number | null;
142
151
  /**
143
152
  * Calculates cost while preserving whether the value was provider-reported,
144
153
  * locally estimated from pricing, or unavailable because the model is unpriced.
154
+ * @deprecated Use calculateNormalizedUsageCost.
145
155
  */
146
156
  export declare function calculateCostWithProvenance(input: CostProvenanceInput): CostWithProvenance;
147
157
  /**
package/dist/modelInfo.js CHANGED
@@ -386,8 +386,9 @@ function getModelInfo(modelId) {
386
386
  /**
387
387
  * Calculates cost from token usage and an explicit pricing object.
388
388
  *
389
- * Reasoning tokens (OpenAI o-series / Codex) are billed at the output rate,
390
- * matching OpenAI's billing behavior.
389
+ * Legacy behavior treats reasoning as disjoint and adds it at the output rate.
390
+ * @deprecated Use calculateNormalizedUsageCost. This legacy function always
391
+ * adds reasoning to output and cannot represent provider inclusion semantics.
391
392
  */
392
393
  function calculateCostWithPricing(usage, pricing) {
393
394
  const reasoning = usage.reasoningTokens ?? 0;
@@ -402,6 +403,7 @@ function calculateCostWithPricing(usage, pricing) {
402
403
  *
403
404
  * Returns `null` for unknown models. Callers should render `—` in that case,
404
405
  * not $0 — so users don't confuse "missing pricing" with "free".
406
+ * @deprecated Use calculateNormalizedUsageCost.
405
407
  */
406
408
  function calculateCost(usage, modelId) {
407
409
  const pricing = getModelPricing(modelId);
@@ -412,6 +414,7 @@ function calculateCost(usage, modelId) {
412
414
  /**
413
415
  * Calculates cost while preserving whether the value was provider-reported,
414
416
  * locally estimated from pricing, or unavailable because the model is unpriced.
417
+ * @deprecated Use calculateNormalizedUsageCost.
415
418
  */
416
419
  function calculateCostWithProvenance(input) {
417
420
  if (typeof input.reportedCostUsd === 'number' && Number.isFinite(input.reportedCostUsd)) {
package/dist/node.d.ts CHANGED
@@ -7,3 +7,7 @@
7
7
  */
8
8
  export { hydratePricingCatalog, normalizeLiteLlmCatalog, LITELLM_CATALOG_URL, } from './pricingCatalog';
9
9
  export type { HydrateOptions, HydrateResult } from './pricingCatalog';
10
+ export { listRecentSessions, readSessionTranscript } from './sessionTranscripts';
11
+ export type { ProviderSessionIndex, ListRecentSessionsOptions } from './sessionTranscripts';
12
+ export { ObservedSessionCollector, observedSessionSourceFromProvider, fileFingerprint, } from './observedSessionCollector';
13
+ export type { ObservedSessionReference, ObservedSessionCollectionSource, ProviderObservedSessionCollectionSource, ObservedSessionDiagnosticKind, ObservedSessionDiagnostic, ObservedSessionCollection, ObservedSessionCollectorOptions, } from './observedSessionCollector';
package/dist/node.js CHANGED
@@ -7,8 +7,15 @@
7
7
  * for browser bundles — use `sidekick-shared/browser` there.
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.LITELLM_CATALOG_URL = exports.normalizeLiteLlmCatalog = exports.hydratePricingCatalog = void 0;
10
+ exports.fileFingerprint = exports.observedSessionSourceFromProvider = exports.ObservedSessionCollector = exports.readSessionTranscript = exports.listRecentSessions = exports.LITELLM_CATALOG_URL = exports.normalizeLiteLlmCatalog = exports.hydratePricingCatalog = void 0;
11
11
  var pricingCatalog_1 = require("./pricingCatalog");
12
12
  Object.defineProperty(exports, "hydratePricingCatalog", { enumerable: true, get: function () { return pricingCatalog_1.hydratePricingCatalog; } });
13
13
  Object.defineProperty(exports, "normalizeLiteLlmCatalog", { enumerable: true, get: function () { return pricingCatalog_1.normalizeLiteLlmCatalog; } });
14
14
  Object.defineProperty(exports, "LITELLM_CATALOG_URL", { enumerable: true, get: function () { return pricingCatalog_1.LITELLM_CATALOG_URL; } });
15
+ var sessionTranscripts_1 = require("./sessionTranscripts");
16
+ Object.defineProperty(exports, "listRecentSessions", { enumerable: true, get: function () { return sessionTranscripts_1.listRecentSessions; } });
17
+ Object.defineProperty(exports, "readSessionTranscript", { enumerable: true, get: function () { return sessionTranscripts_1.readSessionTranscript; } });
18
+ var observedSessionCollector_1 = require("./observedSessionCollector");
19
+ Object.defineProperty(exports, "ObservedSessionCollector", { enumerable: true, get: function () { return observedSessionCollector_1.ObservedSessionCollector; } });
20
+ Object.defineProperty(exports, "observedSessionSourceFromProvider", { enumerable: true, get: function () { return observedSessionCollector_1.observedSessionSourceFromProvider; } });
21
+ Object.defineProperty(exports, "fileFingerprint", { enumerable: true, get: function () { return observedSessionCollector_1.fileFingerprint; } });
@@ -0,0 +1,71 @@
1
+ import type { ObservedAgentSessionV1, ProviderCapabilitiesV1 } from './types/observedSessionV1';
2
+ import type { SessionProviderBase } from './providers/types';
3
+ export interface ObservedSessionReference {
4
+ sessionId: string;
5
+ /** Opaque source locator used only by the source implementation. Never emitted in diagnostics. */
6
+ sourceKey?: string;
7
+ fingerprintHint?: string;
8
+ }
9
+ export interface ObservedSessionCollectionSource<T = unknown> {
10
+ providerId: string;
11
+ discover(): Promise<ObservedSessionReference[]> | ObservedSessionReference[];
12
+ read(reference: ObservedSessionReference): Promise<T> | T;
13
+ dispose?(): void;
14
+ }
15
+ export interface ProviderObservedSessionCollectionSource extends ObservedSessionCollectionSource<ObservedAgentSessionV1> {
16
+ capabilities: ProviderCapabilitiesV1;
17
+ }
18
+ export type ObservedSessionDiagnosticKind = 'provider-discovery-failed' | 'session-read-failed' | 'provider-recovered' | 'session-recovered';
19
+ /** Content-safe diagnostics: identifiers and retry metadata only. */
20
+ export interface ObservedSessionDiagnostic {
21
+ kind: ObservedSessionDiagnosticKind;
22
+ providerId: string;
23
+ sessionId?: string;
24
+ attempt?: number;
25
+ retryAt?: number;
26
+ }
27
+ export interface ObservedSessionCollection<T = unknown> {
28
+ providerId: string;
29
+ sessionId: string;
30
+ value: T;
31
+ }
32
+ export interface ObservedSessionCollectorOptions<T = unknown> {
33
+ sources: ObservedSessionCollectionSource<T>[];
34
+ initialBackoffMs?: number;
35
+ maxBackoffMs?: number;
36
+ clock?: {
37
+ now(): number;
38
+ };
39
+ fingerprint?: (source: ObservedSessionCollectionSource<T>, reference: ObservedSessionReference) => Promise<string | null> | string | null;
40
+ onObservation?: (observation: ObservedSessionCollection<T>) => void;
41
+ onDiagnostic?: (diagnostic: ObservedSessionDiagnostic) => void;
42
+ }
43
+ /**
44
+ * One-shot, host-scheduled observed-session collection with independent
45
+ * provider/session isolation and bounded retry state.
46
+ */
47
+ export declare class ObservedSessionCollector<T = unknown> {
48
+ private readonly options;
49
+ private readonly failures;
50
+ private readonly initialBackoffMs;
51
+ private readonly maxBackoffMs;
52
+ private readonly clock;
53
+ constructor(options: ObservedSessionCollectorOptions<T>);
54
+ collect(): Promise<ObservedSessionCollection<T>[]>;
55
+ reset(): void;
56
+ dispose(): void;
57
+ private collectSource;
58
+ private collectSession;
59
+ private fingerprint;
60
+ private fail;
61
+ private recover;
62
+ private emitDiagnostic;
63
+ private key;
64
+ }
65
+ /**
66
+ * Adapt a provider using path-only discovery and independent adapter reads, so
67
+ * one malformed session cannot reject discovery of its healthy siblings.
68
+ */
69
+ export declare function observedSessionSourceFromProvider(provider: SessionProviderBase, cwd: string): ProviderObservedSessionCollectionSource;
70
+ /** Size/mtime fingerprint suitable for retry bypass; returns null for synthetic DB locators. */
71
+ export declare function fileFingerprint(sourcePath: string): string | null;
@@ -0,0 +1,179 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ObservedSessionCollector = void 0;
4
+ exports.observedSessionSourceFromProvider = observedSessionSourceFromProvider;
5
+ exports.fileFingerprint = fileFingerprint;
6
+ const node_fs_1 = require("node:fs");
7
+ const observedSessionV1_1 = require("./types/observedSessionV1");
8
+ /**
9
+ * One-shot, host-scheduled observed-session collection with independent
10
+ * provider/session isolation and bounded retry state.
11
+ */
12
+ class ObservedSessionCollector {
13
+ options;
14
+ failures = new Map();
15
+ initialBackoffMs;
16
+ maxBackoffMs;
17
+ clock;
18
+ constructor(options) {
19
+ this.options = options;
20
+ this.initialBackoffMs = Math.max(1, options.initialBackoffMs ?? 30_000);
21
+ this.maxBackoffMs = Math.max(this.initialBackoffMs, options.maxBackoffMs ?? 5 * 60_000);
22
+ this.clock = options.clock ?? { now: () => Date.now() };
23
+ }
24
+ async collect() {
25
+ const nested = await Promise.all(this.options.sources.map((source) => this.collectSource(source)));
26
+ return nested.flat();
27
+ }
28
+ reset() {
29
+ this.failures.clear();
30
+ }
31
+ dispose() {
32
+ for (const source of this.options.sources) {
33
+ try {
34
+ source.dispose?.();
35
+ }
36
+ catch {
37
+ // Disposal is best-effort and isolated per source.
38
+ }
39
+ }
40
+ this.reset();
41
+ }
42
+ async collectSource(source) {
43
+ const discoveryKey = `${source.providerId}\0$discovery`;
44
+ const discoveryFailure = this.failures.get(discoveryKey);
45
+ if (discoveryFailure && this.clock.now() < discoveryFailure.retryAt)
46
+ return [];
47
+ let references;
48
+ try {
49
+ references = await source.discover();
50
+ this.recover(discoveryKey, 'provider-recovered', source.providerId);
51
+ }
52
+ catch (error) {
53
+ this.fail(discoveryKey, 'provider-discovery-failed', source.providerId, undefined, null, error);
54
+ return [];
55
+ }
56
+ const activeKeys = new Set(references.map((ref) => this.key(source.providerId, ref.sessionId)));
57
+ for (const key of this.failures.keys()) {
58
+ if (key.startsWith(`${source.providerId}\0`) &&
59
+ !key.endsWith('$discovery') &&
60
+ !activeKeys.has(key)) {
61
+ this.failures.delete(key);
62
+ }
63
+ }
64
+ const rows = await Promise.all(references.map((reference) => this.collectSession(source, reference)));
65
+ return rows.filter((row) => row !== null);
66
+ }
67
+ async collectSession(source, reference) {
68
+ const key = this.key(source.providerId, reference.sessionId);
69
+ const fingerprint = await this.fingerprint(source, reference);
70
+ const prior = this.failures.get(key);
71
+ if (prior && prior.fingerprint === fingerprint && this.clock.now() < prior.retryAt)
72
+ return null;
73
+ let value;
74
+ try {
75
+ value = await source.read(reference);
76
+ }
77
+ catch (error) {
78
+ this.fail(key, 'session-read-failed', source.providerId, reference.sessionId, fingerprint, error);
79
+ return null;
80
+ }
81
+ this.recover(key, 'session-recovered', source.providerId, reference.sessionId);
82
+ const observation = { providerId: source.providerId, sessionId: reference.sessionId, value };
83
+ try {
84
+ this.options.onObservation?.(observation);
85
+ }
86
+ catch {
87
+ // Host callbacks cannot turn a healthy provider read into a collection failure.
88
+ }
89
+ return observation;
90
+ }
91
+ async fingerprint(source, reference) {
92
+ try {
93
+ return this.options.fingerprint
94
+ ? await this.options.fingerprint(source, reference)
95
+ : (reference.fingerprintHint ?? null);
96
+ }
97
+ catch {
98
+ return reference.fingerprintHint ?? null;
99
+ }
100
+ }
101
+ fail(key, kind, providerId, sessionId, fingerprint, error) {
102
+ const previous = this.failures.get(key);
103
+ const signature = failureSignature(error);
104
+ const fingerprintChanged = previous?.fingerprint !== fingerprint;
105
+ const attempt = previous && !fingerprintChanged ? previous.attempt + 1 : 1;
106
+ const delay = Math.min(this.maxBackoffMs, this.initialBackoffMs * 2 ** (attempt - 1));
107
+ const retryAt = this.clock.now() + delay;
108
+ const reportedSignature = previous?.reportedSignature ?? '';
109
+ this.failures.set(key, {
110
+ attempt,
111
+ retryAt,
112
+ fingerprint,
113
+ reportedSignature: signature,
114
+ });
115
+ if (signature !== reportedSignature || fingerprintChanged) {
116
+ this.emitDiagnostic({ kind, providerId, sessionId, attempt, retryAt });
117
+ }
118
+ }
119
+ recover(key, kind, providerId, sessionId) {
120
+ const prior = this.failures.get(key);
121
+ if (!prior)
122
+ return;
123
+ this.failures.delete(key);
124
+ this.emitDiagnostic({ kind, providerId, sessionId, attempt: prior.attempt });
125
+ }
126
+ emitDiagnostic(diagnostic) {
127
+ try {
128
+ this.options.onDiagnostic?.(diagnostic);
129
+ }
130
+ catch {
131
+ // Diagnostics are observational and never affect collection isolation.
132
+ }
133
+ }
134
+ key(providerId, sessionId) {
135
+ return `${providerId}\0${sessionId}`;
136
+ }
137
+ }
138
+ exports.ObservedSessionCollector = ObservedSessionCollector;
139
+ /**
140
+ * Adapt a provider using path-only discovery and independent adapter reads, so
141
+ * one malformed session cannot reject discovery of its healthy siblings.
142
+ */
143
+ function observedSessionSourceFromProvider(provider, cwd) {
144
+ const adapter = (0, observedSessionV1_1.createProviderSessionAdapterV1)(provider);
145
+ return {
146
+ providerId: provider.id,
147
+ capabilities: adapter.capabilities,
148
+ discover() {
149
+ return provider.findAllSessions(cwd).map((sessionPath) => ({
150
+ sessionId: provider.getSessionId(sessionPath),
151
+ sourceKey: sessionPath,
152
+ fingerprintHint: provider.getSessionMetadata?.(sessionPath)?.mtime.getTime().toString() ??
153
+ fileFingerprint(sessionPath) ??
154
+ undefined,
155
+ }));
156
+ },
157
+ read(reference) {
158
+ if (!reference.sourceKey)
159
+ throw new Error('session source unavailable');
160
+ return adapter.read(reference.sourceKey, cwd);
161
+ },
162
+ dispose: () => adapter.dispose(),
163
+ };
164
+ }
165
+ /** Size/mtime fingerprint suitable for retry bypass; returns null for synthetic DB locators. */
166
+ function fileFingerprint(sourcePath) {
167
+ try {
168
+ const stat = (0, node_fs_1.statSync)(sourcePath);
169
+ return `${stat.size}:${stat.mtimeMs}`;
170
+ }
171
+ catch {
172
+ return null;
173
+ }
174
+ }
175
+ function failureSignature(error) {
176
+ if (error instanceof Error)
177
+ return `${error.name}\0${error.message}`;
178
+ return typeof error === 'string' ? error : Object.prototype.toString.call(error);
179
+ }
@@ -18,6 +18,7 @@ exports.normalizeCodexToolInput = normalizeCodexToolInput;
18
18
  exports.extractPatchFilePaths = extractPatchFilePaths;
19
19
  const codex_1 = require("../types/codex");
20
20
  const openCodeParser_1 = require("./openCodeParser");
21
+ const usageNormalization_1 = require("../usageNormalization");
21
22
  function reportedCompaction(tokensBefore, tokensAfter) {
22
23
  if (typeof tokensBefore !== 'number' ||
23
24
  typeof tokensAfter !== 'number' ||
@@ -199,18 +200,21 @@ class CodexRolloutParser {
199
200
  handleSessionMeta(timestamp, payload) {
200
201
  this.sessionMeta = payload;
201
202
  const text = payload.base_instructions?.text?.trim();
202
- if (!text)
203
- return [];
204
203
  return [
205
204
  {
206
205
  type: 'system',
207
- message: {
208
- role: 'system',
209
- id: `${payload.id}:base-instructions`,
210
- sourceLabel: 'base instructions',
211
- content: [{ type: 'text', text }],
212
- },
206
+ ...(text
207
+ ? {
208
+ message: {
209
+ role: 'system',
210
+ id: `${payload.id}:base-instructions`,
211
+ sourceLabel: 'base instructions',
212
+ content: [{ type: 'text', text }],
213
+ },
214
+ }
215
+ : {}),
213
216
  timestamp,
217
+ cwd: payload.cwd || undefined,
214
218
  },
215
219
  ];
216
220
  }
@@ -780,16 +784,28 @@ class CodexRolloutParser {
780
784
  const normalizedRateLimits = normalizeRateLimits(rateLimits);
781
785
  if (!usage && !normalizedRateLimits)
782
786
  return [];
783
- const mappedUsage = usage
787
+ const normalizedUsage = usage
788
+ ? (0, usageNormalization_1.normalizeProviderUsage)({
789
+ semantics: 'openai',
790
+ provider: 'openai',
791
+ source: 'codex-token-count',
792
+ model: this.currentModel || undefined,
793
+ inputTokens: usage.input_tokens,
794
+ outputTokens: usage.output_tokens,
795
+ cacheReadTokens: usage.cached_input_tokens,
796
+ reasoningTokens: usage.reasoning_output_tokens,
797
+ })
798
+ : undefined;
799
+ const mappedUsage = normalizedUsage
784
800
  ? {
785
801
  // Codex reports cached_input_tokens as a subset of input_tokens,
786
802
  // while MessageUsage categories are disjoint. Normalize once here
787
803
  // so aggregation and pricing do not count cached input twice.
788
- input_tokens: Math.max(0, (usage.input_tokens || 0) - (usage.cached_input_tokens || 0)),
789
- output_tokens: usage.output_tokens || 0,
790
- cache_read_input_tokens: usage.cached_input_tokens || 0,
791
- cache_creation_input_tokens: 0,
792
- reasoning_tokens: usage.reasoning_output_tokens || 0,
804
+ input_tokens: normalizedUsage.uncachedInputTokens,
805
+ output_tokens: normalizedUsage.outputTokens,
806
+ cache_read_input_tokens: normalizedUsage.cacheReadTokens,
807
+ cache_creation_input_tokens: normalizedUsage.cacheWriteTokens,
808
+ reasoning_tokens: normalizedUsage.reasoningTokens,
793
809
  }
794
810
  : undefined;
795
811
  if (usage) {
@@ -804,9 +820,11 @@ class CodexRolloutParser {
804
820
  sourceLabel: 'token count',
805
821
  model: this.currentModel || undefined,
806
822
  usage: mappedUsage,
823
+ normalizedUsage,
807
824
  content: [],
808
825
  },
809
826
  timestamp,
827
+ providerMetadata: { providerId: 'codex', source: 'codex-rollout' },
810
828
  rateLimits: normalizedRateLimits,
811
829
  },
812
830
  ];
@@ -18,6 +18,7 @@ exports.normalizeToolInput = normalizeToolInput;
18
18
  exports.convertOpenCodeMessage = convertOpenCodeMessage;
19
19
  exports.parseDbMessageData = parseDbMessageData;
20
20
  exports.parseDbPartData = parseDbPartData;
21
+ const usageNormalization_1 = require("../usageNormalization");
21
22
  /**
22
23
  * OpenCode → Claude Code tool name mapping.
23
24
  * OpenCode uses lowercase names; downstream code expects PascalCase.
@@ -322,7 +323,7 @@ function convertAssistantMessage(message, parts) {
322
323
  }
323
324
  }
324
325
  // Build usage data from message tokens
325
- const reportedCost = message.cost && message.cost > 0
326
+ const reportedCost = typeof message.cost === 'number' && Number.isFinite(message.cost) && message.cost >= 0
326
327
  ? message.cost
327
328
  : stepFinishCost > 0
328
329
  ? stepFinishCost
@@ -335,6 +336,23 @@ function convertAssistantMessage(message, parts) {
335
336
  reported_cost: reportedCost,
336
337
  reasoning_tokens: message.tokens.reasoning || 0,
337
338
  };
339
+ const normalizedUsage = (0, usageNormalization_1.normalizeProviderUsage)({
340
+ semantics: message.providerID === 'openai'
341
+ ? 'openai'
342
+ : message.providerID === 'anthropic'
343
+ ? 'anthropic'
344
+ : 'sidekick',
345
+ provider: message.providerID ?? 'opencode',
346
+ source: 'opencode-message',
347
+ model: message.modelID,
348
+ inputTokens: message.tokens.input,
349
+ outputTokens: message.tokens.output,
350
+ cacheReadTokens: message.tokens.cacheRead,
351
+ cacheWriteTokens: message.tokens.cacheWrite,
352
+ reasoningTokens: message.tokens.reasoning,
353
+ reasoningIncludedInOutput: message.providerID === 'openai',
354
+ reportedCostUsd: reportedCost,
355
+ });
338
356
  const timestamp = toISOString(message.time.completed || message.time.created);
339
357
  const retryAttempts = parts
340
358
  .filter((part) => part.type === 'retry')
@@ -353,10 +371,13 @@ function convertAssistantMessage(message, parts) {
353
371
  id: message.id,
354
372
  model: message.modelID,
355
373
  usage,
374
+ normalizedUsage,
356
375
  content,
357
376
  },
358
377
  timestamp,
359
378
  providerMetadata: {
379
+ providerId: message.providerID ?? 'opencode',
380
+ source: 'opencode-message',
360
381
  ...(retryAttempts.length > 0 ? { retryAttempts } : {}),
361
382
  ...(finishReasons.length > 0 ? { finishReasons } : {}),
362
383
  ...(message.error ? { providerError: message.error } : {}),
@@ -334,13 +334,19 @@ function rawToSessionEvent(raw) {
334
334
  }
335
335
  }
336
336
  }
337
- return {
338
- type: mappedType,
339
- message: message || { role: 'unknown' },
337
+ const common = {
340
338
  timestamp,
341
339
  isSidechain: raw.isSidechain,
342
340
  permissionMode: raw.permissionMode,
343
- tool,
344
- result,
345
341
  };
342
+ if (mappedType === 'user' || mappedType === 'assistant') {
343
+ return { ...common, type: mappedType, message: message || { role: 'unknown' }, tool };
344
+ }
345
+ if (mappedType === 'summary') {
346
+ return { ...common, type: 'summary', message: message || { role: 'summary' } };
347
+ }
348
+ if (mappedType === 'tool_use') {
349
+ return tool ? { ...common, type: 'tool_use', message, tool } : null;
350
+ }
351
+ return result ? { ...common, type: 'tool_result', message, result } : null;
346
352
  }
@@ -52,6 +52,8 @@ const jsonl_1 = require("../parsers/jsonl");
52
52
  const sessionPathResolver_1 = require("../parsers/sessionPathResolver");
53
53
  const subagentScanner_1 = require("../parsers/subagentScanner");
54
54
  const modelContext_1 = require("../modelContext");
55
+ const sessionEvent_1 = require("../schemas/sessionEvent");
56
+ const usageNormalization_1 = require("../usageNormalization");
55
57
  /** Type guard for content blocks with a `type` string property */
56
58
  function isTypedBlock(block) {
57
59
  return (block !== null &&
@@ -102,7 +104,11 @@ class ClaudeCodeReader {
102
104
  constructor(sessionPath) {
103
105
  this.sessionPath = sessionPath;
104
106
  this.parser = new jsonl_1.JsonlParser({
105
- onEvent: (e) => this.events.push(e),
107
+ onEvent: (raw) => {
108
+ for (const event of (0, sessionEvent_1.extractSessionEvents)(raw)) {
109
+ this.events.push(normalizeClaudeUsage(event));
110
+ }
111
+ },
106
112
  onError: (_err, _line) => {
107
113
  // Silently skip parse errors — no logging framework dependency
108
114
  },
@@ -180,6 +186,44 @@ class ClaudeCodeReader {
180
186
  return this._wasTruncated;
181
187
  }
182
188
  }
189
+ function normalizeClaudeUsage(event) {
190
+ const message = event.message;
191
+ const usage = message?.usage;
192
+ if (!message || !usage) {
193
+ return {
194
+ ...event,
195
+ providerMetadata: {
196
+ ...event.providerMetadata,
197
+ providerId: 'claude-code',
198
+ source: 'claude-code-jsonl',
199
+ },
200
+ };
201
+ }
202
+ return {
203
+ ...event,
204
+ message: {
205
+ ...message,
206
+ normalizedUsage: (0, usageNormalization_1.normalizeProviderUsage)({
207
+ semantics: 'anthropic',
208
+ provider: 'anthropic',
209
+ source: 'claude-code-jsonl',
210
+ model: message.model,
211
+ inputTokens: usage.input_tokens,
212
+ outputTokens: usage.output_tokens,
213
+ cacheReadTokens: usage.cache_read_input_tokens,
214
+ cacheWriteTokens: usage.cache_creation_input_tokens,
215
+ reasoningTokens: usage.reasoning_tokens,
216
+ reasoningIncludedInOutput: false,
217
+ reportedCostUsd: usage.reported_cost,
218
+ }),
219
+ },
220
+ providerMetadata: {
221
+ ...event.providerMetadata,
222
+ providerId: 'claude-code',
223
+ source: 'claude-code-jsonl',
224
+ },
225
+ };
226
+ }
183
227
  /**
184
228
  * Session provider for Claude Code CLI.
185
229
  *
@@ -53,6 +53,7 @@ const codexParser_1 = require("../parsers/codexParser");
53
53
  const codexDatabase_1 = require("./codexDatabase");
54
54
  const codexProfiles_1 = require("../codexProfiles");
55
55
  const modelContext_1 = require("../modelContext");
56
+ const usageNormalization_1 = require("../usageNormalization");
56
57
  // ---------------------------------------------------------------------------
57
58
  // Helper functions
58
59
  // ---------------------------------------------------------------------------
@@ -522,13 +523,22 @@ class CodexReader {
522
523
  // Propagate last token usage snapshot to the provider
523
524
  const lastUsage = this.parser.getLastTokenUsage();
524
525
  if (lastUsage && this.onTokenUsage) {
525
- const cacheReadTokens = lastUsage.cached_input_tokens || 0;
526
+ const normalized = (0, usageNormalization_1.normalizeProviderUsage)({
527
+ semantics: 'openai',
528
+ provider: 'openai',
529
+ source: 'codex-reader-snapshot',
530
+ model: this.parser.getCurrentModel() || undefined,
531
+ inputTokens: lastUsage.input_tokens,
532
+ outputTokens: lastUsage.output_tokens,
533
+ cacheReadTokens: lastUsage.cached_input_tokens,
534
+ reasoningTokens: lastUsage.reasoning_output_tokens,
535
+ });
526
536
  this.onTokenUsage({
527
- inputTokens: Math.max(0, (lastUsage.input_tokens || 0) - cacheReadTokens),
528
- outputTokens: lastUsage.output_tokens || 0,
529
- cacheWriteTokens: 0,
530
- cacheReadTokens,
531
- reasoningTokens: lastUsage.reasoning_output_tokens || 0,
537
+ inputTokens: normalized.uncachedInputTokens,
538
+ outputTokens: normalized.outputTokens,
539
+ cacheWriteTokens: normalized.cacheWriteTokens,
540
+ cacheReadTokens: normalized.cacheReadTokens,
541
+ reasoningTokens: normalized.reasoningTokens,
532
542
  model: this.parser.getCurrentModel() || 'unknown',
533
543
  timestamp: new Date(),
534
544
  });
@@ -868,7 +878,7 @@ class CodexProvider {
868
878
  }
869
879
  }
870
880
  // Accumulate tokens from usage events
871
- if (event.message.usage) {
881
+ if (event.message?.usage) {
872
882
  stats.inputTokens += event.message.usage.input_tokens || 0;
873
883
  stats.outputTokens += event.message.usage.output_tokens || 0;
874
884
  }
@@ -1,15 +1,7 @@
1
- /**
2
- * Parse Claude Code JSONL session files into typed transcript entries
3
- * with full, untruncated content suitable for HTML report rendering.
4
- */
1
+ /** Report compatibility adapters over the canonical transcript projection. */
5
2
  import type { SessionEvent } from '../types/sessionEvent';
6
3
  import type { TranscriptEntry } from './types';
7
- /**
8
- * Parse a JSONL session file into an array of TranscriptEntry objects.
9
- *
10
- * Unlike the FollowEvent pipeline (which truncates to 120-200 chars),
11
- * this preserves full message content for HTML report rendering.
12
- */
4
+ /** Parse a Claude Code JSONL session through the canonical provider reader. */
13
5
  export declare function parseTranscript(sessionPath: string): TranscriptEntry[];
14
- /** Parse canonical provider events into transcript entries. */
6
+ /** Project canonical provider events into the legacy report transcript shape. */
15
7
  export declare function parseTranscriptFromEvents(events: SessionEvent[]): TranscriptEntry[];