sidekick-shared 0.27.6 → 0.28.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 (269) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +59 -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 +222 -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 +258 -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,254 @@
1
+ /**
2
+ * Node-only whole-session view of prompt history: every human prompt of a
3
+ * Claude Code or Codex session, grouped by session, for consumers that
4
+ * classify a session as a whole (intent, conflict, issues).
5
+ *
6
+ * Sessions are read from the start with the same scanners as
7
+ * `collectPromptHistory()`, so scope rules, prompt text, first-call model and
8
+ * usage, and ordinals are identical. The replies and interaction signals that
9
+ * the prompt filter leaves out are opt-in through `include`.
10
+ *
11
+ * There is no cursor: each call returns whole sessions. A bound that stops a
12
+ * call lists the sessions it did not read in `unread`; passing them back as
13
+ * `sessionIds` continues. Scope fails closed as in `collectPromptHistory()`,
14
+ * and replies or signals that belong to an out-of-scope prompt are dropped.
15
+ */
16
+ import * as fs from 'fs';
17
+ import { DEFAULT_BOUNDS, compareStrings, emptyStats, listCandidates, resolveRoots, scanFile, } from './promptHistory';
18
+ import { createSessionRecordExtractor, } from './sessionPromptSignals';
19
+ const DEFAULT_SESSION_BOUNDS = {
20
+ maxSessions: 1000,
21
+ maxSessionBytes: 256 * 1024 * 1024,
22
+ maxTotalBytes: 256 * 1024 * 1024,
23
+ maxRecordBytes: DEFAULT_BOUNDS.maxRecordBytes,
24
+ deadlineMs: 60_000,
25
+ };
26
+ const MAX_EXCLUSIONS = 1000;
27
+ /** Collect every human prompt of each matching session, grouped by session. */
28
+ export async function collectSessionPromptHistory(options) {
29
+ const bounds = { ...DEFAULT_SESSION_BOUNDS, ...definedBounds(options.bounds) };
30
+ const include = {
31
+ signals: options.include?.signals === true,
32
+ replies: options.include?.replies === true,
33
+ };
34
+ const startedAt = Date.now();
35
+ const sessions = [];
36
+ const unread = [];
37
+ const boundsHit = new Set();
38
+ const exclusions = [];
39
+ const scanStats = emptyStats();
40
+ const stats = {
41
+ sessionsMatched: 0,
42
+ sessionsScanned: 0,
43
+ sessionsReturned: 0,
44
+ sessionsWithoutPrompts: 0,
45
+ sessionsOutOfScope: 0,
46
+ sessionsNotInteractive: 0,
47
+ sessionsTruncated: 0,
48
+ recordsOverSizeLimit: 0,
49
+ recordsMalformed: 0,
50
+ promptsMissingTimestamp: 0,
51
+ promptsOutOfScope: 0,
52
+ };
53
+ const finish = () => {
54
+ stats.sessionsReturned = sessions.length;
55
+ stats.recordsOverSizeLimit = scanStats.recordsOverSizeLimit;
56
+ stats.recordsMalformed = scanStats.recordsMalformed;
57
+ stats.promptsMissingTimestamp = scanStats.promptsMissingTimestamp;
58
+ stats.promptsOutOfScope = scanStats.promptsOutOfScope;
59
+ return {
60
+ sessions: sessions.sort(compareSessions),
61
+ unread,
62
+ boundsHit: [...boundsHit],
63
+ exclusions,
64
+ stats,
65
+ };
66
+ };
67
+ const realpaths = new Map();
68
+ const { roots, listingRoots } = resolveRoots(options.workspacePaths, options.includeWorktrees !== false, realpaths);
69
+ if (roots.length === 0)
70
+ return finish();
71
+ const selected = await selectCandidates(options, listingRoots);
72
+ stats.sessionsMatched = selected.length;
73
+ const limit = typeof options.limit === 'number' && options.limit > 0 ? Math.floor(options.limit) : Infinity;
74
+ const shouldStop = () => {
75
+ if (options.signal?.aborted)
76
+ return 'aborted';
77
+ if (Date.now() - startedAt >= bounds.deadlineMs)
78
+ return 'deadline';
79
+ return null;
80
+ };
81
+ const refOf = ({ candidate }) => ({
82
+ provider: candidate.provider,
83
+ sessionId: candidate.sessionId,
84
+ sessionKey: candidate.key,
85
+ });
86
+ let totalBytes = 0;
87
+ for (let index = 0; index < selected.length && sessions.length < limit; index++) {
88
+ const item = selected[index];
89
+ const stopAt = (bound, fromIndex) => {
90
+ boundsHit.add(bound);
91
+ unread.push(...selected.slice(fromIndex).map(refOf));
92
+ };
93
+ const stop = shouldStop();
94
+ if (stop) {
95
+ stopAt(stop, index);
96
+ break;
97
+ }
98
+ if (stats.sessionsScanned >= bounds.maxSessions) {
99
+ stopAt('maxSessions', index);
100
+ break;
101
+ }
102
+ const remainingTotal = bounds.maxTotalBytes - totalBytes;
103
+ if (stats.sessionsScanned > 0 && remainingTotal <= 0) {
104
+ stopAt('maxTotalBytes', index);
105
+ break;
106
+ }
107
+ let excludedRecords = 0;
108
+ const extractor = createSessionRecordExtractor(item.candidate.provider, include);
109
+ const context = {
110
+ roots,
111
+ // Sessions come back whole: `since` only selects files.
112
+ sinceMs: null,
113
+ stats: scanStats,
114
+ realpaths,
115
+ bounds: {
116
+ ...DEFAULT_BOUNDS,
117
+ maxRecordBytes: bounds.maxRecordBytes,
118
+ maxEntries: Number.MAX_SAFE_INTEGER,
119
+ },
120
+ shouldStop,
121
+ entriesSoFar: () => 0,
122
+ exclude: (exclusion) => {
123
+ excludedRecords++;
124
+ if (exclusions.length < MAX_EXCLUSIONS)
125
+ exclusions.push(exclusion);
126
+ },
127
+ observe: (record, info) => extractor.observe(record, info),
128
+ };
129
+ const sessionLimited = bounds.maxSessionBytes <= remainingTotal;
130
+ stats.sessionsScanned++;
131
+ let outcome;
132
+ try {
133
+ outcome = await scanFile(item.candidate, item.stat, undefined, {
134
+ bytes: sessionLimited ? bounds.maxSessionBytes : Math.max(0, remainingTotal),
135
+ bound: sessionLimited ? 'maxFileBytes' : 'maxTotalBytes',
136
+ forceProgress: true,
137
+ }, context);
138
+ }
139
+ catch {
140
+ // Changed while opening or verifying it: a later call can read it again.
141
+ unread.push(refOf(item));
142
+ continue;
143
+ }
144
+ totalBytes += outcome.bytesRead;
145
+ if (outcome.notInteractive) {
146
+ stats.sessionsNotInteractive++;
147
+ continue;
148
+ }
149
+ const stoppedBy = outcome.stoppedBy;
150
+ if (stoppedBy === 'deadline' || stoppedBy === 'aborted' || stoppedBy === 'maxTotalBytes') {
151
+ // A partial read cannot be resumed without a cursor: read it whole next call.
152
+ stopAt(stoppedBy, index);
153
+ break;
154
+ }
155
+ if (!outcome.session || (outcome.session.backlog && stoppedBy === null)) {
156
+ // Truncated or rewritten while it was being read.
157
+ unread.push(refOf(item));
158
+ continue;
159
+ }
160
+ if (outcome.inScope === 0) {
161
+ if (outcome.outOfScope > 0)
162
+ stats.sessionsOutOfScope++;
163
+ else
164
+ stats.sessionsWithoutPrompts++;
165
+ continue;
166
+ }
167
+ const truncated = stoppedBy === 'maxFileBytes';
168
+ if (truncated) {
169
+ boundsHit.add('maxSessionBytes');
170
+ stats.sessionsTruncated++;
171
+ }
172
+ sessions.push(buildSession(item.candidate, outcome.entries, outcome.session.lastOrdinal, extractor.result(), include, { truncated, excludedRecords }));
173
+ }
174
+ return finish();
175
+ }
176
+ function definedBounds(bounds) {
177
+ const result = {};
178
+ if (!bounds)
179
+ return result;
180
+ for (const key of Object.keys(DEFAULT_SESSION_BOUNDS)) {
181
+ const value = bounds[key];
182
+ if (typeof value === 'number' && !Number.isNaN(value))
183
+ result[key] = Math.max(0, value);
184
+ }
185
+ return result;
186
+ }
187
+ /** Matching session files, most recently modified first. */
188
+ async function selectCandidates(options, listingRoots) {
189
+ const sinceMs = options.since ? options.since.getTime() : null;
190
+ const providers = options.providers ?? ['claude-code', 'codex'];
191
+ let candidates = await listCandidates(providers, listingRoots, sinceMs);
192
+ if (options.sessionIds || options.sessionRefs) {
193
+ const ids = new Set(options.sessionIds ?? []);
194
+ const keys = new Set();
195
+ const refs = new Set();
196
+ for (const ref of options.sessionRefs ?? []) {
197
+ if (ref.sessionKey)
198
+ keys.add(ref.sessionKey);
199
+ else
200
+ refs.add(`${ref.provider}:${ref.sessionId}`);
201
+ }
202
+ candidates = candidates.filter((candidate) => ids.has(candidate.sessionId) ||
203
+ keys.has(candidate.key) ||
204
+ refs.has(`${candidate.provider}:${candidate.sessionId}`));
205
+ }
206
+ const selected = [];
207
+ for (const candidate of candidates) {
208
+ try {
209
+ selected.push({ candidate, stat: await fs.promises.stat(candidate.filePath) });
210
+ }
211
+ catch {
212
+ // Gone since it was listed.
213
+ }
214
+ }
215
+ return selected.sort((left, right) => right.stat.mtimeMs - left.stat.mtimeMs ||
216
+ compareStrings(left.candidate.key, right.candidate.key));
217
+ }
218
+ function buildSession(candidate, entries, lastOrdinal, extraction, include, read) {
219
+ const ordered = [...entries].sort((left, right) => left.ordinal - right.ordinal);
220
+ const returned = new Set(ordered.map((entry) => entry.ordinal));
221
+ const prompts = ordered.map(({ provider: _provider, sessionId: _sessionId, ...prompt }) => {
222
+ const reply = include.replies ? extraction.replies.get(prompt.ordinal) : undefined;
223
+ return reply ? { ...prompt, reply } : prompt;
224
+ });
225
+ const droppedPrompts = Math.max(0, lastOrdinal + 1 - ordered.length);
226
+ const session = {
227
+ provider: candidate.provider,
228
+ sessionId: candidate.sessionId,
229
+ sessionKey: candidate.key,
230
+ startedAt: extraction.firstTimestamp ?? ordered[0].timestamp,
231
+ lastActivityAt: extraction.lastTimestamp ?? ordered[ordered.length - 1].timestamp,
232
+ cwds: distinct(ordered.map((entry) => entry.cwd)),
233
+ gitBranches: distinct(ordered.map((entry) => entry.gitBranch)),
234
+ models: distinct(ordered.map((entry) => entry.model)),
235
+ prompts,
236
+ complete: !read.truncated && droppedPrompts === 0 && read.excludedRecords === 0,
237
+ truncated: read.truncated,
238
+ droppedPrompts,
239
+ excludedRecords: read.excludedRecords,
240
+ };
241
+ if (include.signals) {
242
+ // Fail closed: nothing that followed an out-of-scope prompt is returned.
243
+ session.signals = extraction.signals.filter((signal) => signal.afterOrdinal === -1 || returned.has(signal.afterOrdinal));
244
+ }
245
+ return session;
246
+ }
247
+ function distinct(values) {
248
+ return [...new Set(values.filter((value) => typeof value === 'string'))];
249
+ }
250
+ function compareSessions(left, right) {
251
+ return (Date.parse(right.lastActivityAt) - Date.parse(left.lastActivityAt) ||
252
+ compareStrings(left.provider, right.provider) ||
253
+ compareStrings(left.sessionId, right.sessionId));
254
+ }
@@ -0,0 +1,364 @@
1
+ /**
2
+ * What the human-prompt filter leaves out of a session, for consumers that
3
+ * classify a whole session: the agent's reply to each prompt and timestamped
4
+ * interaction signals (interrupts, rejected or failed tools, compactions, API
5
+ * errors, rollbacks). Pure: log records in, results out; no file system access.
6
+ *
7
+ * Records reach an extractor after the prompt scanner has seen them, with the
8
+ * ordinal of the latest human prompt, so replies and signals share the prompt
9
+ * ordinals of `collectPromptHistory()`.
10
+ */
11
+ import { parseMcpToolName } from './parsers/mcpToolName';
12
+ import { classifyTool } from './toolCategory';
13
+ /** Error text kept on a signal; tool output can be arbitrarily long. */
14
+ export const SIGNAL_TEXT_CHARS = 1024;
15
+ const CLAUDE_REJECTED_PREFIX = "The user doesn't want to proceed with this tool use";
16
+ const CLAUDE_REJECTION_REASON = /the user said:\s*([\s\S]*)$/;
17
+ const CLAUDE_INTERRUPT_PREFIX = '[Request interrupted by user';
18
+ const CLAUDE_TOOL_USE_INTERRUPT = '[Request interrupted by user for tool use]';
19
+ export function createSessionRecordExtractor(provider, include) {
20
+ return provider === 'claude-code'
21
+ ? new ClaudeRecordExtractor(include)
22
+ : new CodexRecordExtractor(include);
23
+ }
24
+ /** Timestamps, signal bookkeeping, and reply slots shared by both providers. */
25
+ class RecordExtractor {
26
+ include;
27
+ firstMs = Infinity;
28
+ lastMs = -Infinity;
29
+ firstTimestamp;
30
+ lastTimestamp;
31
+ signals = [];
32
+ replies = new Map();
33
+ constructor(include) {
34
+ this.include = include;
35
+ }
36
+ observe(record, info) {
37
+ const timestamp = validTimestamp(record.timestamp);
38
+ if (timestamp) {
39
+ const ms = Date.parse(timestamp);
40
+ if (ms < this.firstMs) {
41
+ this.firstMs = ms;
42
+ this.firstTimestamp = timestamp;
43
+ }
44
+ if (ms > this.lastMs) {
45
+ this.lastMs = ms;
46
+ this.lastTimestamp = timestamp;
47
+ }
48
+ }
49
+ this.accept(record, timestamp, info.ordinal, info.lineOffset);
50
+ }
51
+ result() {
52
+ return {
53
+ ...(this.firstTimestamp ? { firstTimestamp: this.firstTimestamp } : {}),
54
+ ...(this.lastTimestamp ? { lastTimestamp: this.lastTimestamp } : {}),
55
+ signals: [...this.signals],
56
+ replies: new Map(this.replies),
57
+ };
58
+ }
59
+ signal(kind, timestamp, ordinal, detail = {}) {
60
+ if (!this.include.signals || !timestamp)
61
+ return;
62
+ const signal = { kind, timestamp, afterOrdinal: ordinal };
63
+ if (detail.tool)
64
+ signal.tool = detail.tool;
65
+ if (detail.tool || detail.mcp) {
66
+ const mcpServer = detail.mcpServer || (detail.tool ? parseMcpToolName(detail.tool)?.serverName : undefined);
67
+ signal.toolCategory =
68
+ detail.mcp || !detail.tool ? 'mcp' : classifyTool(detail.tool, { mcpServer });
69
+ if (mcpServer)
70
+ signal.mcpServer = mcpServer;
71
+ }
72
+ if (detail.fullText) {
73
+ signal.text = detail.fullText;
74
+ }
75
+ else if (detail.text?.trim()) {
76
+ const text = detail.text.trim();
77
+ if (text.length > SIGNAL_TEXT_CHARS) {
78
+ signal.text = detachedPrefix(text, SIGNAL_TEXT_CHARS);
79
+ signal.textTruncated = true;
80
+ }
81
+ else {
82
+ signal.text = text;
83
+ }
84
+ }
85
+ this.signals.push(signal);
86
+ }
87
+ }
88
+ class ClaudeRecordExtractor extends RecordExtractor {
89
+ toolNames = new Map();
90
+ /** The reply being assembled: split records of one message share an id. */
91
+ reply = null;
92
+ /**
93
+ * A rejection without a reason is always followed by a
94
+ * `[Request interrupted by user for tool use]` record; it is one action.
95
+ */
96
+ rejectionAwaitingInterrupt = false;
97
+ lastOrdinal = -1;
98
+ accept(record, timestamp, ordinal, lineOffset) {
99
+ if (record.isSidechain === true)
100
+ return;
101
+ if (ordinal !== this.lastOrdinal) {
102
+ this.lastOrdinal = ordinal;
103
+ this.rejectionAwaitingInterrupt = false;
104
+ }
105
+ if (record.type === 'system') {
106
+ if (record.subtype === 'compact_boundary')
107
+ this.signal('compaction', timestamp, ordinal);
108
+ return;
109
+ }
110
+ const message = record.message;
111
+ if (typeof message !== 'object' || message === null)
112
+ return;
113
+ const blocks = contentBlocks(message.content);
114
+ if (record.type === 'assistant') {
115
+ this.rejectionAwaitingInterrupt = false;
116
+ for (const block of blocks) {
117
+ if (block.type === 'tool_use' && typeof block.id === 'string') {
118
+ const name = stringValue(block.name);
119
+ if (name)
120
+ this.toolNames.set(block.id, name);
121
+ }
122
+ }
123
+ if (record.isApiErrorMessage === true) {
124
+ this.signal('apiError', timestamp, ordinal, { text: blockText(blocks) });
125
+ return;
126
+ }
127
+ if (message.model === '<synthetic>')
128
+ return;
129
+ this.recordReply(blocks, stringValue(message.id) ?? `line:${lineOffset}`, timestamp, ordinal);
130
+ return;
131
+ }
132
+ if (record.type !== 'user')
133
+ return;
134
+ for (const block of blocks) {
135
+ if (block.type === 'text' && typeof block.text === 'string') {
136
+ const text = block.text.trim();
137
+ if (!text.startsWith(CLAUDE_INTERRUPT_PREFIX))
138
+ continue;
139
+ const paired = text.startsWith(CLAUDE_TOOL_USE_INTERRUPT) && this.rejectionAwaitingInterrupt;
140
+ this.rejectionAwaitingInterrupt = false;
141
+ if (!paired)
142
+ this.signal('interrupt', timestamp, ordinal);
143
+ }
144
+ else if (block.type === 'tool_result' && block.is_error === true) {
145
+ const text = toolResultText(block.content);
146
+ const tool = typeof block.tool_use_id === 'string' ? this.toolNames.get(block.tool_use_id) : undefined;
147
+ if (text.startsWith(CLAUDE_REJECTED_PREFIX)) {
148
+ const reason = text.match(CLAUDE_REJECTION_REASON)?.[1].trim();
149
+ this.signal('toolRejected', timestamp, ordinal, { tool, fullText: reason || undefined });
150
+ this.rejectionAwaitingInterrupt = !reason;
151
+ }
152
+ else {
153
+ this.signal('toolError', timestamp, ordinal, { tool, text });
154
+ }
155
+ }
156
+ }
157
+ }
158
+ recordReply(blocks, id, timestamp, ordinal) {
159
+ if (!this.include.replies || ordinal < 0 || !timestamp)
160
+ return;
161
+ const text = blockText(blocks);
162
+ if (!text.trim())
163
+ return;
164
+ const current = this.reply;
165
+ if (current && current.ordinal === ordinal && current.id === id) {
166
+ current.parts.push(text);
167
+ }
168
+ else {
169
+ this.reply = { ordinal, id, parts: [text], timestamp };
170
+ }
171
+ const reply = this.reply;
172
+ this.replies.set(ordinal, { text: reply.parts.join('\n'), timestamp: reply.timestamp });
173
+ }
174
+ }
175
+ class CodexRecordExtractor extends RecordExtractor {
176
+ /** Ordinals whose reply came from `task_complete`, which later fallbacks do not replace. */
177
+ finalReplies = new Set();
178
+ accept(record, timestamp, ordinal) {
179
+ // Every compaction writes one top-level `compacted` record; the matching
180
+ // event or item exists only in some Codex versions, so it is not counted.
181
+ if (record.type === 'compacted') {
182
+ this.signal('compaction', timestamp, ordinal);
183
+ return;
184
+ }
185
+ const payload = record.payload;
186
+ if (typeof payload !== 'object' || payload === null)
187
+ return;
188
+ if (record.type === 'response_item') {
189
+ if (payload.type === 'message' && payload.role === 'assistant') {
190
+ this.fallbackReply(blockText(contentBlocks(payload.content)), timestamp, ordinal);
191
+ }
192
+ return;
193
+ }
194
+ if (record.type !== 'event_msg')
195
+ return;
196
+ switch (payload.type) {
197
+ case 'turn_aborted':
198
+ this.signal('interrupt', timestamp, ordinal);
199
+ break;
200
+ case 'thread_rolled_back':
201
+ this.signal('rollback', timestamp, ordinal);
202
+ break;
203
+ case 'task_complete': {
204
+ const last = stringValue(payload.last_agent_message);
205
+ if (last?.trim() && this.include.replies && ordinal >= 0 && timestamp) {
206
+ this.replies.set(ordinal, { text: last, timestamp });
207
+ this.finalReplies.add(ordinal);
208
+ }
209
+ if (payload.error !== undefined && payload.error !== null) {
210
+ this.signal('apiError', timestamp, ordinal, { text: errorText(payload.error) });
211
+ }
212
+ break;
213
+ }
214
+ case 'error':
215
+ this.signal('apiError', timestamp, ordinal, { text: errorText(payload) });
216
+ break;
217
+ case 'agent_message':
218
+ this.fallbackReply(stringValue(payload.message) ?? '', timestamp, ordinal);
219
+ break;
220
+ case 'exec_command_end':
221
+ if (payload.status === 'failed' || nonZero(payload.exit_code)) {
222
+ this.signal('toolError', timestamp, ordinal, {
223
+ tool: 'exec_command',
224
+ text: commandText(payload),
225
+ });
226
+ }
227
+ break;
228
+ case 'patch_apply_end':
229
+ if (payload.success === false) {
230
+ this.signal('toolError', timestamp, ordinal, {
231
+ tool: 'apply_patch',
232
+ text: stringValue(payload.stderr) || stringValue(payload.stdout),
233
+ });
234
+ }
235
+ break;
236
+ case 'mcp_tool_call_end': {
237
+ const invocation = payload.invocation;
238
+ const failure = mcpFailure(payload.result);
239
+ if (failure !== null) {
240
+ this.signal('toolError', timestamp, ordinal, {
241
+ tool: invocation ? stringValue(invocation.tool) : undefined,
242
+ mcp: true,
243
+ mcpServer: invocation ? stringValue(invocation.server) : undefined,
244
+ text: failure,
245
+ });
246
+ }
247
+ break;
248
+ }
249
+ case 'item_completed':
250
+ this.acceptItem(payload.item, timestamp, ordinal);
251
+ break;
252
+ }
253
+ }
254
+ acceptItem(item, timestamp, ordinal) {
255
+ if (typeof item !== 'object' || item === null)
256
+ return;
257
+ const row = item;
258
+ const failed = row.status === 'failed';
259
+ switch (row.type) {
260
+ case 'AgentMessage':
261
+ this.fallbackReply(blockText(contentBlocks(row.content)), timestamp, ordinal);
262
+ break;
263
+ case 'CommandExecution':
264
+ if (failed) {
265
+ this.signal('toolError', timestamp, ordinal, {
266
+ tool: 'exec_command',
267
+ text: commandText(row),
268
+ });
269
+ }
270
+ break;
271
+ case 'FileChange':
272
+ if (failed) {
273
+ this.signal('toolError', timestamp, ordinal, {
274
+ tool: 'apply_patch',
275
+ text: stringValue(row.stderr) || stringValue(row.stdout),
276
+ });
277
+ }
278
+ break;
279
+ case 'McpToolCall':
280
+ if (failed) {
281
+ this.signal('toolError', timestamp, ordinal, {
282
+ tool: stringValue(row.tool),
283
+ mcp: true,
284
+ mcpServer: stringValue(row.server),
285
+ text: row.error !== undefined && row.error !== null
286
+ ? errorText(row.error)
287
+ : (mcpFailure(row.result) ?? undefined),
288
+ });
289
+ }
290
+ break;
291
+ }
292
+ }
293
+ /** The latest agent text of the turn, until `task_complete` settles it. */
294
+ fallbackReply(text, timestamp, ordinal) {
295
+ if (!this.include.replies || ordinal < 0 || !timestamp || !text.trim())
296
+ return;
297
+ if (this.finalReplies.has(ordinal))
298
+ return;
299
+ this.replies.set(ordinal, { text, timestamp });
300
+ }
301
+ }
302
+ /**
303
+ * The first `chars` UTF-16 units of `text` as a new string. A plain slice can
304
+ * keep the whole source string alive (V8 sliced strings), which for a tool's
305
+ * output may be megabytes per signal. A surrogate pair is never split.
306
+ */
307
+ function detachedPrefix(text, chars) {
308
+ const end = /[\uD800-\uDBFF]/.test(text.charAt(chars - 1)) ? chars - 1 : chars;
309
+ return Buffer.from(text.slice(0, end), 'utf8').toString('utf8');
310
+ }
311
+ function validTimestamp(value) {
312
+ return typeof value === 'string' && !Number.isNaN(Date.parse(value)) ? value : undefined;
313
+ }
314
+ function stringValue(value) {
315
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
316
+ }
317
+ function contentBlocks(content) {
318
+ if (typeof content === 'string')
319
+ return [{ type: 'text', text: content }];
320
+ if (!Array.isArray(content))
321
+ return [];
322
+ return content.filter((block) => typeof block === 'object' && block !== null);
323
+ }
324
+ /** Text of text-like blocks: Claude `text`, Codex `output_text`, Codex item `Text`. */
325
+ function blockText(blocks) {
326
+ return blocks
327
+ .filter((block) => (block.type === 'text' || block.type === 'output_text' || block.type === 'Text') &&
328
+ typeof block.text === 'string')
329
+ .map((block) => block.text)
330
+ .join('\n');
331
+ }
332
+ function toolResultText(content) {
333
+ return blockText(contentBlocks(content)).trim();
334
+ }
335
+ function nonZero(value) {
336
+ return typeof value === 'number' && value !== 0;
337
+ }
338
+ /** A failed command as `$ <command>` plus its output. */
339
+ function commandText(row) {
340
+ const command = Array.isArray(row.command)
341
+ ? row.command.filter((part) => typeof part === 'string').at(-1)
342
+ : stringValue(row.command);
343
+ const output = stringValue(row.aggregated_output) ?? stringValue(row.stderr) ?? stringValue(row.stdout) ?? '';
344
+ return command ? `$ ${command}\n${output}` : output;
345
+ }
346
+ function errorText(error) {
347
+ if (typeof error === 'string')
348
+ return error;
349
+ if (typeof error !== 'object' || error === null)
350
+ return undefined;
351
+ return stringValue(error.message);
352
+ }
353
+ /** The failure text of an MCP call result, or null when it succeeded. */
354
+ function mcpFailure(result) {
355
+ if (typeof result !== 'object' || result === null)
356
+ return null;
357
+ const row = result;
358
+ if ('Err' in row)
359
+ return typeof row.Err === 'string' ? row.Err : (errorText(row.Err) ?? '');
360
+ const ok = (row.Ok ?? row);
361
+ if (typeof ok !== 'object' || ok === null || ok.isError !== true)
362
+ return null;
363
+ return blockText(contentBlocks(ok.content));
364
+ }