sidekick-shared 0.23.0 → 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 +139 -57
- package/dist/accountManager.js +10 -0
- package/dist/aggregation/EventAggregator.d.ts +8 -1
- package/dist/aggregation/EventAggregator.js +82 -136
- package/dist/aggregation/snapshot.d.ts +1 -1
- package/dist/aggregation/snapshot.js +2 -1
- package/dist/aggregation/types.d.ts +5 -0
- package/dist/autoSwitch.d.ts +1 -1
- package/dist/autoSwitch.js +14 -3
- package/dist/browser.d.ts +6 -0
- package/dist/browser.js +10 -1
- package/dist/codexQuota.d.ts +2 -0
- package/dist/codexQuota.js +27 -0
- package/dist/codexQuotaWatcher.d.ts +7 -0
- package/dist/codexQuotaWatcher.js +37 -19
- package/dist/context/sessionContext.js +43 -15
- package/dist/credentialIO.js +19 -5
- package/dist/errorHistory.d.ts +1 -0
- package/dist/errorHistory.js +3 -2
- package/dist/extractors/tokenUsage.js +1 -1
- package/dist/extractors/toolCall.js +2 -2
- package/dist/index.d.ts +14 -4
- package/dist/index.js +25 -7
- package/dist/modelInfo.d.ts +17 -7
- package/dist/modelInfo.js +11 -2
- package/dist/multiProviderQuotaService.js +13 -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 +24 -6
- package/dist/parsers/openCodeParser.js +22 -1
- package/dist/parsers/sessionActivityDetector.js +2 -0
- package/dist/parsers/sessionPathResolver.js +19 -16
- package/dist/parsers/subagentScanner.d.ts +1 -1
- package/dist/parsers/subagentScanner.js +4 -5
- package/dist/parsers/subagentTraceParser.js +15 -12
- package/dist/providerStatus.js +13 -2
- package/dist/providers/claudeCode.js +63 -5
- package/dist/providers/codex.js +56 -17
- package/dist/providers/detect.js +32 -3
- package/dist/providers/openCodeDatabase.js +12 -3
- package/dist/quota.js +8 -0
- package/dist/quotaPoller.js +17 -6
- package/dist/quotaSnapshots.js +80 -26
- package/dist/readers/decisions.js +3 -1
- package/dist/readers/handoff.js +1 -1
- package/dist/readers/notes.js +9 -4
- package/dist/readers/plans.js +2 -4
- package/dist/readers/tasks.js +3 -1
- package/dist/report/htmlHelpers.js +6 -1
- package/dist/report/openBrowser.js +3 -3
- 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/statusline/formatter.js +8 -1
- package/dist/terminalSync.js +16 -7
- 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/historicalData.d.ts +14 -0
- package/dist/types/historicalData.js +22 -0
- package/dist/types/observedSessionV1.d.ts +3 -1
- package/dist/types/observedSessionV1.js +20 -6
- package/dist/types/sessionEvent.d.ts +66 -7
- package/dist/usageNormalization.d.ts +69 -0
- package/dist/usageNormalization.js +100 -0
- package/dist/watchers/factory.js +14 -3
- package/dist/watchers/jsonlTail.js +34 -14
- package/dist/watchers/jsonlWatcher.js +5 -3
- package/dist/watchers/providerReaderWatcher.js +4 -2
- package/dist/watchers/sqliteWatcher.d.ts +2 -0
- package/dist/watchers/sqliteWatcher.js +27 -14
- package/dist/writers/atomic.js +100 -13
- package/dist/zaiQuotaWatcher.js +6 -0
- package/package.json +1 -1
|
@@ -174,7 +174,7 @@ class MultiProviderQuotaService {
|
|
|
174
174
|
providerId: 'claude-code',
|
|
175
175
|
});
|
|
176
176
|
this.lastEmittedClaudeState = nextState;
|
|
177
|
-
if (nextState.available) {
|
|
177
|
+
if (nextState.available && nextState.stale !== true) {
|
|
178
178
|
this.lastSuccessfulClaudeState = nextState;
|
|
179
179
|
}
|
|
180
180
|
this.quotaByProvider = {
|
|
@@ -323,7 +323,18 @@ class MultiProviderQuotaService {
|
|
|
323
323
|
}
|
|
324
324
|
}
|
|
325
325
|
this.cachedFallbackActive = hasVisibleCachedSuccess;
|
|
326
|
-
if (
|
|
326
|
+
if (hasVisibleCachedSuccess) {
|
|
327
|
+
this.emitClaudeState({
|
|
328
|
+
...this.lastSuccessfulClaudeState,
|
|
329
|
+
error: state.error,
|
|
330
|
+
failureKind: state.failureKind,
|
|
331
|
+
httpStatus: state.httpStatus,
|
|
332
|
+
retryAfterMs: state.retryAfterMs,
|
|
333
|
+
source: 'cache',
|
|
334
|
+
stale: true,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
327
338
|
this.emitClaudeState(state);
|
|
328
339
|
}
|
|
329
340
|
}
|
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,13 +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
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
798
|
+
// Codex reports cached_input_tokens as a subset of input_tokens,
|
|
799
|
+
// while MessageUsage categories are disjoint. Normalize once here
|
|
800
|
+
// so aggregation and pricing do not count cached input twice.
|
|
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,
|
|
790
806
|
}
|
|
791
807
|
: undefined;
|
|
792
808
|
if (usage) {
|
|
@@ -801,9 +817,11 @@ class CodexRolloutParser {
|
|
|
801
817
|
sourceLabel: 'token count',
|
|
802
818
|
model: this.currentModel || undefined,
|
|
803
819
|
usage: mappedUsage,
|
|
820
|
+
normalizedUsage,
|
|
804
821
|
content: [],
|
|
805
822
|
},
|
|
806
823
|
timestamp,
|
|
824
|
+
providerMetadata: { providerId: 'codex', source: 'codex-rollout' },
|
|
807
825
|
rateLimits: normalizedRateLimits,
|
|
808
826
|
},
|
|
809
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 } : {}),
|
|
@@ -126,6 +126,8 @@ function detectSessionActivity(sessionPath) {
|
|
|
126
126
|
}
|
|
127
127
|
if (!hasTerminal) {
|
|
128
128
|
for (const pattern of ENDING_PATTERNS) {
|
|
129
|
+
if (pattern === '"type":"user"' && line.includes('"type":"tool_result"'))
|
|
130
|
+
continue;
|
|
129
131
|
if (line.includes(pattern)) {
|
|
130
132
|
lastEndingIndex = i;
|
|
131
133
|
break;
|
|
@@ -81,10 +81,9 @@ const os = __importStar(require("os"));
|
|
|
81
81
|
function encodeWorkspacePath(workspacePath) {
|
|
82
82
|
// Normalize path separators to forward slash
|
|
83
83
|
const normalized = workspacePath.replace(/\\/g, '/');
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
return normalized.replace(/[:/_]/g, '-');
|
|
84
|
+
// Claude Code replaces every non-alphanumeric path character, including
|
|
85
|
+
// dots and spaces, with a hyphen.
|
|
86
|
+
return normalized.replace(/[^a-zA-Z0-9]/g, '-');
|
|
88
87
|
}
|
|
89
88
|
/**
|
|
90
89
|
* Gets the session directory path for a workspace.
|
|
@@ -249,12 +248,7 @@ function discoverSessionDirectory(workspacePath) {
|
|
|
249
248
|
});
|
|
250
249
|
// Try to match by workspace path components
|
|
251
250
|
// Normalize workspace path for comparison
|
|
252
|
-
const normalizedWorkspace = workspacePath
|
|
253
|
-
.replace(/\\/g, '/')
|
|
254
|
-
.replace(/:/g, '-')
|
|
255
|
-
.replace(/_/g, '-')
|
|
256
|
-
.replace(/\//g, '-')
|
|
257
|
-
.toLowerCase();
|
|
251
|
+
const normalizedWorkspace = encodeWorkspacePath(workspacePath).toLowerCase();
|
|
258
252
|
for (const dir of existingDirs) {
|
|
259
253
|
// Check if the directory name matches (case-insensitive)
|
|
260
254
|
if (dir.toLowerCase() === normalizedWorkspace) {
|
|
@@ -262,7 +256,10 @@ function discoverSessionDirectory(workspacePath) {
|
|
|
262
256
|
}
|
|
263
257
|
}
|
|
264
258
|
// Fallback: match by final path component (project name)
|
|
265
|
-
const workspaceBasename = path
|
|
259
|
+
const workspaceBasename = path
|
|
260
|
+
.basename(workspacePath)
|
|
261
|
+
.replace(/[^a-zA-Z0-9]/g, '-')
|
|
262
|
+
.toLowerCase();
|
|
266
263
|
for (const dir of existingDirs) {
|
|
267
264
|
const dirLower = dir.toLowerCase();
|
|
268
265
|
// Check if dir ends with the project name
|
|
@@ -406,12 +403,18 @@ function findAllSessions(workspacePath) {
|
|
|
406
403
|
.filter((file) => file.endsWith('.jsonl'))
|
|
407
404
|
.map((file) => {
|
|
408
405
|
const fullPath = path.join(sessionDir, file);
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
406
|
+
try {
|
|
407
|
+
const stats = fs.statSync(fullPath);
|
|
408
|
+
return {
|
|
409
|
+
path: fullPath,
|
|
410
|
+
mtime: stats.mtime.getTime(),
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
414
416
|
})
|
|
417
|
+
.filter((file) => file !== null)
|
|
415
418
|
.sort((a, b) => b.mtime - a.mtime)
|
|
416
419
|
.map((file) => file.path);
|
|
417
420
|
return files;
|
|
@@ -26,7 +26,7 @@ import type { SubagentStats, TaskState } from '../types/sessionEvent';
|
|
|
26
26
|
* }
|
|
27
27
|
* ```
|
|
28
28
|
*/
|
|
29
|
-
export declare function scanSubagentDir(sessionDir: string, sessionId: string, logger?: (msg: string) => void): SubagentStats[];
|
|
29
|
+
export declare function scanSubagentDir(sessionDir: string, sessionId: string, logger?: (msg: string) => void, skipFiles?: ReadonlySet<string>): SubagentStats[];
|
|
30
30
|
/**
|
|
31
31
|
* Handles TaskUpdate tool input and updates task state.
|
|
32
32
|
*/
|
|
@@ -93,7 +93,7 @@ const reportedMissing = new Set();
|
|
|
93
93
|
* }
|
|
94
94
|
* ```
|
|
95
95
|
*/
|
|
96
|
-
function scanSubagentDir(sessionDir, sessionId, logger) {
|
|
96
|
+
function scanSubagentDir(sessionDir, sessionId, logger, skipFiles = new Set()) {
|
|
97
97
|
const log = logger || (() => { });
|
|
98
98
|
const subagentsDir = path.join(sessionDir, sessionId, 'subagents');
|
|
99
99
|
const results = [];
|
|
@@ -110,6 +110,8 @@ function scanSubagentDir(sessionDir, sessionId, logger) {
|
|
|
110
110
|
const files = fs.readdirSync(subagentsDir);
|
|
111
111
|
log(`[SubagentScanner] Found ${files.length} files: ${files.join(', ')}`);
|
|
112
112
|
for (const file of files) {
|
|
113
|
+
if (skipFiles.has(file))
|
|
114
|
+
continue;
|
|
113
115
|
const match = file.match(AGENT_FILE_PATTERN);
|
|
114
116
|
if (!match) {
|
|
115
117
|
continue;
|
|
@@ -169,10 +171,7 @@ function parseAgentFile(filePath, agentId, log) {
|
|
|
169
171
|
// Extract token usage from assistant messages
|
|
170
172
|
if (event.type === 'assistant' && event.message?.usage) {
|
|
171
173
|
const usage = event.message.usage;
|
|
172
|
-
inputTokens +=
|
|
173
|
-
(usage.input_tokens || 0) +
|
|
174
|
-
(usage.cache_creation_input_tokens || 0) +
|
|
175
|
-
(usage.cache_read_input_tokens || 0);
|
|
174
|
+
inputTokens += usage.input_tokens || 0;
|
|
176
175
|
outputTokens += usage.output_tokens || 0;
|
|
177
176
|
}
|
|
178
177
|
// Extract agent type and description from Task tool invocation
|
|
@@ -141,7 +141,7 @@ function parseAgentTrace(filePath, agentId) {
|
|
|
141
141
|
}
|
|
142
142
|
// Format tool summary
|
|
143
143
|
let toolSummary;
|
|
144
|
-
if (sessionEvent.
|
|
144
|
+
if (sessionEvent.tool) {
|
|
145
145
|
toolSummary = (0, toolSummary_1.formatToolSummary)(sessionEvent.tool.name, sessionEvent.tool.input);
|
|
146
146
|
}
|
|
147
147
|
events.push({
|
|
@@ -155,10 +155,7 @@ function parseAgentTrace(filePath, agentId) {
|
|
|
155
155
|
// Extract token usage
|
|
156
156
|
if (raw.type === 'assistant' && raw.message?.usage) {
|
|
157
157
|
const usage = raw.message.usage;
|
|
158
|
-
inputTokens +=
|
|
159
|
-
(usage.input_tokens || 0) +
|
|
160
|
-
(usage.cache_creation_input_tokens || 0) +
|
|
161
|
-
(usage.cache_read_input_tokens || 0);
|
|
158
|
+
inputTokens += usage.input_tokens || 0;
|
|
162
159
|
outputTokens += usage.output_tokens || 0;
|
|
163
160
|
}
|
|
164
161
|
// Extract tool calls
|
|
@@ -251,7 +248,7 @@ function linkTraces(traces) {
|
|
|
251
248
|
for (const match of matches) {
|
|
252
249
|
const childId = match[1];
|
|
253
250
|
const child = traceById.get(childId);
|
|
254
|
-
if (child && child !== trace) {
|
|
251
|
+
if (child && child !== trace && !child.parentToolUseId) {
|
|
255
252
|
child.parentToolUseId = trace.agentId;
|
|
256
253
|
trace.children.push(child);
|
|
257
254
|
}
|
|
@@ -274,7 +271,7 @@ function linkTraces(traces) {
|
|
|
274
271
|
continue; // Already linked
|
|
275
272
|
for (const traceEvent of potential.events) {
|
|
276
273
|
const evt = traceEvent.event;
|
|
277
|
-
if (evt.type === '
|
|
274
|
+
if (evt.type === 'assistant' && evt.tool?.name === 'Task') {
|
|
278
275
|
const taskTime = new Date(evt.timestamp).getTime();
|
|
279
276
|
// Within 2 seconds before child start
|
|
280
277
|
if (taskTime <= childStart && childStart - taskTime < 2000) {
|
|
@@ -337,13 +334,19 @@ function rawToSessionEvent(raw) {
|
|
|
337
334
|
}
|
|
338
335
|
}
|
|
339
336
|
}
|
|
340
|
-
|
|
341
|
-
type: mappedType,
|
|
342
|
-
message: message || { role: 'unknown' },
|
|
337
|
+
const common = {
|
|
343
338
|
timestamp,
|
|
344
339
|
isSidechain: raw.isSidechain,
|
|
345
340
|
permissionMode: raw.permissionMode,
|
|
346
|
-
tool,
|
|
347
|
-
result,
|
|
348
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;
|
|
349
352
|
}
|
package/dist/providerStatus.js
CHANGED
|
@@ -24,7 +24,7 @@ function fallbackState() {
|
|
|
24
24
|
*/
|
|
25
25
|
async function fetchStatusPage(baseUrl) {
|
|
26
26
|
try {
|
|
27
|
-
const statusRes = await
|
|
27
|
+
const statusRes = await fetchWithTimeout(`${baseUrl}/api/v2/status.json`);
|
|
28
28
|
if (!statusRes.ok)
|
|
29
29
|
return fallbackState();
|
|
30
30
|
const statusData = await statusRes.json();
|
|
@@ -36,7 +36,7 @@ async function fetchStatusPage(baseUrl) {
|
|
|
36
36
|
return { indicator, description, affectedComponents: [], activeIncident: null, updatedAt };
|
|
37
37
|
}
|
|
38
38
|
// Degraded — fetch summary for components + incidents
|
|
39
|
-
const summaryRes = await
|
|
39
|
+
const summaryRes = await fetchWithTimeout(`${baseUrl}/api/v2/summary.json`);
|
|
40
40
|
if (!summaryRes.ok) {
|
|
41
41
|
return { indicator, description, affectedComponents: [], activeIncident: null, updatedAt };
|
|
42
42
|
}
|
|
@@ -61,6 +61,17 @@ async function fetchStatusPage(baseUrl) {
|
|
|
61
61
|
return fallbackState();
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
+
async function fetchWithTimeout(url) {
|
|
65
|
+
const controller = new AbortController();
|
|
66
|
+
const timeout = setTimeout(() => controller.abort(), 10_000);
|
|
67
|
+
timeout.unref();
|
|
68
|
+
try {
|
|
69
|
+
return await fetch(url, { signal: controller.signal });
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
clearTimeout(timeout);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
64
75
|
/**
|
|
65
76
|
* Fetch current Claude API status from status.claude.com.
|
|
66
77
|
*
|