sidekick-shared 0.22.0 → 0.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +75 -63
  2. package/dist/accountManager.js +10 -0
  3. package/dist/accountRegistry.d.ts +2 -0
  4. package/dist/accountStatus.d.ts +1 -0
  5. package/dist/accountStatus.js +6 -0
  6. package/dist/accounts.js +2 -0
  7. package/dist/aggregation/EventAggregator.d.ts +36 -2
  8. package/dist/aggregation/EventAggregator.js +259 -33
  9. package/dist/aggregation/snapshot.d.ts +1 -1
  10. package/dist/aggregation/snapshot.js +2 -1
  11. package/dist/aggregation/types.d.ts +25 -1
  12. package/dist/analytics/codeImpact.d.ts +20 -0
  13. package/dist/analytics/codeImpact.js +21 -0
  14. package/dist/analytics/compactionLedger.d.ts +11 -0
  15. package/dist/analytics/compactionLedger.js +31 -0
  16. package/dist/analytics/qualityScore.d.ts +26 -0
  17. package/dist/analytics/qualityScore.js +115 -0
  18. package/dist/autoSwitch.d.ts +1 -1
  19. package/dist/autoSwitch.js +14 -3
  20. package/dist/codexProfiles.js +2 -0
  21. package/dist/codexQuota.d.ts +2 -0
  22. package/dist/codexQuota.js +27 -0
  23. package/dist/codexQuotaWatcher.d.ts +7 -0
  24. package/dist/codexQuotaWatcher.js +37 -19
  25. package/dist/context/composer.d.ts +3 -2
  26. package/dist/context/composer.js +5 -5
  27. package/dist/context/sessionContext.js +35 -9
  28. package/dist/credentialIO.js +19 -5
  29. package/dist/doctor.d.ts +55 -0
  30. package/dist/doctor.js +244 -0
  31. package/dist/errorHistory.d.ts +33 -0
  32. package/dist/errorHistory.js +95 -0
  33. package/dist/extractors/errorTaxonomy.d.ts +6 -0
  34. package/dist/extractors/errorTaxonomy.js +47 -0
  35. package/dist/extractors/toolCall.d.ts +9 -0
  36. package/dist/extractors/toolCall.js +79 -0
  37. package/dist/handoffUrl.d.ts +7 -0
  38. package/dist/handoffUrl.js +26 -0
  39. package/dist/index.d.ts +32 -9
  40. package/dist/index.js +64 -24
  41. package/dist/modelInfo.js +6 -0
  42. package/dist/multiProviderQuotaService.js +13 -2
  43. package/dist/parsers/codexParser.js +17 -1
  44. package/dist/parsers/openCodeParser.js +13 -0
  45. package/dist/parsers/planExtractor.js +17 -6
  46. package/dist/parsers/sessionActivityDetector.js +2 -0
  47. package/dist/parsers/sessionPathResolver.js +19 -16
  48. package/dist/parsers/subagentScanner.d.ts +1 -1
  49. package/dist/parsers/subagentScanner.js +4 -5
  50. package/dist/parsers/subagentTraceParser.js +4 -7
  51. package/dist/paths.d.ts +10 -0
  52. package/dist/paths.js +18 -5
  53. package/dist/projectMigration.d.ts +23 -0
  54. package/dist/projectMigration.js +95 -0
  55. package/dist/providerStatus.js +13 -2
  56. package/dist/providers/claudeCode.js +18 -4
  57. package/dist/providers/codex.js +42 -13
  58. package/dist/providers/codexDatabase.d.ts +1 -11
  59. package/dist/providers/detect.js +32 -3
  60. package/dist/providers/openCodeDatabase.d.ts +1 -31
  61. package/dist/providers/openCodeDatabase.js +21 -11
  62. package/dist/providers/types.d.ts +1 -0
  63. package/dist/quota.js +8 -0
  64. package/dist/quotaPoller.js +17 -6
  65. package/dist/quotaSnapshots.js +80 -26
  66. package/dist/readers/decisions.d.ts +2 -1
  67. package/dist/readers/decisions.js +5 -5
  68. package/dist/readers/handoff.d.ts +2 -1
  69. package/dist/readers/handoff.js +13 -8
  70. package/dist/readers/helpers.d.ts +3 -0
  71. package/dist/readers/helpers.js +12 -0
  72. package/dist/readers/notes.d.ts +2 -1
  73. package/dist/readers/notes.js +11 -8
  74. package/dist/readers/plans.js +3 -5
  75. package/dist/readers/tasks.d.ts +2 -1
  76. package/dist/readers/tasks.js +5 -5
  77. package/dist/report/htmlHelpers.js +6 -1
  78. package/dist/report/openBrowser.js +3 -3
  79. package/dist/schemas/index.d.ts +2 -0
  80. package/dist/schemas/index.js +10 -1
  81. package/dist/schemas/observedSessionV1.d.ts +499 -0
  82. package/dist/schemas/observedSessionV1.js +69 -0
  83. package/dist/schemas/sessionEvent.d.ts +13 -0
  84. package/dist/schemas/sessionEvent.js +19 -0
  85. package/dist/search/sessionSearch.d.ts +2 -2
  86. package/dist/search/sessionSearch.js +2 -3
  87. package/dist/sessionMonitor.d.ts +29 -0
  88. package/dist/sessionMonitor.js +71 -0
  89. package/dist/statusline/BurnRateCalculator.d.ts +14 -0
  90. package/dist/statusline/BurnRateCalculator.js +45 -0
  91. package/dist/statusline/formatter.d.ts +16 -0
  92. package/dist/statusline/formatter.js +84 -0
  93. package/dist/statusline/index.d.ts +7 -0
  94. package/dist/statusline/index.js +13 -0
  95. package/dist/terminalSync.js +16 -7
  96. package/dist/types/codex.d.ts +2 -0
  97. package/dist/types/historicalData.d.ts +49 -1
  98. package/dist/types/historicalData.js +28 -2
  99. package/dist/types/observedSessionV1.d.ts +70 -0
  100. package/dist/types/observedSessionV1.js +186 -0
  101. package/dist/types/opencode.d.ts +1 -0
  102. package/dist/types/sessionEvent.d.ts +19 -1
  103. package/dist/watchers/eventBridge.js +1 -0
  104. package/dist/watchers/factory.js +14 -3
  105. package/dist/watchers/jsonlTail.js +34 -14
  106. package/dist/watchers/jsonlWatcher.js +5 -3
  107. package/dist/watchers/providerReaderWatcher.js +4 -2
  108. package/dist/watchers/sqliteWatcher.d.ts +2 -0
  109. package/dist/watchers/sqliteWatcher.js +27 -14
  110. package/dist/watchers/types.d.ts +5 -0
  111. package/dist/writers/atomic.d.ts +4 -0
  112. package/dist/writers/atomic.js +200 -0
  113. package/dist/writers/decisions.d.ts +12 -0
  114. package/dist/writers/decisions.js +88 -0
  115. package/dist/writers/notes.d.ts +8 -0
  116. package/dist/writers/notes.js +82 -0
  117. package/dist/writers/tasks.d.ts +6 -0
  118. package/dist/writers/tasks.js +100 -0
  119. package/dist/zaiQuotaWatcher.js +6 -0
  120. package/package.json +5 -1
