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.
Files changed (90) hide show
  1. package/README.md +139 -57
  2. package/dist/accountManager.js +10 -0
  3. package/dist/aggregation/EventAggregator.d.ts +8 -1
  4. package/dist/aggregation/EventAggregator.js +82 -136
  5. package/dist/aggregation/snapshot.d.ts +1 -1
  6. package/dist/aggregation/snapshot.js +2 -1
  7. package/dist/aggregation/types.d.ts +5 -0
  8. package/dist/autoSwitch.d.ts +1 -1
  9. package/dist/autoSwitch.js +14 -3
  10. package/dist/browser.d.ts +6 -0
  11. package/dist/browser.js +10 -1
  12. package/dist/codexQuota.d.ts +2 -0
  13. package/dist/codexQuota.js +27 -0
  14. package/dist/codexQuotaWatcher.d.ts +7 -0
  15. package/dist/codexQuotaWatcher.js +37 -19
  16. package/dist/context/sessionContext.js +43 -15
  17. package/dist/credentialIO.js +19 -5
  18. package/dist/errorHistory.d.ts +1 -0
  19. package/dist/errorHistory.js +3 -2
  20. package/dist/extractors/tokenUsage.js +1 -1
  21. package/dist/extractors/toolCall.js +2 -2
  22. package/dist/index.d.ts +14 -4
  23. package/dist/index.js +25 -7
  24. package/dist/modelInfo.d.ts +17 -7
  25. package/dist/modelInfo.js +11 -2
  26. package/dist/multiProviderQuotaService.js +13 -2
  27. package/dist/node.d.ts +4 -0
  28. package/dist/node.js +8 -1
  29. package/dist/observedSessionCollector.d.ts +68 -0
  30. package/dist/observedSessionCollector.js +178 -0
  31. package/dist/parsers/codexParser.js +24 -6
  32. package/dist/parsers/openCodeParser.js +22 -1
  33. package/dist/parsers/sessionActivityDetector.js +2 -0
  34. package/dist/parsers/sessionPathResolver.js +19 -16
  35. package/dist/parsers/subagentScanner.d.ts +1 -1
  36. package/dist/parsers/subagentScanner.js +4 -5
  37. package/dist/parsers/subagentTraceParser.js +15 -12
  38. package/dist/providerStatus.js +13 -2
  39. package/dist/providers/claudeCode.js +63 -5
  40. package/dist/providers/codex.js +56 -17
  41. package/dist/providers/detect.js +32 -3
  42. package/dist/providers/openCodeDatabase.js +12 -3
  43. package/dist/quota.js +8 -0
  44. package/dist/quotaPoller.js +17 -6
  45. package/dist/quotaSnapshots.js +80 -26
  46. package/dist/readers/decisions.js +3 -1
  47. package/dist/readers/handoff.js +1 -1
  48. package/dist/readers/notes.js +9 -4
  49. package/dist/readers/plans.js +2 -4
  50. package/dist/readers/tasks.js +3 -1
  51. package/dist/report/htmlHelpers.js +6 -1
  52. package/dist/report/openBrowser.js +3 -3
  53. package/dist/report/transcriptParser.d.ts +3 -11
  54. package/dist/report/transcriptParser.js +69 -154
  55. package/dist/schemas/index.d.ts +7 -1
  56. package/dist/schemas/index.js +18 -2
  57. package/dist/schemas/observedSessionV1.d.ts +32 -1
  58. package/dist/schemas/observedSessionV1.js +11 -1
  59. package/dist/schemas/sessionEvent.d.ts +353 -12
  60. package/dist/schemas/sessionEvent.js +76 -14
  61. package/dist/schemas/tokenEstimation.d.ts +14 -0
  62. package/dist/schemas/tokenEstimation.js +10 -0
  63. package/dist/schemas/transcript.d.ts +24 -0
  64. package/dist/schemas/transcript.js +81 -0
  65. package/dist/schemas/usageNormalization.d.ts +55 -0
  66. package/dist/schemas/usageNormalization.js +41 -0
  67. package/dist/sessionTranscripts.d.ts +16 -0
  68. package/dist/sessionTranscripts.js +47 -0
  69. package/dist/statusline/formatter.js +8 -1
  70. package/dist/terminalSync.js +16 -7
  71. package/dist/tokenEstimation.d.ts +24 -0
  72. package/dist/tokenEstimation.js +62 -0
  73. package/dist/transcript.d.ts +92 -0
  74. package/dist/transcript.js +240 -0
  75. package/dist/types/historicalData.d.ts +14 -0
  76. package/dist/types/historicalData.js +22 -0
  77. package/dist/types/observedSessionV1.d.ts +3 -1
  78. package/dist/types/observedSessionV1.js +20 -6
  79. package/dist/types/sessionEvent.d.ts +66 -7
  80. package/dist/usageNormalization.d.ts +69 -0
  81. package/dist/usageNormalization.js +100 -0
  82. package/dist/watchers/factory.js +14 -3
  83. package/dist/watchers/jsonlTail.js +34 -14
  84. package/dist/watchers/jsonlWatcher.js +5 -3
  85. package/dist/watchers/providerReaderWatcher.js +4 -2
  86. package/dist/watchers/sqliteWatcher.d.ts +2 -0
  87. package/dist/watchers/sqliteWatcher.js +27 -14
  88. package/dist/writers/atomic.js +100 -13
  89. package/dist/zaiQuotaWatcher.js +6 -0
  90. 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 (!hasVisibleCachedSuccess) {
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 mappedUsage = usage
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
- input_tokens: usage.input_tokens || 0,
786
- output_tokens: usage.output_tokens || 0,
787
- cache_read_input_tokens: usage.cached_input_tokens || 0,
788
- cache_creation_input_tokens: 0,
789
- reasoning_tokens: usage.reasoning_output_tokens || 0,
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 > 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 } : {}),
@@ -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
- // Replace colons, slashes, and underscores with hyphens
85
- // Windows: C:\Users\foo_bar -> C:/Users/foo_bar -> C--Users-foo-bar
86
- // Unix: /home/user/foo_bar -> -home-user-foo-bar
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.basename(workspacePath).replace(/_/g, '-').toLowerCase();
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
- const stats = fs.statSync(fullPath);
410
- return {
411
- path: fullPath,
412
- mtime: stats.mtime.getTime(),
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.type === 'tool_use' && sessionEvent.tool) {
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 === 'tool_use' && evt.tool?.name === 'Task') {
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
- return {
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
  }
@@ -24,7 +24,7 @@ function fallbackState() {
24
24
  */
25
25
  async function fetchStatusPage(baseUrl) {
26
26
  try {
27
- const statusRes = await fetch(`${baseUrl}/api/v2/status.json`);
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 fetch(`${baseUrl}/api/v2/summary.json`);
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
  *