sidekick-shared 0.27.6 → 0.28.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 (269) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +58 -18
  3. package/dist/browser.d.ts +13 -2
  4. package/dist/browser.js +30 -2
  5. package/dist/claudePaths.d.ts +11 -0
  6. package/dist/claudePaths.js +61 -0
  7. package/dist/claudeProfiles.d.ts +2 -9
  8. package/dist/claudeProfiles.js +10 -25
  9. package/dist/codexPaths.d.ts +22 -0
  10. package/dist/codexPaths.js +67 -0
  11. package/dist/codexProfiles.d.ts +1 -4
  12. package/dist/codexProfiles.js +5 -43
  13. package/dist/doctor.js +2 -2
  14. package/dist/esm/accountChangeSignal.js +15 -0
  15. package/dist/esm/accountChanges.js +152 -0
  16. package/dist/esm/accountHealth.js +288 -0
  17. package/dist/esm/accountLaunch.js +145 -0
  18. package/dist/esm/accountLease.js +60 -0
  19. package/dist/esm/accountManager.js +606 -0
  20. package/dist/esm/accountRegistry.js +214 -0
  21. package/dist/esm/accountStatus.js +51 -0
  22. package/dist/esm/accountSwitch.js +74 -0
  23. package/dist/esm/accountSync.js +156 -0
  24. package/dist/esm/accountSyncTypes.js +4 -0
  25. package/dist/esm/accounts.js +1291 -0
  26. package/dist/esm/aggregation/EventAggregator.js +2034 -0
  27. package/dist/esm/aggregation/FrequencyTracker.js +69 -0
  28. package/dist/esm/aggregation/HeatmapTracker.js +89 -0
  29. package/dist/esm/aggregation/PatternExtractor.js +168 -0
  30. package/dist/esm/aggregation/costProvenance.js +31 -0
  31. package/dist/esm/aggregation/snapshot.js +161 -0
  32. package/dist/esm/aggregation/types.js +5 -0
  33. package/dist/esm/analytics/codeImpact.js +18 -0
  34. package/dist/esm/analytics/compactionLedger.js +37 -0
  35. package/dist/esm/analytics/qualityScore.js +112 -0
  36. package/dist/esm/autoSwitch.js +185 -0
  37. package/dist/esm/browser.js +34 -0
  38. package/dist/esm/claudeCredentialDoctor.js +239 -0
  39. package/dist/esm/claudeCredentialLedger.js +91 -0
  40. package/dist/esm/claudeCredentials.js +111 -0
  41. package/dist/esm/claudeLiveGuard.js +43 -0
  42. package/dist/esm/claudePaths.js +22 -0
  43. package/dist/esm/claudeProfiles.js +142 -0
  44. package/dist/esm/claudeRefreshLock.js +106 -0
  45. package/dist/esm/codexAuth.js +101 -0
  46. package/dist/esm/codexCredentialDoctor.js +265 -0
  47. package/dist/esm/codexCredentialStore.js +164 -0
  48. package/dist/esm/codexKeepAlive.js +112 -0
  49. package/dist/esm/codexPaths.js +172 -0
  50. package/dist/esm/codexProfiles.js +1251 -0
  51. package/dist/esm/codexQuota.js +564 -0
  52. package/dist/esm/codexQuotaWatcher.js +363 -0
  53. package/dist/esm/context/composer.js +71 -0
  54. package/dist/esm/context/sessionContext.js +546 -0
  55. package/dist/esm/credentialIO.js +283 -0
  56. package/dist/esm/credentials.js +49 -0
  57. package/dist/esm/doctor.js +305 -0
  58. package/dist/esm/ensureDefaultAccounts.js +62 -0
  59. package/dist/esm/errorHistory.js +56 -0
  60. package/dist/esm/extractors/errorTaxonomy.js +43 -0
  61. package/dist/esm/extractors/gatherAssets.js +32 -0
  62. package/dist/esm/extractors/sessionAssets.js +176 -0
  63. package/dist/esm/extractors/sources/claudeAssets.js +158 -0
  64. package/dist/esm/extractors/sources/codexAssets.js +234 -0
  65. package/dist/esm/extractors/tokenUsage.js +41 -0
  66. package/dist/esm/extractors/toolCall.js +142 -0
  67. package/dist/esm/failingTools.js +65 -0
  68. package/dist/esm/fileHead.js +27 -0
  69. package/dist/esm/formatters/eventHighlighter.js +332 -0
  70. package/dist/esm/formatters/noiseClassifier.js +209 -0
  71. package/dist/esm/formatters/sessionDump.js +328 -0
  72. package/dist/esm/formatters/toolSummary.js +226 -0
  73. package/dist/esm/formatting.js +82 -0
  74. package/dist/esm/handoffUrl.js +23 -0
  75. package/dist/esm/historicalStore.js +305 -0
  76. package/dist/esm/humanPrompt.js +88 -0
  77. package/dist/esm/index.js +213 -0
  78. package/dist/esm/keepAlive.js +189 -0
  79. package/dist/esm/liveCredentialGuard.js +33 -0
  80. package/dist/esm/modelAliases.js +51 -0
  81. package/dist/esm/modelCatalog.js +139 -0
  82. package/dist/esm/modelContext.js +280 -0
  83. package/dist/esm/modelInfo.js +832 -0
  84. package/dist/esm/multiProviderQuotaService.js +450 -0
  85. package/dist/esm/node.js +46 -0
  86. package/dist/esm/observedContextWindows.js +165 -0
  87. package/dist/esm/observedSessionCollector.js +939 -0
  88. package/dist/esm/parsers/changelogParser.js +71 -0
  89. package/dist/esm/parsers/codexHistory.js +89 -0
  90. package/dist/esm/parsers/codexParser.js +906 -0
  91. package/dist/esm/parsers/debugLogParser.js +126 -0
  92. package/dist/esm/parsers/jsonl.js +66 -0
  93. package/dist/esm/parsers/mcpToolName.js +28 -0
  94. package/dist/esm/parsers/openCodeParser.js +625 -0
  95. package/dist/esm/parsers/planExtractor.js +335 -0
  96. package/dist/esm/parsers/sessionActivityDetector.js +246 -0
  97. package/dist/esm/parsers/sessionPathResolver.js +713 -0
  98. package/dist/esm/parsers/subagentScanner.js +360 -0
  99. package/dist/esm/parsers/subagentStatsCache.js +45 -0
  100. package/dist/esm/parsers/subagentTraceParser.js +325 -0
  101. package/dist/esm/paths.js +134 -0
  102. package/dist/esm/peakHours.js +194 -0
  103. package/dist/esm/phrases.js +1330 -0
  104. package/dist/esm/pricingCatalog.js +247 -0
  105. package/dist/esm/processDetection.js +200 -0
  106. package/dist/esm/projectMigration.js +59 -0
  107. package/dist/esm/promptHistory.js +1221 -0
  108. package/dist/esm/providerFailure.js +362 -0
  109. package/dist/esm/providerIds.js +6 -0
  110. package/dist/esm/providerQuota.js +1 -0
  111. package/dist/esm/providerServiceStatus.js +145 -0
  112. package/dist/esm/providerServiceStatusTypes.js +1 -0
  113. package/dist/esm/providerStatus.js +117 -0
  114. package/dist/esm/providers/claudeCode.js +575 -0
  115. package/dist/esm/providers/claudeListing.js +121 -0
  116. package/dist/esm/providers/codex.js +1034 -0
  117. package/dist/esm/providers/codexDatabase.js +297 -0
  118. package/dist/esm/providers/codexRollouts.js +215 -0
  119. package/dist/esm/providers/detect.js +185 -0
  120. package/dist/esm/providers/diagnostics.js +83 -0
  121. package/dist/esm/providers/directoryListingCache.js +206 -0
  122. package/dist/esm/providers/factory.js +54 -0
  123. package/dist/esm/providers/openCode.js +2008 -0
  124. package/dist/esm/providers/openCodeDatabase.js +419 -0
  125. package/dist/esm/providers/rolloutWalker.js +252 -0
  126. package/dist/esm/providers/types.js +12 -0
  127. package/dist/esm/quota.js +182 -0
  128. package/dist/esm/quotaHistory.js +364 -0
  129. package/dist/esm/quotaPoller.js +230 -0
  130. package/dist/esm/quotaPresentation.js +97 -0
  131. package/dist/esm/quotaResolver.js +231 -0
  132. package/dist/esm/quotaSnapshots.js +130 -0
  133. package/dist/esm/quotaThresholds.js +85 -0
  134. package/dist/esm/readers/decisions.js +24 -0
  135. package/dist/esm/readers/handoff.js +20 -0
  136. package/dist/esm/readers/helpers.js +27 -0
  137. package/dist/esm/readers/history.js +9 -0
  138. package/dist/esm/readers/notes.js +48 -0
  139. package/dist/esm/readers/plans.js +225 -0
  140. package/dist/esm/readers/tasks.js +19 -0
  141. package/dist/esm/report/htmlHelpers.js +280 -0
  142. package/dist/esm/report/htmlReportGenerator.js +704 -0
  143. package/dist/esm/report/index.js +7 -0
  144. package/dist/esm/report/logo.js +2 -0
  145. package/dist/esm/report/openBrowser.js +19 -0
  146. package/dist/esm/report/sessionReportInputs.js +33 -0
  147. package/dist/esm/report/transcriptParser.js +83 -0
  148. package/dist/esm/report/types.js +4 -0
  149. package/dist/esm/schemas/accountManager.js +204 -0
  150. package/dist/esm/schemas/accountStatus.js +25 -0
  151. package/dist/esm/schemas/assistantTurn.js +74 -0
  152. package/dist/esm/schemas/index.js +25 -0
  153. package/dist/esm/schemas/observedSessionV1.js +77 -0
  154. package/dist/esm/schemas/providerFailure.js +37 -0
  155. package/dist/esm/schemas/quota.js +132 -0
  156. package/dist/esm/schemas/quotaHistory.js +40 -0
  157. package/dist/esm/schemas/sessionEvent.js +216 -0
  158. package/dist/esm/schemas/stateFile.js +75 -0
  159. package/dist/esm/schemas/tokenEstimation.js +7 -0
  160. package/dist/esm/schemas/transcript.js +86 -0
  161. package/dist/esm/schemas/usageNormalization.js +38 -0
  162. package/dist/esm/search/advancedFilter.js +197 -0
  163. package/dist/esm/search/sessionSearch.js +66 -0
  164. package/dist/esm/sessionFingerprint.js +40 -0
  165. package/dist/esm/sessionMonitor.js +198 -0
  166. package/dist/esm/sessionPreviews.js +522 -0
  167. package/dist/esm/sessionPromptHistory.js +254 -0
  168. package/dist/esm/sessionPromptSignals.js +364 -0
  169. package/dist/esm/sessionStats.js +160 -0
  170. package/dist/esm/sessionTokenTotals.js +98 -0
  171. package/dist/esm/sessionTranscripts.js +49 -0
  172. package/dist/esm/stateFile.js +119 -0
  173. package/dist/esm/statusline/BurnRateCalculator.js +40 -0
  174. package/dist/esm/statusline/claudeStatuslinePayload.js +148 -0
  175. package/dist/esm/statusline/formatter.js +122 -0
  176. package/dist/esm/statusline/index.js +8 -0
  177. package/dist/esm/terminalSync.js +95 -0
  178. package/dist/esm/testing/credentialGuard.js +197 -0
  179. package/dist/esm/testing/index.js +12 -0
  180. package/dist/esm/testing/sessionLogFixtures.js +128 -0
  181. package/dist/esm/tokenEstimation.js +67 -0
  182. package/dist/esm/tokenSummary.js +113 -0
  183. package/dist/esm/toolCategory.js +82 -0
  184. package/dist/esm/transcript.js +257 -0
  185. package/dist/esm/turns/assistantTurn.js +373 -0
  186. package/dist/esm/types/codex.js +23 -0
  187. package/dist/esm/types/decisionLog.js +5 -0
  188. package/dist/esm/types/historicalData.js +47 -0
  189. package/dist/esm/types/knowledgeNote.js +15 -0
  190. package/dist/esm/types/observedSessionV1.js +257 -0
  191. package/dist/esm/types/opencode.js +12 -0
  192. package/dist/esm/types/plan.js +7 -0
  193. package/dist/esm/types/sessionEvent.js +10 -0
  194. package/dist/esm/types/taskPersistence.js +12 -0
  195. package/dist/esm/usage/billingBlocks.js +148 -0
  196. package/dist/esm/usage/claudeUsageDedupe.js +189 -0
  197. package/dist/esm/usage/importSessionHistory.js +84 -0
  198. package/dist/esm/usage/subagentUsage.js +18 -0
  199. package/dist/esm/usage/usageEvents.js +402 -0
  200. package/dist/esm/usage/usageReports.js +202 -0
  201. package/dist/esm/usageNormalization.js +95 -0
  202. package/dist/esm/watchers/eventBridge.js +219 -0
  203. package/dist/esm/watchers/factory.js +63 -0
  204. package/dist/esm/watchers/jsonlTail.js +289 -0
  205. package/dist/esm/watchers/jsonlWatcher.js +435 -0
  206. package/dist/esm/watchers/providerReaderWatcher.js +118 -0
  207. package/dist/esm/watchers/sqliteWatcher.js +285 -0
  208. package/dist/esm/watchers/types.js +4 -0
  209. package/dist/esm/workspaceRoots.js +56 -0
  210. package/dist/esm/writers/atomic.js +375 -0
  211. package/dist/esm/writers/decisions.js +51 -0
  212. package/dist/esm/writers/notes.js +46 -0
  213. package/dist/esm/writers/tasks.js +63 -0
  214. package/dist/esm/zaiQuota.js +323 -0
  215. package/dist/esm/zaiQuotaApi.js +271 -0
  216. package/dist/esm/zaiQuotaWatcher.js +265 -0
  217. package/dist/fileHead.d.ts +22 -0
  218. package/dist/fileHead.js +33 -0
  219. package/dist/index.d.ts +16 -7
  220. package/dist/index.js +49 -15
  221. package/dist/node.d.ts +13 -1
  222. package/dist/node.js +43 -2
  223. package/dist/parsers/codexHistory.js +2 -2
  224. package/dist/parsers/codexParser.d.ts +12 -0
  225. package/dist/parsers/codexParser.js +34 -12
  226. package/dist/parsers/debugLogParser.js +2 -2
  227. package/dist/parsers/sessionPathResolver.js +5 -4
  228. package/dist/promptHistory.d.ts +32 -0
  229. package/dist/promptHistory.js +115 -59
  230. package/dist/providerFailure.d.ts +12 -6
  231. package/dist/providerFailure.js +45 -0
  232. package/dist/providers/claudeCode.js +7 -99
  233. package/dist/providers/claudeListing.d.ts +22 -0
  234. package/dist/providers/claudeListing.js +158 -0
  235. package/dist/providers/codex.d.ts +3 -1
  236. package/dist/providers/codex.js +26 -227
  237. package/dist/providers/codexRollouts.d.ts +44 -0
  238. package/dist/providers/codexRollouts.js +261 -0
  239. package/dist/providers/detect.js +4 -4
  240. package/dist/readers/plans.js +3 -3
  241. package/dist/schemas/index.d.ts +2 -0
  242. package/dist/schemas/index.js +7 -2
  243. package/dist/schemas/providerFailure.d.ts +154 -0
  244. package/dist/schemas/providerFailure.js +40 -0
  245. package/dist/sessionPromptHistory.d.ts +12 -7
  246. package/dist/sessionPromptHistory.js +15 -3
  247. package/dist/sessionPromptSignals.d.ts +9 -0
  248. package/dist/sessionPromptSignals.js +13 -0
  249. package/dist/testing/credentialGuard.d.ts +23 -0
  250. package/dist/testing/credentialGuard.js +233 -0
  251. package/dist/testing/index.d.ts +14 -0
  252. package/dist/testing/index.js +22 -0
  253. package/dist/testing/sessionLogFixtures.d.ts +28 -0
  254. package/dist/testing/sessionLogFixtures.js +169 -0
  255. package/dist/tokenEstimation.d.ts +2 -0
  256. package/dist/tokenEstimation.js +21 -11
  257. package/dist/toolCategory.d.ts +18 -0
  258. package/dist/toolCategory.js +86 -0
  259. package/dist/types/codex.d.ts +4 -0
  260. package/dist/usage/usageEvents.d.ts +16 -0
  261. package/dist/usage/usageEvents.js +53 -9
  262. package/dist/watchers/jsonlTail.d.ts +26 -1
  263. package/dist/watchers/jsonlTail.js +136 -28
  264. package/dist/watchers/jsonlWatcher.js +7 -0
  265. package/dist/watchers/providerReaderWatcher.js +1 -0
  266. package/dist/watchers/types.d.ts +6 -0
  267. package/dist/workspaceRoots.d.ts +23 -0
  268. package/dist/workspaceRoots.js +93 -0
  269. package/package.json +22 -2
