sidekick-shared 0.23.1 → 0.24.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.
- package/README.md +115 -37
- package/dist/aggregation/EventAggregator.d.ts +5 -1
- package/dist/aggregation/EventAggregator.js +64 -131
- package/dist/aggregation/types.d.ts +3 -0
- package/dist/browser.d.ts +6 -0
- package/dist/browser.js +10 -1
- package/dist/context/sessionContext.js +8 -6
- package/dist/extractors/tokenUsage.js +1 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +23 -7
- package/dist/modelInfo.d.ts +17 -7
- package/dist/modelInfo.js +5 -2
- package/dist/node.d.ts +4 -0
- package/dist/node.js +8 -1
- package/dist/observedSessionCollector.d.ts +68 -0
- package/dist/observedSessionCollector.js +178 -0
- package/dist/parsers/codexParser.js +21 -6
- package/dist/parsers/openCodeParser.js +22 -1
- package/dist/parsers/subagentTraceParser.js +11 -5
- package/dist/providers/claudeCode.js +45 -1
- package/dist/providers/codex.js +17 -7
- package/dist/report/transcriptParser.d.ts +3 -11
- package/dist/report/transcriptParser.js +69 -154
- package/dist/schemas/index.d.ts +7 -1
- package/dist/schemas/index.js +18 -2
- package/dist/schemas/observedSessionV1.d.ts +32 -1
- package/dist/schemas/observedSessionV1.js +11 -1
- package/dist/schemas/sessionEvent.d.ts +353 -12
- package/dist/schemas/sessionEvent.js +76 -14
- package/dist/schemas/tokenEstimation.d.ts +14 -0
- package/dist/schemas/tokenEstimation.js +10 -0
- package/dist/schemas/transcript.d.ts +24 -0
- package/dist/schemas/transcript.js +81 -0
- package/dist/schemas/usageNormalization.d.ts +55 -0
- package/dist/schemas/usageNormalization.js +41 -0
- package/dist/sessionTranscripts.d.ts +16 -0
- package/dist/sessionTranscripts.js +47 -0
- package/dist/tokenEstimation.d.ts +24 -0
- package/dist/tokenEstimation.js +62 -0
- package/dist/transcript.d.ts +92 -0
- package/dist/transcript.js +240 -0
- package/dist/types/observedSessionV1.d.ts +3 -1
- package/dist/types/observedSessionV1.js +14 -1
- package/dist/types/sessionEvent.d.ts +65 -7
- package/dist/usageNormalization.d.ts +69 -0
- package/dist/usageNormalization.js +100 -0
- package/package.json +1 -1
package/dist/modelInfo.d.ts
CHANGED
|
@@ -24,16 +24,20 @@ export interface ModelPricing {
|
|
|
24
24
|
cacheWriteCostPerMillion: number;
|
|
25
25
|
cacheReadCostPerMillion: number;
|
|
26
26
|
}
|
|
27
|
-
/**
|
|
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
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
132
|
-
*
|
|
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
|
-
*
|
|
390
|
-
*
|
|
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, 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,68 @@
|
|
|
1
|
+
import type { ObservedAgentSessionV1 } 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 type ObservedSessionDiagnosticKind = 'provider-discovery-failed' | 'session-read-failed' | 'provider-recovered' | 'session-recovered';
|
|
16
|
+
/** Content-safe diagnostics: identifiers and retry metadata only. */
|
|
17
|
+
export interface ObservedSessionDiagnostic {
|
|
18
|
+
kind: ObservedSessionDiagnosticKind;
|
|
19
|
+
providerId: string;
|
|
20
|
+
sessionId?: string;
|
|
21
|
+
attempt?: number;
|
|
22
|
+
retryAt?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface ObservedSessionCollection<T = unknown> {
|
|
25
|
+
providerId: string;
|
|
26
|
+
sessionId: string;
|
|
27
|
+
value: T;
|
|
28
|
+
}
|
|
29
|
+
export interface ObservedSessionCollectorOptions<T = unknown> {
|
|
30
|
+
sources: ObservedSessionCollectionSource<T>[];
|
|
31
|
+
initialBackoffMs?: number;
|
|
32
|
+
maxBackoffMs?: number;
|
|
33
|
+
clock?: {
|
|
34
|
+
now(): number;
|
|
35
|
+
};
|
|
36
|
+
fingerprint?: (source: ObservedSessionCollectionSource<T>, reference: ObservedSessionReference) => Promise<string | null> | string | null;
|
|
37
|
+
onObservation?: (observation: ObservedSessionCollection<T>) => void;
|
|
38
|
+
onDiagnostic?: (diagnostic: ObservedSessionDiagnostic) => void;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* One-shot, host-scheduled observed-session collection with independent
|
|
42
|
+
* provider/session isolation and bounded retry state.
|
|
43
|
+
*/
|
|
44
|
+
export declare class ObservedSessionCollector<T = unknown> {
|
|
45
|
+
private readonly options;
|
|
46
|
+
private readonly failures;
|
|
47
|
+
private readonly initialBackoffMs;
|
|
48
|
+
private readonly maxBackoffMs;
|
|
49
|
+
private readonly clock;
|
|
50
|
+
constructor(options: ObservedSessionCollectorOptions<T>);
|
|
51
|
+
collect(): Promise<ObservedSessionCollection<T>[]>;
|
|
52
|
+
reset(): void;
|
|
53
|
+
dispose(): void;
|
|
54
|
+
private collectSource;
|
|
55
|
+
private collectSession;
|
|
56
|
+
private fingerprint;
|
|
57
|
+
private fail;
|
|
58
|
+
private recover;
|
|
59
|
+
private emitDiagnostic;
|
|
60
|
+
private key;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Adapt a provider using path-only discovery and independent adapter reads, so
|
|
64
|
+
* one malformed session cannot reject discovery of its healthy siblings.
|
|
65
|
+
*/
|
|
66
|
+
export declare function observedSessionSourceFromProvider(provider: SessionProviderBase, cwd: string): ObservedSessionCollectionSource<ObservedAgentSessionV1>;
|
|
67
|
+
/** Size/mtime fingerprint suitable for retry bypass; returns null for synthetic DB locators. */
|
|
68
|
+
export declare function fileFingerprint(sourcePath: string): string | null;
|
|
@@ -0,0 +1,178 @@
|
|
|
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
|
+
discover() {
|
|
148
|
+
return provider.findAllSessions(cwd).map((sessionPath) => ({
|
|
149
|
+
sessionId: provider.getSessionId(sessionPath),
|
|
150
|
+
sourceKey: sessionPath,
|
|
151
|
+
fingerprintHint: provider.getSessionMetadata?.(sessionPath)?.mtime.getTime().toString() ??
|
|
152
|
+
fileFingerprint(sessionPath) ??
|
|
153
|
+
undefined,
|
|
154
|
+
}));
|
|
155
|
+
},
|
|
156
|
+
read(reference) {
|
|
157
|
+
if (!reference.sourceKey)
|
|
158
|
+
throw new Error('session source unavailable');
|
|
159
|
+
return adapter.read(reference.sourceKey, cwd);
|
|
160
|
+
},
|
|
161
|
+
dispose: () => adapter.dispose(),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/** Size/mtime fingerprint suitable for retry bypass; returns null for synthetic DB locators. */
|
|
165
|
+
function fileFingerprint(sourcePath) {
|
|
166
|
+
try {
|
|
167
|
+
const stat = (0, node_fs_1.statSync)(sourcePath);
|
|
168
|
+
return `${stat.size}:${stat.mtimeMs}`;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function failureSignature(error) {
|
|
175
|
+
if (error instanceof Error)
|
|
176
|
+
return `${error.name}\0${error.message}`;
|
|
177
|
+
return typeof error === 'string' ? error : Object.prototype.toString.call(error);
|
|
178
|
+
}
|
|
@@ -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' ||
|
|
@@ -780,16 +781,28 @@ class CodexRolloutParser {
|
|
|
780
781
|
const normalizedRateLimits = normalizeRateLimits(rateLimits);
|
|
781
782
|
if (!usage && !normalizedRateLimits)
|
|
782
783
|
return [];
|
|
783
|
-
const
|
|
784
|
+
const normalizedUsage = usage
|
|
785
|
+
? (0, usageNormalization_1.normalizeProviderUsage)({
|
|
786
|
+
semantics: 'openai',
|
|
787
|
+
provider: 'openai',
|
|
788
|
+
source: 'codex-token-count',
|
|
789
|
+
model: this.currentModel || undefined,
|
|
790
|
+
inputTokens: usage.input_tokens,
|
|
791
|
+
outputTokens: usage.output_tokens,
|
|
792
|
+
cacheReadTokens: usage.cached_input_tokens,
|
|
793
|
+
reasoningTokens: usage.reasoning_output_tokens,
|
|
794
|
+
})
|
|
795
|
+
: undefined;
|
|
796
|
+
const mappedUsage = normalizedUsage
|
|
784
797
|
? {
|
|
785
798
|
// Codex reports cached_input_tokens as a subset of input_tokens,
|
|
786
799
|
// while MessageUsage categories are disjoint. Normalize once here
|
|
787
800
|
// so aggregation and pricing do not count cached input twice.
|
|
788
|
-
input_tokens:
|
|
789
|
-
output_tokens:
|
|
790
|
-
cache_read_input_tokens:
|
|
791
|
-
cache_creation_input_tokens:
|
|
792
|
-
reasoning_tokens:
|
|
801
|
+
input_tokens: normalizedUsage.uncachedInputTokens,
|
|
802
|
+
output_tokens: normalizedUsage.outputTokens,
|
|
803
|
+
cache_read_input_tokens: normalizedUsage.cacheReadTokens,
|
|
804
|
+
cache_creation_input_tokens: normalizedUsage.cacheWriteTokens,
|
|
805
|
+
reasoning_tokens: normalizedUsage.reasoningTokens,
|
|
793
806
|
}
|
|
794
807
|
: undefined;
|
|
795
808
|
if (usage) {
|
|
@@ -804,9 +817,11 @@ class CodexRolloutParser {
|
|
|
804
817
|
sourceLabel: 'token count',
|
|
805
818
|
model: this.currentModel || undefined,
|
|
806
819
|
usage: mappedUsage,
|
|
820
|
+
normalizedUsage,
|
|
807
821
|
content: [],
|
|
808
822
|
},
|
|
809
823
|
timestamp,
|
|
824
|
+
providerMetadata: { providerId: 'codex', source: 'codex-rollout' },
|
|
810
825
|
rateLimits: normalizedRateLimits,
|
|
811
826
|
},
|
|
812
827
|
];
|
|
@@ -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
|
|
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
|
-
|
|
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: (
|
|
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
|
*
|
package/dist/providers/codex.js
CHANGED
|
@@ -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
|
|
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:
|
|
528
|
-
outputTokens:
|
|
529
|
-
cacheWriteTokens:
|
|
530
|
-
cacheReadTokens,
|
|
531
|
-
reasoningTokens:
|
|
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
|
|
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
|
-
/**
|
|
6
|
+
/** Project canonical provider events into the legacy report transcript shape. */
|
|
15
7
|
export declare function parseTranscriptFromEvents(events: SessionEvent[]): TranscriptEntry[];
|