@@ -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) {
package/dist/paths.d.ts CHANGED
@@ -30,6 +30,16 @@ export declare function encodeWorkspacePath(workspacePath: string): string;
30
30
  * Resolves symlinks, then encodes for use as a filename.
31
31
  */
32
32
  export declare function getProjectSlug(cwd?: string): string;
33
+ export interface ProjectIdentity {
34
+ cwd: string;
35
+ resolvedCwd: string;
36
+ canonicalSlug: string;
37
+ legacySlug: string;
38
+ candidates: string[];
39
+ hasLegacyMismatch: boolean;
40
+ }
41
+ /** Resolves the canonical project identity used by all new readers and writers. */
42
+ export declare function resolveProjectIdentity(cwd?: string): ProjectIdentity;
33
43
  /**
34
44
  * Derives a project slug WITHOUT resolving symlinks.
35
45
  * Matches the VS Code extension's behavior (encodeWorkspacePath on raw path).
package/dist/paths.js CHANGED
@@ -43,6 +43,7 @@ exports.getProjectDataPath = getProjectDataPath;
43
43
  exports.getGlobalDataPath = getGlobalDataPath;
44
44
  exports.encodeWorkspacePath = encodeWorkspacePath;
45
45
  exports.getProjectSlug = getProjectSlug;
46
+ exports.resolveProjectIdentity = resolveProjectIdentity;
46
47
  exports.getProjectSlugRaw = getProjectSlugRaw;
47
48
  const fs = __importStar(require("fs"));
48
49
  const path = __importStar(require("path"));
@@ -86,15 +87,28 @@ function encodeWorkspacePath(workspacePath) {
86
87
  * Resolves symlinks, then encodes for use as a filename.
87
88
  */
88
89
  function getProjectSlug(cwd) {
89
- const dir = cwd || process.cwd();
90
+ return resolveProjectIdentity(cwd).canonicalSlug;
91
+ }
92
+ /** Resolves the canonical project identity used by all new readers and writers. */
93
+ function resolveProjectIdentity(cwd) {
94
+ const dir = path.resolve(cwd || process.cwd());
90
95
  let resolved;
91
96
  try {
92
97
  resolved = fs.realpathSync(dir);
93
98
  }
94
99
  catch {
95
- resolved = path.resolve(dir);
100
+ resolved = dir;
96
101
  }
97
- return encodeWorkspacePath(resolved);
102
+ const canonicalSlug = encodeWorkspacePath(resolved);
103
+ const legacySlug = encodeWorkspacePath(dir);
104
+ return {
105
+ cwd: dir,
106
+ resolvedCwd: resolved,
107
+ canonicalSlug,
108
+ legacySlug,
109
+ candidates: canonicalSlug === legacySlug ? [canonicalSlug] : [canonicalSlug, legacySlug],
110
+ hasLegacyMismatch: canonicalSlug !== legacySlug,
111
+ };
98
112
  }
99
113
  /**
100
114
  * Derives a project slug WITHOUT resolving symlinks.
@@ -102,6 +116,5 @@ function getProjectSlug(cwd) {
102
116
  * Use this when reading data written by the extension.
103
117
  */
104
118
  function getProjectSlugRaw(cwd) {
105
- const dir = cwd || process.cwd();
106
- return encodeWorkspacePath(path.resolve(dir));
119
+ return resolveProjectIdentity(cwd).legacySlug;
107
120
  }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @fileoverview One-time migration of legacy raw-slug project stores.
3
+ *
4
+ * 0.23.0 switched all readers and writers to the canonical
5
+ * (symlink-resolved) project slug. Workspaces whose literal path differs
6
+ * from its realpath still hold pre-0.23 data under the legacy raw slug;
7
+ * this helper copies those stores to the canonical slug so they stay
8
+ * visible without any manual merge.
9
+ *
10
+ * @module projectMigration
11
+ */
12
+ import { type ProjectIdentity } from './paths';
13
+ /**
14
+ * Copies legacy raw-slug project stores to the canonical slug, once.
15
+ *
16
+ * Only copies when the canonical file does not exist yet, so it can never
17
+ * overwrite newer data; the legacy file is kept as a read-only fallback.
18
+ * Best-effort per store — a failed copy leaves that store on the legacy
19
+ * read path.
20
+ *
21
+ * @returns The canonical paths that were created (empty when none).
22
+ */
23
+ export declare function migrateLegacyProjectStores(cwdOrIdentity: string | ProjectIdentity): string[];
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ /**
3
+ * @fileoverview One-time migration of legacy raw-slug project stores.
4
+ *
5
+ * 0.23.0 switched all readers and writers to the canonical
6
+ * (symlink-resolved) project slug. Workspaces whose literal path differs
7
+ * from its realpath still hold pre-0.23 data under the legacy raw slug;
8
+ * this helper copies those stores to the canonical slug so they stay
9
+ * visible without any manual merge.
10
+ *
11
+ * @module projectMigration
12
+ */
13
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ var desc = Object.getOwnPropertyDescriptor(m, k);
16
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
17
+ desc = { enumerable: true, get: function() { return m[k]; } };
18
+ }
19
+ Object.defineProperty(o, k2, desc);
20
+ }) : (function(o, m, k, k2) {
21
+ if (k2 === undefined) k2 = k;
22
+ o[k2] = m[k];
23
+ }));
24
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
25
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
26
+ }) : function(o, v) {
27
+ o["default"] = v;
28
+ });
29
+ var __importStar = (this && this.__importStar) || (function () {
30
+ var ownKeys = function(o) {
31
+ ownKeys = Object.getOwnPropertyNames || function (o) {
32
+ var ar = [];
33
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
34
+ return ar;
35
+ };
36
+ return ownKeys(o);
37
+ };
38
+ return function (mod) {
39
+ if (mod && mod.__esModule) return mod;
40
+ var result = {};
41
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
42
+ __setModuleDefault(result, mod);
43
+ return result;
44
+ };
45
+ })();
46
+ Object.defineProperty(exports, "__esModule", { value: true });
47
+ exports.migrateLegacyProjectStores = migrateLegacyProjectStores;
48
+ const fs = __importStar(require("fs"));
49
+ const path = __importStar(require("path"));
50
+ const paths_1 = require("./paths");
51
+ /** Project-scoped JSON store subdomains shared by the extension and CLI. */
52
+ const PROJECT_STORE_SUBDOMAINS = [
53
+ 'tasks',
54
+ 'decisions',
55
+ 'notifications',
56
+ 'knowledge-notes',
57
+ 'plans',
58
+ ];
59
+ /**
60
+ * Copies legacy raw-slug project stores to the canonical slug, once.
61
+ *
62
+ * Only copies when the canonical file does not exist yet, so it can never
63
+ * overwrite newer data; the legacy file is kept as a read-only fallback.
64
+ * Best-effort per store — a failed copy leaves that store on the legacy
65
+ * read path.
66
+ *
67
+ * @returns The canonical paths that were created (empty when none).
68
+ */
69
+ function migrateLegacyProjectStores(cwdOrIdentity) {
70
+ const identity = typeof cwdOrIdentity === 'string' ? (0, paths_1.resolveProjectIdentity)(cwdOrIdentity) : cwdOrIdentity;
71
+ if (!identity.hasLegacyMismatch)
72
+ return [];
73
+ const pairs = PROJECT_STORE_SUBDOMAINS.map((subdomain) => [
74
+ (0, paths_1.getProjectDataPath)(identity.legacySlug, subdomain),
75
+ (0, paths_1.getProjectDataPath)(identity.canonicalSlug, subdomain),
76
+ ]);
77
+ pairs.push([
78
+ path.join((0, paths_1.getConfigDir)(), 'handoffs', `${identity.legacySlug}-latest.md`),
79
+ path.join((0, paths_1.getConfigDir)(), 'handoffs', `${identity.canonicalSlug}-latest.md`),
80
+ ]);
81
+ const migrated = [];
82
+ for (const [legacyPath, canonicalPath] of pairs) {
83
+ try {
84
+ if (!fs.existsSync(legacyPath) || fs.existsSync(canonicalPath))
85
+ continue;
86
+ fs.mkdirSync(path.dirname(canonicalPath), { recursive: true });
87
+ fs.copyFileSync(legacyPath, canonicalPath, fs.constants.COPYFILE_EXCL);
88
+ migrated.push(canonicalPath);
89
+ }
90
+ catch {
91
+ // Best effort: an unmigrated store falls back to the legacy read path.
92
+ }
93
+ }
94
+ return migrated;
95
+ }
@@ -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
  *