@@ -0,0 +1,906 @@
1
+ /**
2
+ * @fileoverview Stateful parser converting Codex rollout lines to SessionEvent[].
3
+ *
4
+ * Unlike OpenCode's stateless converter, this is stateful because:
5
+ * - exec_command_begin/end and mcp_tool_call_begin/end need pairing by call_id
6
+ * - turn_context sets the current model for subsequent events
7
+ * - token_count repeats its cumulative total on duplicate events, so each
8
+ * call is counted only when `total_token_usage` changes
9
+ *
10
+ * Moved from sidekick-vscode to sidekick-shared for reuse across packages.
11
+ *
12
+ * @module parsers/codexParser
13
+ */
14
+ import { isAggregateCodexLimit } from '../types/codex';
15
+ import { normalizeToolName } from './openCodeParser';
16
+ import { normalizeProviderUsage } from '../usageNormalization';
17
+ function reportedCompaction(tokensBefore, tokensAfter) {
18
+ if (typeof tokensBefore !== 'number' ||
19
+ typeof tokensAfter !== 'number' ||
20
+ !Number.isFinite(tokensBefore) ||
21
+ !Number.isFinite(tokensAfter) ||
22
+ tokensBefore < tokensAfter ||
23
+ tokensAfter < 0) {
24
+ return {};
25
+ }
26
+ return { compaction: { tokensBefore, tokensAfter } };
27
+ }
28
+ function normalizeRateLimits(rateLimits) {
29
+ if (!rateLimits?.primary && !rateLimits?.secondary)
30
+ return undefined;
31
+ return {
32
+ primary: rateLimits.primary
33
+ ? {
34
+ usedPercent: rateLimits.primary.used_percent || 0,
35
+ windowMinutes: rateLimits.primary.window_minutes || 0,
36
+ resetsAt: rateLimits.primary.resets_at || 0,
37
+ }
38
+ : undefined,
39
+ secondary: rateLimits.secondary
40
+ ? {
41
+ usedPercent: rateLimits.secondary.used_percent || 0,
42
+ windowMinutes: rateLimits.secondary.window_minutes || 0,
43
+ resetsAt: rateLimits.secondary.resets_at || 0,
44
+ }
45
+ : undefined,
46
+ };
47
+ }
48
+ const CODEX_USAGE_FIELDS = [
49
+ 'input_tokens',
50
+ 'cached_input_tokens',
51
+ 'output_tokens',
52
+ 'reasoning_output_tokens',
53
+ 'total_tokens',
54
+ ];
55
+ function usageField(usage, field) {
56
+ const value = usage[field];
57
+ return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : 0;
58
+ }
59
+ function codexTotalOf(usage) {
60
+ return (usageField(usage, 'total_tokens') ||
61
+ usageField(usage, 'input_tokens') + usageField(usage, 'output_tokens'));
62
+ }
63
+ /**
64
+ * Resolve the usage of the single call a Codex `token_count` event reports.
65
+ *
66
+ * Codex emits `token_count` repeatedly — rate-limit refreshes and resumes
67
+ * resend the same `info` — so summing `last_token_usage` per event counts
68
+ * those calls again. `total_token_usage` is cumulative, which makes an
69
+ * unchanged total the reliable duplicate signal. When `last_token_usage` is
70
+ * missing, the call is the growth of the cumulative total (never the total
71
+ * itself). A total that drops means the counter restarted; re-baseline on it.
72
+ */
73
+ export function resolveCodexCallUsage(info, previousTotal) {
74
+ if (!info)
75
+ return { usage: null, total: previousTotal };
76
+ const last = info.last_token_usage ?? null;
77
+ const total = info.total_token_usage ?? null;
78
+ if (total && previousTotal) {
79
+ const unchanged = CODEX_USAGE_FIELDS.every((field) => usageField(total, field) === usageField(previousTotal, field));
80
+ if (unchanged)
81
+ return { usage: null, total };
82
+ if (codexTotalOf(total) < codexTotalOf(previousTotal)) {
83
+ return { usage: last ?? total, total };
84
+ }
85
+ }
86
+ if (last)
87
+ return { usage: last, total: total ?? previousTotal };
88
+ if (!total)
89
+ return { usage: null, total: previousTotal };
90
+ const delta = {};
91
+ let any = false;
92
+ for (const field of CODEX_USAGE_FIELDS) {
93
+ const value = Math.max(0, usageField(total, field) - (previousTotal ? usageField(previousTotal, field) : 0));
94
+ delta[field] = value;
95
+ if (value > 0)
96
+ any = true;
97
+ }
98
+ return { usage: any ? delta : null, total };
99
+ }
100
+ /**
101
+ * Codex-specific tool name normalization.
102
+ * Extends the shared normalizeToolName with Codex-specific mappings.
103
+ */
104
+ export function normalizeCodexToolName(name) {
105
+ if (!name)
106
+ return name;
107
+ const lower = name.toLowerCase();
108
+ if (lower === 'local_shell' || lower === 'local_shell_call')
109
+ return 'Bash';
110
+ if (lower === 'exec_command')
111
+ return 'Bash';
112
+ if (lower === 'write_stdin')
113
+ return 'Bash';
114
+ if (lower === 'request_user_input')
115
+ return 'AskUserQuestion';
116
+ if (lower === 'update_plan')
117
+ return 'UpdatePlan';
118
+ if (lower === 'web_search')
119
+ return 'WebSearch';
120
+ return normalizeToolName(name);
121
+ }
122
+ /**
123
+ * Normalizes Codex tool input fields for downstream monitor compatibility.
124
+ */
125
+ export function normalizeCodexToolInput(canonicalToolName, rawToolName, input) {
126
+ const normalized = { ...input };
127
+ normalized._sidekickRawToolName = rawToolName;
128
+ // Codex exec_command uses cmd; monitor expects command for Bash context extraction.
129
+ if (canonicalToolName === 'Bash' &&
130
+ typeof normalized.cmd === 'string' &&
131
+ normalized.command === undefined) {
132
+ normalized.command = normalized.cmd;
133
+ }
134
+ // request_user_input currently emits questions; keep as-is (compatible with AskUserQuestion extractor).
135
+ return normalized;
136
+ }
137
+ function parsePlainTextFailure(output) {
138
+ if (!output)
139
+ return false;
140
+ const exitCodeMatch = /Process exited with code\s+(-?\d+)/i.exec(output);
141
+ if (exitCodeMatch) {
142
+ return Number(exitCodeMatch[1]) !== 0;
143
+ }
144
+ return /(Permission denied|No such file|not found|SyntaxError|failed|error:)/i.test(output);
145
+ }
146
+ /**
147
+ * Extracts plain text from Codex message content.
148
+ */
149
+ function extractTextFromContent(content) {
150
+ if (typeof content === 'string')
151
+ return content;
152
+ const texts = [];
153
+ for (const part of content) {
154
+ if (part.text)
155
+ texts.push(part.text);
156
+ }
157
+ return texts.join('\n');
158
+ }
159
+ /**
160
+ * Extracts file paths from an apply_patch input string.
161
+ * Matches lines like: `*** Add File: src/math.ts` or `*** Update File: src/index.ts`
162
+ */
163
+ export function extractPatchFilePaths(input) {
164
+ const paths = [];
165
+ const re = /\*\*\* (?:Add|Update|Delete) File: (.+)/g;
166
+ let m;
167
+ while ((m = re.exec(input)) !== null) {
168
+ paths.push(m[1].trim());
169
+ }
170
+ return paths;
171
+ }
172
+ /** Default cap on each piece of per-call correlation state the parser keeps. */
173
+ export const DEFAULT_MAX_TRACKED_TOOL_CALLS = 4096;
174
+ /** Adds `key`, refreshing its position, and evicts the oldest past `max`. */
175
+ function rememberBounded(set, key, max) {
176
+ set.delete(key);
177
+ set.add(key);
178
+ while (set.size > max)
179
+ set.delete(set.values().next().value);
180
+ }
181
+ /** Sets `key`, refreshing its position, and evicts the oldest past `max`. */
182
+ function setBounded(map, key, value, max) {
183
+ map.delete(key);
184
+ map.set(key, value);
185
+ while (map.size > max)
186
+ map.delete(map.keys().next().value);
187
+ }
188
+ /**
189
+ * Stateful parser for Codex JSONL rollout lines.
190
+ *
191
+ * Maintains internal state for:
192
+ * - Session metadata (from session_meta)
193
+ * - Current model (from turn_context)
194
+ * - Pending exec commands and MCP tool calls (begin/end pairing)
195
+ * - Previous cumulative token total (to skip duplicate token_count events)
196
+ */
197
+ export class CodexRolloutParser {
198
+ sessionMeta = null;
199
+ currentModel = null;
200
+ pendingExecCommands = new Map();
201
+ pendingMcpToolCalls = new Map();
202
+ lastTokenUsage = null;
203
+ /** Cumulative `total_token_usage` from the previous token_count event. */
204
+ prevTotalUsage = null;
205
+ modelContextWindow = null;
206
+ lastRateLimits = null;
207
+ hasAggregateRateLimits = false;
208
+ inPlanMode = false;
209
+ emittedToolUseIds = new Set();
210
+ patchExpandedToolUseIds = new Map();
211
+ maxTrackedToolCalls;
212
+ constructor(options = {}) {
213
+ const max = options.maxTrackedToolCalls;
214
+ this.maxTrackedToolCalls =
215
+ typeof max === 'number' && max >= 1 ? Math.floor(max) : DEFAULT_MAX_TRACKED_TOOL_CALLS;
216
+ }
217
+ /** Get stored session metadata. */
218
+ getSessionMeta() {
219
+ return this.sessionMeta;
220
+ }
221
+ /** Get current model from turn_context. */
222
+ getCurrentModel() {
223
+ return this.currentModel;
224
+ }
225
+ /** Get the last observed token usage snapshot. */
226
+ getLastTokenUsage() {
227
+ return this.lastTokenUsage;
228
+ }
229
+ /** Get the model context window size from token_count events. */
230
+ getModelContextWindow() {
231
+ return this.modelContextWindow;
232
+ }
233
+ /** Get the last observed rate limits from token_count events. */
234
+ getLastRateLimits() {
235
+ return this.lastRateLimits;
236
+ }
237
+ /**
238
+ * Converts a single rollout line to zero or more session events.
239
+ */
240
+ convertLine(line) {
241
+ switch (line.type) {
242
+ case 'session_meta':
243
+ return this.handleSessionMeta(line.timestamp, line.payload);
244
+ case 'response_item':
245
+ return this.handleResponseItem(line.timestamp, line.payload);
246
+ case 'compacted':
247
+ return this.handleCompacted(line.timestamp, line.payload);
248
+ case 'turn_context':
249
+ return this.handleTurnContext(line.payload);
250
+ case 'event_msg':
251
+ return this.handleEventMsg(line.timestamp, line.payload);
252
+ default:
253
+ return [];
254
+ }
255
+ }
256
+ /** Reset all parser state. */
257
+ reset() {
258
+ this.sessionMeta = null;
259
+ this.currentModel = null;
260
+ this.pendingExecCommands.clear();
261
+ this.pendingMcpToolCalls.clear();
262
+ this.lastTokenUsage = null;
263
+ this.prevTotalUsage = null;
264
+ this.modelContextWindow = null;
265
+ this.lastRateLimits = null;
266
+ this.hasAggregateRateLimits = false;
267
+ this.inPlanMode = false;
268
+ this.emittedToolUseIds.clear();
269
+ this.patchExpandedToolUseIds.clear();
270
+ }
271
+ // --- Handlers ---
272
+ handleSessionMeta(timestamp, payload) {
273
+ this.sessionMeta = payload;
274
+ const text = payload.base_instructions?.text?.trim();
275
+ return [
276
+ {
277
+ type: 'system',
278
+ ...(text
279
+ ? {
280
+ message: {
281
+ role: 'system',
282
+ id: `${payload.id}:base-instructions`,
283
+ sourceLabel: 'base instructions',
284
+ content: [{ type: 'text', text }],
285
+ },
286
+ }
287
+ : {}),
288
+ timestamp,
289
+ cwd: payload.cwd || undefined,
290
+ },
291
+ ];
292
+ }
293
+ handleResponseItem(timestamp, payload) {
294
+ // payload IS the item directly (no .item wrapper)
295
+ if (!payload || !payload.type)
296
+ return [];
297
+ switch (payload.type) {
298
+ case 'message':
299
+ return this.handleMessage(timestamp, payload);
300
+ case 'reasoning':
301
+ return this.handleReasoning(timestamp, payload);
302
+ case 'function_call':
303
+ return this.handleFunctionCall(timestamp, payload);
304
+ case 'function_call_output':
305
+ return this.handleFunctionCallOutput(timestamp, payload);
306
+ case 'local_shell_call':
307
+ return this.handleLocalShellCall(timestamp, payload);
308
+ case 'custom_tool_call':
309
+ return this.handleCustomToolCall(timestamp, payload);
310
+ case 'custom_tool_call_output':
311
+ return this.handleCustomToolCallOutput(timestamp, payload);
312
+ default:
313
+ return [];
314
+ }
315
+ }
316
+ handleMessage(timestamp, item) {
317
+ const text = extractTextFromContent(item.content);
318
+ if (!text)
319
+ return [];
320
+ if (item.role === 'user') {
321
+ return [
322
+ {
323
+ type: 'user',
324
+ message: {
325
+ role: 'user',
326
+ id: item.id,
327
+ content: [{ type: 'text', text }],
328
+ },
329
+ timestamp,
330
+ },
331
+ ];
332
+ }
333
+ if (item.role === 'assistant') {
334
+ return [
335
+ {
336
+ type: 'assistant',
337
+ message: {
338
+ role: 'assistant',
339
+ id: item.id,
340
+ model: this.currentModel || undefined,
341
+ content: [{ type: 'text', text }],
342
+ },
343
+ timestamp,
344
+ },
345
+ ];
346
+ }
347
+ if (item.role === 'developer' || item.role === 'system') {
348
+ return [
349
+ {
350
+ type: 'system',
351
+ message: {
352
+ role: item.role,
353
+ id: item.id,
354
+ sourceLabel: item.role,
355
+ content: [{ type: 'text', text }],
356
+ },
357
+ timestamp,
358
+ },
359
+ ];
360
+ }
361
+ return [];
362
+ }
363
+ handleReasoning(timestamp, item) {
364
+ const summaryTexts = (item.summary || [])
365
+ .filter((s) => s.type === 'summary_text' && s.text)
366
+ .map((s) => s.text);
367
+ if (summaryTexts.length === 0)
368
+ return [];
369
+ return [
370
+ {
371
+ type: 'assistant',
372
+ message: {
373
+ role: 'assistant',
374
+ id: item.id,
375
+ model: this.currentModel || undefined,
376
+ content: [{ type: 'thinking', thinking: summaryTexts.join('\n') }],
377
+ },
378
+ timestamp,
379
+ },
380
+ ];
381
+ }
382
+ handleFunctionCall(timestamp, item) {
383
+ let parsedArgs = {};
384
+ try {
385
+ parsedArgs = JSON.parse(item.arguments);
386
+ }
387
+ catch {
388
+ parsedArgs = { raw: item.arguments };
389
+ }
390
+ const canonicalName = normalizeCodexToolName(item.name);
391
+ const normalizedInput = normalizeCodexToolInput(canonicalName, item.name, parsedArgs);
392
+ rememberBounded(this.emittedToolUseIds, item.call_id, this.maxTrackedToolCalls);
393
+ return [
394
+ {
395
+ type: 'assistant',
396
+ message: {
397
+ role: 'assistant',
398
+ id: item.id,
399
+ model: this.currentModel || undefined,
400
+ content: [
401
+ {
402
+ type: 'tool_use',
403
+ id: item.call_id,
404
+ name: canonicalName,
405
+ input: normalizedInput,
406
+ },
407
+ ],
408
+ },
409
+ timestamp,
410
+ },
411
+ ];
412
+ }
413
+ handleFunctionCallOutput(timestamp, item) {
414
+ const isError = parsePlainTextFailure(item.output);
415
+ return [
416
+ {
417
+ type: 'user',
418
+ message: {
419
+ role: 'user',
420
+ id: `${item.call_id}:result`,
421
+ content: [
422
+ {
423
+ type: 'tool_result',
424
+ tool_use_id: item.call_id,
425
+ content: item.output,
426
+ is_error: isError,
427
+ },
428
+ ],
429
+ },
430
+ timestamp,
431
+ },
432
+ ];
433
+ }
434
+ handleLocalShellCall(timestamp, item) {
435
+ const command = item.action?.command?.join(' ') || '';
436
+ rememberBounded(this.emittedToolUseIds, item.call_id, this.maxTrackedToolCalls);
437
+ return [
438
+ {
439
+ type: 'assistant',
440
+ message: {
441
+ role: 'assistant',
442
+ id: item.id,
443
+ model: this.currentModel || undefined,
444
+ content: [
445
+ {
446
+ type: 'tool_use',
447
+ id: item.call_id,
448
+ name: 'Bash',
449
+ input: {
450
+ command,
451
+ workdir: item.action?.workdir,
452
+ _sidekickRawToolName: 'local_shell_call',
453
+ },
454
+ },
455
+ ],
456
+ },
457
+ timestamp,
458
+ },
459
+ ];
460
+ }
461
+ handleCustomToolCall(timestamp, item) {
462
+ if (item.name === 'apply_patch') {
463
+ const filePaths = extractPatchFilePaths(item.input);
464
+ if (filePaths.length === 0)
465
+ return [];
466
+ const toolUseIds = filePaths.map((fp) => `${item.call_id}-${fp}`);
467
+ setBounded(this.patchExpandedToolUseIds, item.call_id, toolUseIds, this.maxTrackedToolCalls);
468
+ for (const id of toolUseIds) {
469
+ rememberBounded(this.emittedToolUseIds, id, this.maxTrackedToolCalls);
470
+ }
471
+ return filePaths.map((fp) => ({
472
+ type: 'assistant',
473
+ message: {
474
+ role: 'assistant',
475
+ id: `${item.call_id}-${fp}`,
476
+ model: this.currentModel || undefined,
477
+ content: [
478
+ {
479
+ type: 'tool_use',
480
+ id: `${item.call_id}-${fp}`,
481
+ name: 'Edit',
482
+ input: {
483
+ file_path: fp,
484
+ _sidekickRawToolName: 'apply_patch',
485
+ },
486
+ },
487
+ ],
488
+ },
489
+ timestamp,
490
+ }));
491
+ }
492
+ // Generic custom tool
493
+ let parsedInput;
494
+ try {
495
+ parsedInput = JSON.parse(item.input);
496
+ }
497
+ catch {
498
+ parsedInput = { raw: item.input };
499
+ }
500
+ const canonicalName = normalizeCodexToolName(item.name);
501
+ const normalizedInput = normalizeCodexToolInput(canonicalName, item.name, parsedInput);
502
+ rememberBounded(this.emittedToolUseIds, item.call_id, this.maxTrackedToolCalls);
503
+ return [
504
+ {
505
+ type: 'assistant',
506
+ message: {
507
+ role: 'assistant',
508
+ id: item.call_id,
509
+ model: this.currentModel || undefined,
510
+ content: [
511
+ {
512
+ type: 'tool_use',
513
+ id: item.call_id,
514
+ name: canonicalName,
515
+ input: normalizedInput,
516
+ },
517
+ ],
518
+ },
519
+ timestamp,
520
+ },
521
+ ];
522
+ }
523
+ handleCustomToolCallOutput(timestamp, item) {
524
+ let isError = false;
525
+ let duration;
526
+ try {
527
+ const parsed = JSON.parse(item.output);
528
+ isError = parsed?.metadata?.exit_code !== 0 && parsed?.metadata?.exit_code !== undefined;
529
+ if (parsed?.metadata?.duration_seconds) {
530
+ duration = Math.round(parsed.metadata.duration_seconds * 1000);
531
+ }
532
+ }
533
+ catch {
534
+ isError = parsePlainTextFailure(item.output);
535
+ }
536
+ const toolUseIds = this.patchExpandedToolUseIds.get(item.call_id) ?? [item.call_id];
537
+ this.patchExpandedToolUseIds.delete(item.call_id);
538
+ return toolUseIds.map((toolUseId) => ({
539
+ type: 'user',
540
+ message: {
541
+ role: 'user',
542
+ id: `${toolUseId}:result`,
543
+ content: [
544
+ {
545
+ type: 'tool_result',
546
+ tool_use_id: toolUseId,
547
+ content: item.output,
548
+ is_error: isError,
549
+ duration,
550
+ },
551
+ ],
552
+ },
553
+ timestamp,
554
+ }));
555
+ }
556
+ handleCompacted(timestamp, payload) {
557
+ return [
558
+ {
559
+ type: 'summary',
560
+ message: {
561
+ role: 'assistant',
562
+ id: `compacted-${timestamp}`,
563
+ content: payload.summary || 'Context compacted',
564
+ },
565
+ timestamp,
566
+ ...reportedCompaction(payload.tokens_before, payload.tokens_after),
567
+ },
568
+ ];
569
+ }
570
+ handleTurnContext(payload) {
571
+ if (payload.model) {
572
+ this.currentModel = payload.model;
573
+ }
574
+ return [];
575
+ }
576
+ handleEventMsg(timestamp, event) {
577
+ // payload IS the event directly (no .event wrapper)
578
+ if (!event || !event.type)
579
+ return [];
580
+ switch (event.type) {
581
+ case 'token_count': {
582
+ const e = event;
583
+ // Store model_context_window if provided (actual limit for current model)
584
+ if (e.info?.model_context_window) {
585
+ this.modelContextWindow = e.info.model_context_window;
586
+ }
587
+ // Store rate_limits if provided (subscription quota data). Codex interleaves
588
+ // multiple families (aggregate "codex" plus per-model ones like codex_bengalfox);
589
+ // keep the latest aggregate sample so a trailing model-specific event at 0% does
590
+ // not mask the plan quota. Fall back to a non-aggregate sample only until an
591
+ // aggregate one has been seen.
592
+ if (e.rate_limits) {
593
+ if (isAggregateCodexLimit(e.rate_limits.limit_id)) {
594
+ this.lastRateLimits = e.rate_limits;
595
+ this.hasAggregateRateLimits = true;
596
+ }
597
+ else if (!this.hasAggregateRateLimits) {
598
+ this.lastRateLimits = e.rate_limits;
599
+ }
600
+ }
601
+ // Usage data is nested under info (info can be null). Codex repeats an
602
+ // unchanged cumulative total on duplicate events; count each call once.
603
+ const resolved = resolveCodexCallUsage(e.info, this.prevTotalUsage);
604
+ this.prevTotalUsage = resolved.total;
605
+ return this.handleTokenCount(timestamp, resolved.usage, e.rate_limits);
606
+ }
607
+ // agent_message and user_message are suppressed — they duplicate
608
+ // response_item/message events which carry richer metadata (id, content parts, role).
609
+ case 'exec_command_begin': {
610
+ const e = event;
611
+ setBounded(this.pendingExecCommands, e.call_id, {
612
+ call_id: e.call_id,
613
+ command: e.command,
614
+ workdir: e.workdir,
615
+ timestamp,
616
+ }, this.maxTrackedToolCalls);
617
+ return [];
618
+ }
619
+ case 'exec_command_end': {
620
+ const e = event;
621
+ const pending = this.pendingExecCommands.get(e.call_id);
622
+ this.pendingExecCommands.delete(e.call_id);
623
+ const command = pending?.command?.join(' ') || '';
624
+ const events = [];
625
+ if (!this.emittedToolUseIds.has(e.call_id)) {
626
+ rememberBounded(this.emittedToolUseIds, e.call_id, this.maxTrackedToolCalls);
627
+ events.push({
628
+ type: 'assistant',
629
+ message: {
630
+ role: 'assistant',
631
+ id: `exec-${e.call_id}`,
632
+ model: this.currentModel || undefined,
633
+ content: [
634
+ {
635
+ type: 'tool_use',
636
+ id: e.call_id,
637
+ name: 'Bash',
638
+ input: {
639
+ command,
640
+ workdir: pending?.workdir,
641
+ _sidekickRawToolName: 'exec_command',
642
+ },
643
+ },
644
+ ],
645
+ },
646
+ timestamp: pending?.timestamp || timestamp,
647
+ });
648
+ }
649
+ const output = [e.stdout, e.stderr].filter(Boolean).join('\n') || '';
650
+ events.push({
651
+ type: 'user',
652
+ message: {
653
+ role: 'user',
654
+ id: `exec-${e.call_id}:result`,
655
+ content: [
656
+ {
657
+ type: 'tool_result',
658
+ tool_use_id: e.call_id,
659
+ content: output,
660
+ is_error: e.exit_code !== 0,
661
+ duration: e.duration_ms,
662
+ },
663
+ ],
664
+ },
665
+ timestamp,
666
+ });
667
+ return events;
668
+ }
669
+ case 'mcp_tool_call_begin': {
670
+ const e = event;
671
+ setBounded(this.pendingMcpToolCalls, e.call_id, {
672
+ call_id: e.call_id,
673
+ server_name: e.server_name,
674
+ tool_name: e.tool_name,
675
+ arguments: e.arguments,
676
+ timestamp,
677
+ }, this.maxTrackedToolCalls);
678
+ return [];
679
+ }
680
+ case 'mcp_tool_call_end': {
681
+ const e = event;
682
+ const pendingMcp = this.pendingMcpToolCalls.get(e.call_id);
683
+ this.pendingMcpToolCalls.delete(e.call_id);
684
+ const toolName = pendingMcp?.tool_name || 'McpTool';
685
+ const events = [];
686
+ if (!this.emittedToolUseIds.has(e.call_id)) {
687
+ rememberBounded(this.emittedToolUseIds, e.call_id, this.maxTrackedToolCalls);
688
+ events.push({
689
+ type: 'assistant',
690
+ message: {
691
+ role: 'assistant',
692
+ id: `mcp-${e.call_id}`,
693
+ model: this.currentModel || undefined,
694
+ content: [
695
+ {
696
+ type: 'tool_use',
697
+ id: e.call_id,
698
+ name: normalizeCodexToolName(toolName),
699
+ input: {
700
+ ...(pendingMcp?.arguments || {}),
701
+ _sidekickRawToolName: toolName,
702
+ _sidekickMcpServerName: pendingMcp?.server_name,
703
+ },
704
+ },
705
+ ],
706
+ },
707
+ timestamp: pendingMcp?.timestamp || timestamp,
708
+ });
709
+ }
710
+ events.push({
711
+ type: 'user',
712
+ message: {
713
+ role: 'user',
714
+ id: `mcp-${e.call_id}:result`,
715
+ content: [
716
+ {
717
+ type: 'tool_result',
718
+ tool_use_id: e.call_id,
719
+ content: e.result || '',
720
+ is_error: e.is_error || false,
721
+ duration: e.duration_ms,
722
+ },
723
+ ],
724
+ },
725
+ timestamp,
726
+ });
727
+ return events;
728
+ }
729
+ case 'error': {
730
+ const e = event;
731
+ return [
732
+ {
733
+ type: 'assistant',
734
+ message: {
735
+ role: 'assistant',
736
+ id: `error-${timestamp}`,
737
+ model: this.currentModel || undefined,
738
+ content: [
739
+ { type: 'text', text: `[Error${e.code ? ` (${e.code})` : ''}] ${e.message}` },
740
+ ],
741
+ },
742
+ timestamp,
743
+ },
744
+ ];
745
+ }
746
+ case 'context_compacted': {
747
+ const e = event;
748
+ return [
749
+ {
750
+ type: 'summary',
751
+ message: {
752
+ role: 'assistant',
753
+ id: `ctx-compacted-${timestamp}`,
754
+ content: e.summary || 'Context compacted',
755
+ },
756
+ timestamp,
757
+ ...reportedCompaction(e.tokens_before, e.tokens_after),
758
+ },
759
+ ];
760
+ }
761
+ case 'patch_applied': {
762
+ const e = event;
763
+ if (!e.file_path)
764
+ return [];
765
+ const patchId = `patch-${timestamp}-${e.file_path}`;
766
+ return [
767
+ {
768
+ type: 'assistant',
769
+ message: {
770
+ role: 'assistant',
771
+ id: patchId,
772
+ model: this.currentModel || undefined,
773
+ content: [
774
+ {
775
+ type: 'tool_use',
776
+ id: patchId,
777
+ name: 'Edit',
778
+ input: {
779
+ file_path: e.file_path,
780
+ additions: e.additions ?? 0,
781
+ deletions: e.deletions ?? 0,
782
+ _sidekickRawToolName: 'patch_applied',
783
+ },
784
+ },
785
+ ],
786
+ },
787
+ timestamp,
788
+ },
789
+ ];
790
+ }
791
+ case 'turn_started':
792
+ case 'turn_complete':
793
+ case 'task_started': {
794
+ const collaborationMode = event.collaboration_mode_kind;
795
+ if (collaborationMode !== 'plan' || this.inPlanMode)
796
+ return [];
797
+ this.inPlanMode = true;
798
+ return [
799
+ {
800
+ type: 'assistant',
801
+ message: {
802
+ role: 'assistant',
803
+ id: `plan-enter-${timestamp}`,
804
+ model: this.currentModel || undefined,
805
+ content: [
806
+ {
807
+ type: 'tool_use',
808
+ id: `plan-enter-${timestamp}`,
809
+ name: 'EnterPlanMode',
810
+ input: {
811
+ source: 'codex_task_started',
812
+ _sidekickRawToolName: 'task_started',
813
+ },
814
+ },
815
+ ],
816
+ },
817
+ timestamp,
818
+ },
819
+ ];
820
+ }
821
+ case 'task_complete':
822
+ if (!this.inPlanMode)
823
+ return [];
824
+ this.inPlanMode = false;
825
+ return [
826
+ {
827
+ type: 'assistant',
828
+ message: {
829
+ role: 'assistant',
830
+ id: `plan-exit-${timestamp}`,
831
+ model: this.currentModel || undefined,
832
+ content: [
833
+ {
834
+ type: 'tool_use',
835
+ id: `plan-exit-${timestamp}`,
836
+ name: 'ExitPlanMode',
837
+ input: {
838
+ source: 'codex_task_complete',
839
+ _sidekickRawToolName: 'task_complete',
840
+ },
841
+ },
842
+ ],
843
+ },
844
+ timestamp,
845
+ },
846
+ ];
847
+ case 'turn_aborted':
848
+ case 'agent_reasoning':
849
+ case 'agent_message':
850
+ case 'user_message':
851
+ case 'background':
852
+ return [];
853
+ default:
854
+ return [];
855
+ }
856
+ }
857
+ handleTokenCount(timestamp, usage, rateLimits) {
858
+ const normalizedRateLimits = normalizeRateLimits(rateLimits);
859
+ if (!usage && !normalizedRateLimits)
860
+ return [];
861
+ const normalizedUsage = usage
862
+ ? normalizeProviderUsage({
863
+ semantics: 'openai',
864
+ provider: 'openai',
865
+ source: 'codex-token-count',
866
+ model: this.currentModel || undefined,
867
+ inputTokens: usage.input_tokens,
868
+ outputTokens: usage.output_tokens,
869
+ cacheReadTokens: usage.cached_input_tokens,
870
+ reasoningTokens: usage.reasoning_output_tokens,
871
+ })
872
+ : undefined;
873
+ const mappedUsage = normalizedUsage
874
+ ? {
875
+ // Codex reports cached_input_tokens as a subset of input_tokens,
876
+ // while MessageUsage categories are disjoint. Normalize once here
877
+ // so aggregation and pricing do not count cached input twice.
878
+ input_tokens: normalizedUsage.uncachedInputTokens,
879
+ output_tokens: normalizedUsage.outputTokens,
880
+ cache_read_input_tokens: normalizedUsage.cacheReadTokens,
881
+ cache_creation_input_tokens: normalizedUsage.cacheWriteTokens,
882
+ reasoning_tokens: normalizedUsage.reasoningTokens,
883
+ }
884
+ : undefined;
885
+ if (usage) {
886
+ this.lastTokenUsage = usage;
887
+ }
888
+ return [
889
+ {
890
+ type: 'system',
891
+ message: {
892
+ role: 'system',
893
+ id: `token-count-${timestamp}`,
894
+ sourceLabel: 'token count',
895
+ model: this.currentModel || undefined,
896
+ usage: mappedUsage,
897
+ normalizedUsage,
898
+ content: [],
899
+ },
900
+ timestamp,
901
+ providerMetadata: { providerId: 'codex', source: 'codex-rollout' },
902
+ rateLimits: normalizedRateLimits,
903
+ },
904
+ ];
905
+ }
906
+ }