@@ -46,6 +46,7 @@ exports.ClaudeCodeProvider = void 0;
46
46
  const fs = __importStar(require("fs"));
47
47
  const os = __importStar(require("os"));
48
48
  const path = __importStar(require("path"));
49
+ const string_decoder_1 = require("string_decoder");
49
50
  const sessionContext_1 = require("../context/sessionContext");
50
51
  const jsonl_1 = require("../parsers/jsonl");
51
52
  const sessionPathResolver_1 = require("../parsers/sessionPathResolver");
@@ -97,6 +98,7 @@ class ClaudeCodeReader {
97
98
  filePosition = 0;
98
99
  events = [];
99
100
  _wasTruncated = false;
101
+ decoder = new string_decoder_1.StringDecoder('utf8');
100
102
  constructor(sessionPath) {
101
103
  this.sessionPath = sessionPath;
102
104
  this.parser = new jsonl_1.JsonlParser({
@@ -120,6 +122,7 @@ class ClaudeCodeReader {
120
122
  this._wasTruncated = true;
121
123
  this.filePosition = 0;
122
124
  this.parser.reset();
125
+ this.decoder = new string_decoder_1.StringDecoder('utf8');
123
126
  }
124
127
  // No new content
125
128
  if (currentSize <= this.filePosition) {
@@ -129,11 +132,16 @@ class ClaudeCodeReader {
129
132
  const fd = fs.openSync(this.sessionPath, 'r');
130
133
  const bufferSize = currentSize - this.filePosition;
131
134
  const buffer = Buffer.alloc(bufferSize);
132
- fs.readSync(fd, buffer, 0, bufferSize, this.filePosition);
133
- fs.closeSync(fd);
134
- const chunk = buffer.toString('utf-8');
135
+ let bytesRead = 0;
136
+ try {
137
+ bytesRead = fs.readSync(fd, buffer, 0, bufferSize, this.filePosition);
138
+ }
139
+ finally {
140
+ fs.closeSync(fd);
141
+ }
142
+ const chunk = this.decoder.write(buffer.subarray(0, bytesRead));
135
143
  this.parser.processChunk(chunk);
136
- this.filePosition = currentSize;
144
+ this.filePosition += bytesRead;
137
145
  }
138
146
  catch (error) {
139
147
  console.error(`ClaudeCodeReader: error reading ${this.sessionPath}: ${error}`);
@@ -147,13 +155,18 @@ class ClaudeCodeReader {
147
155
  reset() {
148
156
  this.filePosition = 0;
149
157
  this.parser.reset();
158
+ this.decoder = new string_decoder_1.StringDecoder('utf8');
150
159
  this._wasTruncated = false;
151
160
  }
152
161
  exists() {
153
162
  return fs.existsSync(this.sessionPath);
154
163
  }
155
164
  flush() {
165
+ const finalChunk = this.decoder.end();
166
+ if (finalChunk)
167
+ this.parser.processChunk(finalChunk);
156
168
  this.parser.flush();
169
+ this.decoder = new string_decoder_1.StringDecoder('utf8');
157
170
  }
158
171
  getPosition() {
159
172
  return this.filePosition;
@@ -161,6 +174,7 @@ class ClaudeCodeReader {
161
174
  seekTo(position) {
162
175
  this.filePosition = position;
163
176
  this.parser.reset();
177
+ this.decoder = new string_decoder_1.StringDecoder('utf8');
164
178
  }
165
179
  wasTruncated() {
166
180
  return this._wasTruncated;
@@ -46,6 +46,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
46
46
  exports.CodexProvider = void 0;
47
47
  const fs = __importStar(require("fs"));
48
48
  const path = __importStar(require("path"));
49
+ const string_decoder_1 = require("string_decoder");
49
50
  const EventAggregator_1 = require("../aggregation/EventAggregator");
50
51
  const sessionContext_1 = require("../context/sessionContext");
51
52
  const codexParser_1 = require("../parsers/codexParser");
@@ -131,6 +132,23 @@ function findRolloutFiles(dir) {
131
132
  }
132
133
  return results;
133
134
  }
135
+ function hasRolloutFile(dir) {
136
+ try {
137
+ if (!fs.existsSync(dir))
138
+ return false;
139
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
140
+ const fullPath = path.join(dir, entry.name);
141
+ if (entry.isDirectory() && hasRolloutFile(fullPath))
142
+ return true;
143
+ if (entry.isFile() && isRolloutFile(entry.name))
144
+ return true;
145
+ }
146
+ }
147
+ catch {
148
+ return false;
149
+ }
150
+ return false;
151
+ }
134
152
  function findRolloutFilesInConfiguredHomes() {
135
153
  const results = [];
136
154
  const seen = new Set();
@@ -148,7 +166,7 @@ function findRolloutFilesInConfiguredHomes() {
148
166
  function getSessionsDir() {
149
167
  const dirs = getSessionsDirs();
150
168
  for (const dir of dirs) {
151
- if (findRolloutFiles(dir).length > 0) {
169
+ if (hasRolloutFile(dir)) {
152
170
  return dir;
153
171
  }
154
172
  }
@@ -430,6 +448,7 @@ class CodexReader {
430
448
  filePosition = 0;
431
449
  lineBuffer = '';
432
450
  _wasTruncated = false;
451
+ decoder = new string_decoder_1.StringDecoder('utf8');
433
452
  constructor(rolloutPath, onContextWindowLimit, onRateLimits, onTokenUsage) {
434
453
  this.rolloutPath = rolloutPath;
435
454
  this.onContextWindowLimit = onContextWindowLimit;
@@ -455,6 +474,7 @@ class CodexReader {
455
474
  this.filePosition = 0;
456
475
  this.lineBuffer = '';
457
476
  this.parser.reset();
477
+ this.decoder = new string_decoder_1.StringDecoder('utf8');
458
478
  }
459
479
  // No new content
460
480
  if (currentSize <= this.filePosition)
@@ -463,10 +483,15 @@ class CodexReader {
463
483
  const fd = fs.openSync(this.rolloutPath, 'r');
464
484
  const bufferSize = currentSize - this.filePosition;
465
485
  const buffer = Buffer.alloc(bufferSize);
466
- fs.readSync(fd, buffer, 0, bufferSize, this.filePosition);
467
- fs.closeSync(fd);
468
- const chunk = buffer.toString('utf-8');
469
- this.filePosition = currentSize;
486
+ let bytesRead = 0;
487
+ try {
488
+ bytesRead = fs.readSync(fd, buffer, 0, bufferSize, this.filePosition);
489
+ }
490
+ finally {
491
+ fs.closeSync(fd);
492
+ }
493
+ const chunk = this.decoder.write(buffer.subarray(0, bytesRead));
494
+ this.filePosition += bytesRead;
470
495
  // Process lines
471
496
  const text = this.lineBuffer + chunk;
472
497
  const lines = text.split('\n');
@@ -497,11 +522,12 @@ class CodexReader {
497
522
  // Propagate last token usage snapshot to the provider
498
523
  const lastUsage = this.parser.getLastTokenUsage();
499
524
  if (lastUsage && this.onTokenUsage) {
525
+ const cacheReadTokens = lastUsage.cached_input_tokens || 0;
500
526
  this.onTokenUsage({
501
- inputTokens: lastUsage.input_tokens || 0,
527
+ inputTokens: Math.max(0, (lastUsage.input_tokens || 0) - cacheReadTokens),
502
528
  outputTokens: lastUsage.output_tokens || 0,
503
529
  cacheWriteTokens: 0,
504
- cacheReadTokens: lastUsage.cached_input_tokens || 0,
530
+ cacheReadTokens,
505
531
  reasoningTokens: lastUsage.reasoning_output_tokens || 0,
506
532
  model: this.parser.getCurrentModel() || 'unknown',
507
533
  timestamp: new Date(),
@@ -521,12 +547,14 @@ class CodexReader {
521
547
  this.filePosition = 0;
522
548
  this.lineBuffer = '';
523
549
  this.parser.reset();
550
+ this.decoder = new string_decoder_1.StringDecoder('utf8');
524
551
  this._wasTruncated = false;
525
552
  }
526
553
  exists() {
527
554
  return fs.existsSync(this.rolloutPath);
528
555
  }
529
556
  flush() {
557
+ this.lineBuffer += this.decoder.end();
530
558
  // Process any remaining data in the line buffer
531
559
  if (this.lineBuffer.trim()) {
532
560
  try {
@@ -537,6 +565,7 @@ class CodexReader {
537
565
  // Not a complete line yet
538
566
  }
539
567
  }
568
+ this.decoder = new string_decoder_1.StringDecoder('utf8');
540
569
  }
541
570
  getPosition() {
542
571
  return this.filePosition;
@@ -544,6 +573,7 @@ class CodexReader {
544
573
  seekTo(position) {
545
574
  this.filePosition = position;
546
575
  this.lineBuffer = '';
576
+ this.decoder = new string_decoder_1.StringDecoder('utf8');
547
577
  }
548
578
  wasTruncated() {
549
579
  return this._wasTruncated;
@@ -675,9 +705,10 @@ class CodexProvider {
675
705
  const db = this.ensureDb();
676
706
  if (db) {
677
707
  const cwdStats = db.getAllDistinctCwds();
708
+ const sessionsDir = getSessionsDir();
678
709
  for (const stat of cwdStats) {
679
710
  seenCwds.set(stat.cwd, {
680
- dir: getSessionsDir(),
711
+ dir: sessionsDir,
681
712
  name: stat.cwd,
682
713
  encodedName: stat.cwd,
683
714
  sessionCount: stat.count,
@@ -971,11 +1002,9 @@ class CodexProvider {
971
1002
  return this.lastTokenUsageData;
972
1003
  }
973
1004
  computeContextSize(usage) {
974
- // OpenAI's input_tokens already includes cached_input_tokens (it's a subset,
975
- // not additive like Anthropic's model). So inputTokens alone represents the
976
- // full context window usage. This differs from OpenCode's formula which adds
977
- // cacheWrite + cacheRead because Anthropic reports them separately.
978
- return usage.inputTokens;
1005
+ // Codex usage is normalized to the disjoint TokenUsage shape at the reader
1006
+ // boundary, so reconstruct the full prompt/context from uncached + cached.
1007
+ return usage.inputTokens + usage.cacheReadTokens;
979
1008
  }
980
1009
  // --- Rate limits (for vscode wrapper to build QuotaState) ---
981
1010
  /** Get the last rate limits data received from token_count events. */
@@ -2,17 +2,7 @@
2
2
  * Read-only SQLite wrapper for Codex CLI's state database.
3
3
  * Ported from sidekick-vscode/src/services/providers/CodexDatabase.ts
4
4
  */
5
- export interface CodexDbThread {
6
- id: string;
7
- rollout_path: string;
8
- cwd: string;
9
- created_at: number;
10
- updated_at: number;
11
- title?: string;
12
- tokens_used?: number;
13
- first_user_message?: string;
14
- forked_from_id?: string;
15
- }
5
+ import type { CodexDbThread } from '../types/codex';
16
6
  export declare class CodexDatabase {
17
7
  private readonly dbPath;
18
8
  private sqlite3Available;
@@ -100,6 +100,35 @@ function getMostRecentMtime(dir) {
100
100
  return 0;
101
101
  }
102
102
  }
103
+ function getMostRecentFileMtime(dir, isCandidate, depth) {
104
+ if (depth < 0)
105
+ return 0;
106
+ let latest = 0;
107
+ try {
108
+ for (const entry of fs.readdirSync(dir)) {
109
+ const filePath = path.join(dir, entry);
110
+ try {
111
+ const stats = fs.statSync(filePath);
112
+ if (stats.isDirectory?.()) {
113
+ latest = Math.max(latest, getMostRecentFileMtime(filePath, isCandidate, depth - 1));
114
+ }
115
+ else if (isCandidate(filePath)) {
116
+ latest = Math.max(latest, stats.mtime.getTime());
117
+ }
118
+ }
119
+ catch {
120
+ /* skip */
121
+ }
122
+ }
123
+ }
124
+ catch {
125
+ return 0;
126
+ }
127
+ return latest;
128
+ }
129
+ function getClaudeActivityMtime(claudeBase) {
130
+ return getMostRecentFileMtime(claudeBase, (filePath) => filePath.endsWith('.jsonl'), 2);
131
+ }
103
132
  function getOpenCodeActivityMtime() {
104
133
  const dataDir = (0, openCode_1.getOpenCodeDataDir)();
105
134
  const dbPath = path.join(dataDir, 'opencode.db');
@@ -123,7 +152,7 @@ function getCodexActivityMtime() {
123
152
  const dbMtime = getCodexStateDbMtime(codexHome);
124
153
  if (dbMtime > latest)
125
154
  latest = dbMtime;
126
- const sessionsMtime = getMostRecentMtime(path.join(codexHome, 'sessions'));
155
+ const sessionsMtime = getMostRecentFileMtime(path.join(codexHome, 'sessions'), (filePath) => path.basename(filePath).startsWith('rollout-') && filePath.endsWith('.jsonl'), 4);
127
156
  if (sessionsMtime > latest)
128
157
  latest = sessionsMtime;
129
158
  }
@@ -151,7 +180,7 @@ function getAllDetectedProviders() {
151
180
  const hasCodex = codexHomes.some((codexHome) => fs.existsSync(path.join(codexHome, 'sessions')) || hasCodexStateDb(codexHome));
152
181
  const available = [];
153
182
  if (hasClaude)
154
- available.push({ id: 'claude-code', mtime: getMostRecentMtime(claudeBase) });
183
+ available.push({ id: 'claude-code', mtime: getClaudeActivityMtime(claudeBase) });
155
184
  if (hasOpenCode)
156
185
  available.push({ id: 'opencode', mtime: getOpenCodeActivityMtime() });
157
186
  if (hasCodex)
@@ -172,7 +201,7 @@ function detectProvider(override) {
172
201
  const hasCodex = codexHomes.some((codexHome) => fs.existsSync(path.join(codexHome, 'sessions')) || hasCodexStateDb(codexHome));
173
202
  const available = [];
174
203
  if (hasClaude) {
175
- available.push({ id: 'claude-code', mtime: getMostRecentMtime(claudeBase) });
204
+ available.push({ id: 'claude-code', mtime: getClaudeActivityMtime(claudeBase) });
176
205
  }
177
206
  if (hasOpenCode) {
178
207
  available.push({ id: 'opencode', mtime: getOpenCodeActivityMtime() });
@@ -2,37 +2,7 @@
2
2
  * Read-only SQLite wrapper for OpenCode's database.
3
3
  * Ported from sidekick-vscode/src/services/providers/OpenCodeDatabase.ts
4
4
  */
5
- export interface DbProject {
6
- id: string;
7
- worktree: string;
8
- name: string | null;
9
- sandboxes: string[];
10
- time_created: number;
11
- time_updated: number;
12
- }
13
- export interface DbSession {
14
- id: string;
15
- project_id: string;
16
- title: string;
17
- directory: string;
18
- time_created: number;
19
- time_updated: number;
20
- }
21
- export interface DbMessage {
22
- id: string;
23
- session_id: string;
24
- time_created: number;
25
- time_updated: number;
26
- data: string;
27
- }
28
- export interface DbPart {
29
- id: string;
30
- message_id: string;
31
- session_id: string;
32
- time_created: number;
33
- time_updated: number;
34
- data: string;
35
- }
5
+ import type { DbMessage, DbPart, DbProject, DbSession } from '../types/opencode';
36
6
  export interface OpenCodeDbRuntimeStatus {
37
7
  available: boolean;
38
8
  kind: 'available' | 'db_missing' | 'sqlite_missing' | 'sqlite_blocked' | 'query_failed';