botmux 2.85.0 → 2.86.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 (153) hide show
  1. package/dist/cli.d.ts.map +1 -1
  2. package/dist/cli.js +22 -13
  3. package/dist/cli.js.map +1 -1
  4. package/dist/core/command-handler.d.ts.map +1 -1
  5. package/dist/core/command-handler.js +209 -1
  6. package/dist/core/command-handler.js.map +1 -1
  7. package/dist/core/cost-calculator.d.ts.map +1 -1
  8. package/dist/core/cost-calculator.js +7 -106
  9. package/dist/core/cost-calculator.js.map +1 -1
  10. package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
  11. package/dist/core/dashboard-ipc-server.js +240 -2
  12. package/dist/core/dashboard-ipc-server.js.map +1 -1
  13. package/dist/core/passthrough-commands.d.ts.map +1 -1
  14. package/dist/core/passthrough-commands.js +1 -1
  15. package/dist/core/passthrough-commands.js.map +1 -1
  16. package/dist/core/role-resolver.d.ts +1 -0
  17. package/dist/core/role-resolver.d.ts.map +1 -1
  18. package/dist/core/role-resolver.js +14 -0
  19. package/dist/core/role-resolver.js.map +1 -1
  20. package/dist/daemon.d.ts.map +1 -1
  21. package/dist/daemon.js +4 -1
  22. package/dist/daemon.js.map +1 -1
  23. package/dist/dashboard/bot-onboarding.d.ts +24 -8
  24. package/dist/dashboard/bot-onboarding.d.ts.map +1 -1
  25. package/dist/dashboard/bot-onboarding.js +170 -49
  26. package/dist/dashboard/bot-onboarding.js.map +1 -1
  27. package/dist/dashboard/bot-payload.d.ts +43 -0
  28. package/dist/dashboard/bot-payload.d.ts.map +1 -0
  29. package/dist/dashboard/bot-payload.js +44 -0
  30. package/dist/dashboard/bot-payload.js.map +1 -0
  31. package/dist/dashboard/registry.d.ts +2 -0
  32. package/dist/dashboard/registry.d.ts.map +1 -1
  33. package/dist/dashboard/registry.js.map +1 -1
  34. package/dist/dashboard/web/app.d.ts.map +1 -1
  35. package/dist/dashboard/web/app.js +15 -4
  36. package/dist/dashboard/web/app.js.map +1 -1
  37. package/dist/dashboard/web/bot-defaults.d.ts +1 -0
  38. package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
  39. package/dist/dashboard/web/bot-defaults.js +122 -3
  40. package/dist/dashboard/web/bot-defaults.js.map +1 -1
  41. package/dist/dashboard/web/bot-onboarding.d.ts.map +1 -1
  42. package/dist/dashboard/web/bot-onboarding.js +60 -4
  43. package/dist/dashboard/web/bot-onboarding.js.map +1 -1
  44. package/dist/dashboard/web/groups.d.ts +2 -0
  45. package/dist/dashboard/web/groups.d.ts.map +1 -1
  46. package/dist/dashboard/web/groups.js +419 -3
  47. package/dist/dashboard/web/groups.js.map +1 -1
  48. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  49. package/dist/dashboard/web/i18n.js +631 -3
  50. package/dist/dashboard/web/i18n.js.map +1 -1
  51. package/dist/dashboard/web/insights.d.ts +2 -0
  52. package/dist/dashboard/web/insights.d.ts.map +1 -0
  53. package/dist/dashboard/web/insights.js +1523 -0
  54. package/dist/dashboard/web/insights.js.map +1 -0
  55. package/dist/dashboard/web/overview.d.ts +22 -0
  56. package/dist/dashboard/web/overview.d.ts.map +1 -1
  57. package/dist/dashboard/web/overview.js +6 -1
  58. package/dist/dashboard/web/overview.js.map +1 -1
  59. package/dist/dashboard/web/role-profile-match.d.ts +31 -0
  60. package/dist/dashboard/web/role-profile-match.d.ts.map +1 -0
  61. package/dist/dashboard/web/role-profile-match.js +58 -0
  62. package/dist/dashboard/web/role-profile-match.js.map +1 -0
  63. package/dist/dashboard/web/roles.d.ts +1 -0
  64. package/dist/dashboard/web/roles.d.ts.map +1 -1
  65. package/dist/dashboard/web/roles.js +520 -27
  66. package/dist/dashboard/web/roles.js.map +1 -1
  67. package/dist/dashboard/web/sessions.d.ts.map +1 -1
  68. package/dist/dashboard/web/sessions.js +84 -0
  69. package/dist/dashboard/web/sessions.js.map +1 -1
  70. package/dist/dashboard-web/app.js +1246 -823
  71. package/dist/dashboard-web/index.html +2 -1
  72. package/dist/dashboard-web/style.css +1085 -3
  73. package/dist/dashboard.js +273 -39
  74. package/dist/dashboard.js.map +1 -1
  75. package/dist/i18n/en.d.ts.map +1 -1
  76. package/dist/i18n/en.js +34 -1
  77. package/dist/i18n/en.js.map +1 -1
  78. package/dist/i18n/zh.d.ts.map +1 -1
  79. package/dist/i18n/zh.js +34 -1
  80. package/dist/i18n/zh.js.map +1 -1
  81. package/dist/im/lark/client.d.ts.map +1 -1
  82. package/dist/im/lark/client.js +23 -1
  83. package/dist/im/lark/client.js.map +1 -1
  84. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  85. package/dist/im/lark/event-dispatcher.js +16 -9
  86. package/dist/im/lark/event-dispatcher.js.map +1 -1
  87. package/dist/services/group-creator.d.ts +6 -0
  88. package/dist/services/group-creator.d.ts.map +1 -1
  89. package/dist/services/group-creator.js +54 -5
  90. package/dist/services/group-creator.js.map +1 -1
  91. package/dist/services/insight/antigravity-span-reader.d.ts +3 -0
  92. package/dist/services/insight/antigravity-span-reader.d.ts.map +1 -0
  93. package/dist/services/insight/antigravity-span-reader.js +249 -0
  94. package/dist/services/insight/antigravity-span-reader.js.map +1 -0
  95. package/dist/services/insight/classify.d.ts +7 -0
  96. package/dist/services/insight/classify.d.ts.map +1 -0
  97. package/dist/services/insight/classify.js +46 -0
  98. package/dist/services/insight/classify.js.map +1 -0
  99. package/dist/services/insight/claude-span-reader.d.ts +3 -0
  100. package/dist/services/insight/claude-span-reader.d.ts.map +1 -0
  101. package/dist/services/insight/claude-span-reader.js +257 -0
  102. package/dist/services/insight/claude-span-reader.js.map +1 -0
  103. package/dist/services/insight/codex-span-reader.d.ts +3 -0
  104. package/dist/services/insight/codex-span-reader.d.ts.map +1 -0
  105. package/dist/services/insight/codex-span-reader.js +290 -0
  106. package/dist/services/insight/codex-span-reader.js.map +1 -0
  107. package/dist/services/insight/intent.d.ts +5 -0
  108. package/dist/services/insight/intent.d.ts.map +1 -0
  109. package/dist/services/insight/intent.js +145 -0
  110. package/dist/services/insight/intent.js.map +1 -0
  111. package/dist/services/insight/jsonl.d.ts +10 -0
  112. package/dist/services/insight/jsonl.d.ts.map +1 -0
  113. package/dist/services/insight/jsonl.js +36 -0
  114. package/dist/services/insight/jsonl.js.map +1 -0
  115. package/dist/services/insight/prompt.d.ts +3 -0
  116. package/dist/services/insight/prompt.d.ts.map +1 -0
  117. package/dist/services/insight/prompt.js +99 -0
  118. package/dist/services/insight/prompt.js.map +1 -0
  119. package/dist/services/insight/redact.d.ts +4 -0
  120. package/dist/services/insight/redact.d.ts.map +1 -0
  121. package/dist/services/insight/redact.js +67 -0
  122. package/dist/services/insight/redact.js.map +1 -0
  123. package/dist/services/insight/report.d.ts +29 -0
  124. package/dist/services/insight/report.d.ts.map +1 -0
  125. package/dist/services/insight/report.js +1126 -0
  126. package/dist/services/insight/report.js.map +1 -0
  127. package/dist/services/insight/safe-detail.d.ts +5 -0
  128. package/dist/services/insight/safe-detail.d.ts.map +1 -0
  129. package/dist/services/insight/safe-detail.js +59 -0
  130. package/dist/services/insight/safe-detail.js.map +1 -0
  131. package/dist/services/insight/scrub.d.ts +22 -0
  132. package/dist/services/insight/scrub.d.ts.map +1 -0
  133. package/dist/services/insight/scrub.js +70 -0
  134. package/dist/services/insight/scrub.js.map +1 -0
  135. package/dist/services/insight/types.d.ts +394 -0
  136. package/dist/services/insight/types.d.ts.map +1 -0
  137. package/dist/services/insight/types.js +2 -0
  138. package/dist/services/insight/types.js.map +1 -0
  139. package/dist/services/role-profile-store.d.ts +25 -0
  140. package/dist/services/role-profile-store.d.ts.map +1 -0
  141. package/dist/services/role-profile-store.js +171 -0
  142. package/dist/services/role-profile-store.js.map +1 -0
  143. package/dist/services/transcript-resolver.d.ts +26 -0
  144. package/dist/services/transcript-resolver.d.ts.map +1 -0
  145. package/dist/services/transcript-resolver.js +111 -0
  146. package/dist/services/transcript-resolver.js.map +1 -0
  147. package/dist/setup/cli-selection.d.ts +20 -1
  148. package/dist/setup/cli-selection.d.ts.map +1 -1
  149. package/dist/setup/cli-selection.js +45 -5
  150. package/dist/setup/cli-selection.js.map +1 -1
  151. package/dist/worker.js +10 -1
  152. package/dist/worker.js.map +1 -1
  153. package/package.json +1 -1
@@ -0,0 +1,1126 @@
1
+ import { existsSync, statSync } from 'node:fs';
2
+ import { basename, isAbsolute, relative } from 'node:path';
3
+ import { resolveSessionTranscriptPath } from '../transcript-resolver.js';
4
+ import { isReadPhase, isWritePhase } from './classify.js';
5
+ import { parseAntigravityInsight } from './antigravity-span-reader.js';
6
+ import { parseClaudeInsight } from './claude-span-reader.js';
7
+ import { parseCodexInsight } from './codex-span-reader.js';
8
+ import { safeErrorMessage, toSafeSpan } from './redact.js';
9
+ import { INSIGHT_PHASES } from './types.js';
10
+ const DEFAULT_MAX_SPANS = 500;
11
+ const DEFAULT_OVERVIEW_LIMIT = 200;
12
+ const MAX_OVERVIEW_LIMIT = 500;
13
+ const DEFAULT_SLOW_THRESHOLD_MS = 60_000;
14
+ const DIAGNOSTIC_TARGET_CAP = 100;
15
+ const SEVERITY_ORDER = { bad: 0, warn: 1, info: 2 };
16
+ const SUPPORTED_CLI_IDS = new Set(['claude-code', 'seed', 'relay', 'aiden', 'codex', 'traex', 'antigravity']);
17
+ // Sized above MAX_OVERVIEW_LIMIT so a single overview pass doesn't evict its own
18
+ // earlier sessions (FIFO thrash), defeating the parse-cache amortization.
19
+ const PARSE_CACHE_MAX = 600;
20
+ const parseCache = new Map();
21
+ export function __resetInsightReportCacheForTest() {
22
+ parseCache.clear();
23
+ }
24
+ function emptyPhase() {
25
+ return {
26
+ research: { count: 0, ms: 0 },
27
+ edit: { count: 0, ms: 0 },
28
+ run: { count: 0, ms: 0 },
29
+ delegate: { count: 0, ms: 0 },
30
+ discuss: { count: 0, ms: 0 },
31
+ };
32
+ }
33
+ function emptyAgg() {
34
+ return {
35
+ totalSpans: 0,
36
+ failedSpans: 0,
37
+ slowSpans: 0,
38
+ failByTool: {},
39
+ phase: emptyPhase(),
40
+ readWriteRatio: null,
41
+ compactions: 0,
42
+ subagentCostShare: null,
43
+ };
44
+ }
45
+ function baseReport(q, detail, parsedAt) {
46
+ return {
47
+ sessionId: q.sessionId,
48
+ cliId: q.cliId ?? 'unknown',
49
+ status: 'ok',
50
+ meta: { parsedAt, partial: false, detail },
51
+ agg: emptyAgg(),
52
+ suggestions: [],
53
+ diagnostics: [],
54
+ recommendations: [],
55
+ turnDiagnostics: [],
56
+ turnTimeline: [],
57
+ };
58
+ }
59
+ function unsupported(q, detail, parsedAt, code) {
60
+ return {
61
+ ...baseReport(q, detail, parsedAt),
62
+ status: code,
63
+ error: { code, message: safeErrorMessage(code) },
64
+ };
65
+ }
66
+ function promptUnavailable(q, turnIndex, offset, limit, code) {
67
+ return {
68
+ sessionId: q.sessionId,
69
+ cliId: q.cliId ?? 'unknown',
70
+ status: code,
71
+ turnIndex,
72
+ offset,
73
+ limit,
74
+ total: 0,
75
+ hasMore: false,
76
+ error: { code, message: safeErrorMessage(code) },
77
+ };
78
+ }
79
+ function conversationUnavailable(q, offset, limit, code) {
80
+ return {
81
+ sessionId: q.sessionId,
82
+ cliId: q.cliId ?? 'unknown',
83
+ status: code,
84
+ offset,
85
+ limit,
86
+ total: 0,
87
+ hasMore: false,
88
+ messages: [],
89
+ error: { code, message: safeErrorMessage(code) },
90
+ };
91
+ }
92
+ function parseForKind(kind, path, opts = {}) {
93
+ if (kind === 'claude')
94
+ return parseClaudeInsight(path, opts);
95
+ if (kind === 'codex' || kind === 'traex')
96
+ return parseCodexInsight(path, opts);
97
+ if (kind === 'antigravity')
98
+ return parseAntigravityInsight(path, opts);
99
+ return null;
100
+ }
101
+ function isSupportedTranscriptKind(kind) {
102
+ return kind === 'claude' || kind === 'codex' || kind === 'traex' || kind === 'antigravity';
103
+ }
104
+ function cachedParseForKind(kind, path) {
105
+ let key;
106
+ try {
107
+ const st = statSync(path);
108
+ key = `${kind}:${path}:${st.mtimeMs}:${st.size}`;
109
+ }
110
+ catch {
111
+ key = `${kind}:${path}:missing`;
112
+ }
113
+ const hit = parseCache.get(key);
114
+ if (hit)
115
+ return hit;
116
+ const parsed = parseForKind(kind, path);
117
+ if (!parsed)
118
+ return null;
119
+ if (parseCache.size >= PARSE_CACHE_MAX && !parseCache.has(key)) {
120
+ const oldest = parseCache.keys().next().value;
121
+ if (oldest !== undefined)
122
+ parseCache.delete(oldest);
123
+ }
124
+ parseCache.set(key, parsed);
125
+ return parsed;
126
+ }
127
+ function aggregate(spans, compactions, slowThresholdMs) {
128
+ const agg = emptyAgg();
129
+ agg.totalSpans = spans.length;
130
+ agg.compactions = compactions;
131
+ let reads = 0;
132
+ let writes = 0;
133
+ for (const s of spans) {
134
+ const phase = INSIGHT_PHASES.includes(s.phase) ? s.phase : 'discuss';
135
+ agg.phase[phase].count++;
136
+ if (s.durationMs !== undefined)
137
+ agg.phase[phase].ms += Math.max(0, Math.round(s.durationMs));
138
+ if (s.status === 'error') {
139
+ agg.failedSpans++;
140
+ agg.failByTool[s.tool] = (agg.failByTool[s.tool] ?? 0) + 1;
141
+ }
142
+ if ((s.durationMs ?? 0) >= slowThresholdMs)
143
+ agg.slowSpans++;
144
+ if (isReadPhase(phase))
145
+ reads++;
146
+ if (isWritePhase(phase))
147
+ writes++;
148
+ }
149
+ agg.readWriteRatio = writes > 0 ? Math.round((reads / writes) * 100) / 100 : null;
150
+ return agg;
151
+ }
152
+ function topEntry(input) {
153
+ return Object.entries(input).sort((a, b) => b[1] - a[1])[0];
154
+ }
155
+ function suggestionsFor(agg, spans, slowThresholdMs) {
156
+ const out = [];
157
+ const topFail = topEntry(agg.failByTool);
158
+ if (agg.failedSpans >= 3) {
159
+ out.push({
160
+ id: 'high_tool_failure',
161
+ title: 'Tool failures are concentrated',
162
+ severity: 'bad',
163
+ evidence: [
164
+ `${agg.failedSpans} failed spans`,
165
+ topFail ? `${topFail[0]} failed ${topFail[1]} times` : 'multiple tools failed',
166
+ ],
167
+ action: 'Check the repeated failing tool first, then add preflight checks or clearer execution constraints.',
168
+ });
169
+ }
170
+ else if (agg.failedSpans > 0) {
171
+ out.push({
172
+ id: 'tool_failure_present',
173
+ title: 'Some tool calls failed',
174
+ severity: 'warn',
175
+ evidence: [`${agg.failedSpans} failed spans`],
176
+ action: 'Review the failed spans before repeating the workflow.',
177
+ });
178
+ }
179
+ const slowest = spans
180
+ .filter(s => s.durationMs !== undefined)
181
+ .sort((a, b) => (b.durationMs ?? 0) - (a.durationMs ?? 0))[0];
182
+ if (slowest && (slowest.durationMs ?? 0) >= slowThresholdMs) {
183
+ out.push({
184
+ id: 'slow_span',
185
+ title: 'A slow span dominates the session',
186
+ severity: (slowest.durationMs ?? 0) >= 180_000 ? 'bad' : 'warn',
187
+ evidence: [`${slowest.tool} ran for ${Math.round((slowest.durationMs ?? 0) / 1000)}s`],
188
+ action: 'Split or bound the slow operation, and surface timeout or progress feedback earlier.',
189
+ });
190
+ }
191
+ if (agg.readWriteRatio !== null && agg.readWriteRatio < 1) {
192
+ out.push({
193
+ id: 'low_read_write_ratio',
194
+ title: 'Edits outpaced reads',
195
+ severity: 'warn',
196
+ evidence: [`read/write ratio ${agg.readWriteRatio}`],
197
+ action: 'Add a read/search pass before edits when changing unfamiliar files.',
198
+ });
199
+ }
200
+ if (agg.compactions > 0) {
201
+ out.push({
202
+ id: 'context_compaction',
203
+ title: 'Context compaction happened',
204
+ severity: 'info',
205
+ evidence: [`compactions ${agg.compactions}`],
206
+ action: 'For long tasks, preserve decisions and checkpoints in durable notes or smaller subtasks.',
207
+ });
208
+ }
209
+ if (out.length === 0) {
210
+ out.push({
211
+ id: 'no_major_friction',
212
+ title: 'No major trace friction detected',
213
+ severity: 'info',
214
+ evidence: [`${agg.totalSpans} spans analyzed`],
215
+ action: 'Use the span timeline for spot checks or compare against slower sessions.',
216
+ });
217
+ }
218
+ return out.sort((a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]);
219
+ }
220
+ function uniqueNumbers(values, cap = DIAGNOSTIC_TARGET_CAP) {
221
+ return [...new Set(values)].slice(0, cap);
222
+ }
223
+ function diagnosticForSuggestion(s, agg, spans, visibleSpans, slowThresholdMs) {
224
+ const detail = visibleSpans !== undefined;
225
+ const visibleRaw = visibleSpans ?? [];
226
+ const target = (predicate) => {
227
+ const allMatches = spans
228
+ .map((span, index) => ({ span, index }))
229
+ .filter(x => predicate(x.span));
230
+ const visibleMatches = visibleRaw
231
+ .map((span, index) => ({ span, index }))
232
+ .filter(x => predicate(x.span))
233
+ .slice(0, DIAGNOSTIC_TARGET_CAP);
234
+ return {
235
+ spanIndexes: detail ? visibleMatches.map(x => x.index) : undefined,
236
+ turnIndexes: detail ? uniqueNumbers(visibleMatches.map(x => x.span.turnIndex)) : undefined,
237
+ matchedSpans: allMatches.length,
238
+ returnedSpans: visibleMatches.length,
239
+ };
240
+ };
241
+ const targetTools = (matches, cap = 5) => [...new Set(matches.map(x => x.tool))].slice(0, cap);
242
+ const base = (kind, reason, targets, stats) => ({
243
+ id: s.id,
244
+ suggestionId: s.id,
245
+ kind,
246
+ severity: s.severity,
247
+ title: s.title,
248
+ reason,
249
+ targets,
250
+ stats,
251
+ });
252
+ if (s.id === 'high_tool_failure' || s.id === 'tool_failure_present') {
253
+ const allErrorSpans = spans.filter(span => span.status === 'error');
254
+ const t = target(span => span.status === 'error');
255
+ const topFail = topEntry(agg.failByTool);
256
+ const tools = targetTools(allErrorSpans);
257
+ return base('tool_failure', topFail ? `${agg.failedSpans} failed spans; ${topFail[0]} failed ${topFail[1]} times.` : `${agg.failedSpans} failed spans.`, { spanIndexes: t.spanIndexes, turnIndexes: t.turnIndexes, tools }, {
258
+ failedSpans: agg.failedSpans,
259
+ matchedSpans: t.matchedSpans,
260
+ returnedSpans: t.returnedSpans,
261
+ topTool: topFail?.[0] ?? '',
262
+ topToolFailures: topFail?.[1] ?? 0,
263
+ });
264
+ }
265
+ if (s.id === 'slow_span') {
266
+ const slowSpans = spans
267
+ .filter(span => (span.durationMs ?? 0) >= slowThresholdMs)
268
+ .sort((a, b) => (b.durationMs ?? 0) - (a.durationMs ?? 0));
269
+ const slowSet = new Set(slowSpans);
270
+ const t = target(span => slowSet.has(span));
271
+ const slowest = slowSpans[0];
272
+ const tools = targetTools(slowSpans);
273
+ return base('slow_span', slowest ? `${slowSpans.length} slow spans; slowest ${slowest.tool} ran ${Math.round((slowest.durationMs ?? 0) / 1000)}s.` : 'No slow spans matched.', { spanIndexes: t.spanIndexes, turnIndexes: t.turnIndexes, tools }, {
274
+ slowSpansTotal: slowSpans.length,
275
+ matchedSpans: t.matchedSpans,
276
+ returnedSpans: t.returnedSpans,
277
+ slowThresholdMs,
278
+ slowestTool: slowest?.tool ?? '',
279
+ slowestDurationMs: slowest?.durationMs ?? 0,
280
+ });
281
+ }
282
+ if (s.id === 'low_read_write_ratio') {
283
+ const reads = spans.filter(span => isReadPhase(span.phase));
284
+ const writes = spans.filter(span => isWritePhase(span.phase));
285
+ const t = target(span => isWritePhase(span.phase) || span.phase === 'run');
286
+ const targetSpans = spans.filter(span => isWritePhase(span.phase) || span.phase === 'run');
287
+ return base('read_write_imbalance', `Read/write ratio ${agg.readWriteRatio}; ${reads.length} read spans and ${writes.length} write spans.`, { spanIndexes: t.spanIndexes, turnIndexes: t.turnIndexes, tools: targetTools(targetSpans) }, {
288
+ readWriteRatio: agg.readWriteRatio ?? '',
289
+ readSpans: reads.length,
290
+ writeSpans: writes.length,
291
+ matchedSpans: t.matchedSpans,
292
+ returnedSpans: t.returnedSpans,
293
+ });
294
+ }
295
+ if (s.id === 'context_compaction') {
296
+ return base('compaction', `${agg.compactions} context compactions happened in this session.`, {}, { compactions: agg.compactions });
297
+ }
298
+ return base('none', `${agg.totalSpans} spans analyzed; no major trace friction detected.`, {}, { totalSpans: agg.totalSpans });
299
+ }
300
+ function diagnosticsFor(suggestions, agg, spans, visibleSpans, slowThresholdMs) {
301
+ return suggestions.map(s => diagnosticForSuggestion(s, agg, spans, visibleSpans, slowThresholdMs));
302
+ }
303
+ function selectVisibleSpans(spans, maxSpans, slowThresholdMs) {
304
+ const limit = Math.max(0, Math.floor(maxSpans));
305
+ if (spans.length <= limit)
306
+ return spans;
307
+ if (limit <= 0)
308
+ return [];
309
+ const selected = new Set();
310
+ const addWhere = (predicate) => {
311
+ for (let i = 0; i < spans.length && selected.size < limit; i++) {
312
+ if (predicate(spans[i]))
313
+ selected.add(i);
314
+ }
315
+ };
316
+ addWhere(span => span.status === 'error');
317
+ addWhere(span => (span.durationMs ?? 0) >= slowThresholdMs);
318
+ addWhere(() => true);
319
+ return [...selected].sort((a, b) => a - b).map(i => spans[i]);
320
+ }
321
+ function safeFilePathLabel(path, cwd) {
322
+ const raw = path.trim();
323
+ if (!raw)
324
+ return 'unknown';
325
+ let label = raw;
326
+ if (cwd && isAbsolute(raw)) {
327
+ const rel = relative(cwd, raw);
328
+ if (rel && !rel.startsWith('..') && !isAbsolute(rel))
329
+ label = rel;
330
+ }
331
+ if (isAbsolute(label))
332
+ label = basename(label) || 'file';
333
+ label = label.replace(/\\/g, '/').replace(/\s+/g, ' ').trim();
334
+ if (!label || label === '.' || label === '..')
335
+ return 'file';
336
+ return label.length > 120 ? `.../${basename(label) || label.slice(-80)}` : label;
337
+ }
338
+ function patchLineCounts(patch) {
339
+ if (!patch)
340
+ return undefined;
341
+ let added = 0;
342
+ let removed = 0;
343
+ for (const line of patch.split('\n')) {
344
+ if (line.startsWith('+++') || line.startsWith('---'))
345
+ continue;
346
+ if (line.startsWith('+'))
347
+ added++;
348
+ else if (line.startsWith('-'))
349
+ removed++;
350
+ }
351
+ return added > 0 || removed > 0 ? { added, removed } : undefined;
352
+ }
353
+ function uniqueSortedNumbers(values, cap = DIAGNOSTIC_TARGET_CAP) {
354
+ return [...new Set(values)].sort((a, b) => a - b).slice(0, cap);
355
+ }
356
+ function buildWorkSummary(spans, safeSpans, cwd) {
357
+ const fileMap = new Map();
358
+ for (let i = 0; i < spans.length; i++) {
359
+ const span = spans[i];
360
+ for (const rawPath of span.filePaths ?? []) {
361
+ const label = safeFilePathLabel(rawPath, cwd);
362
+ const cur = fileMap.get(label) ?? {
363
+ path: label,
364
+ reads: 0,
365
+ edits: 0,
366
+ added: 0,
367
+ removed: 0,
368
+ hasLineCounts: false,
369
+ turnIndexes: [],
370
+ spanIndexes: [],
371
+ };
372
+ if (isReadPhase(span.phase))
373
+ cur.reads++;
374
+ if (isWritePhase(span.phase))
375
+ cur.edits++;
376
+ const counts = isWritePhase(span.phase) ? (span.lineCounts ?? patchLineCounts(span.patchText)) : undefined;
377
+ if (counts) {
378
+ cur.added += counts.added;
379
+ cur.removed += counts.removed;
380
+ cur.hasLineCounts = true;
381
+ }
382
+ cur.turnIndexes.push(span.turnIndex);
383
+ cur.spanIndexes.push(i);
384
+ fileMap.set(label, cur);
385
+ }
386
+ }
387
+ const fileChanges = [...fileMap.values()]
388
+ .map(row => ({
389
+ path: row.path,
390
+ reads: row.reads,
391
+ edits: row.edits,
392
+ ...(row.hasLineCounts ? { added: row.added, removed: row.removed } : {}),
393
+ turnIndexes: uniqueSortedNumbers(row.turnIndexes),
394
+ spanIndexes: uniqueSortedNumbers(row.spanIndexes),
395
+ }))
396
+ .sort((a, b) => ((b.added ?? 0) + (b.removed ?? 0)) - ((a.added ?? 0) + (a.removed ?? 0))
397
+ || b.edits - a.edits
398
+ || b.reads - a.reads
399
+ || a.path.localeCompare(b.path))
400
+ .slice(0, 100);
401
+ const commandMap = new Map();
402
+ for (let i = 0; i < (safeSpans?.length ?? 0); i++) {
403
+ const span = safeSpans[i];
404
+ const command = span.evidence?.command;
405
+ if (!command)
406
+ continue;
407
+ const key = command.text;
408
+ const cur = commandMap.get(key) ?? {
409
+ command,
410
+ count: 0,
411
+ failures: 0,
412
+ totalDurationMs: 0,
413
+ maxDurationMs: undefined,
414
+ lastStatus: span.status,
415
+ turnIndexes: [],
416
+ spanIndexes: [],
417
+ };
418
+ cur.count++;
419
+ if (span.status === 'error')
420
+ cur.failures++;
421
+ if (span.durationMs !== undefined) {
422
+ cur.totalDurationMs += span.durationMs;
423
+ cur.maxDurationMs = cur.maxDurationMs === undefined ? span.durationMs : Math.max(cur.maxDurationMs, span.durationMs);
424
+ }
425
+ cur.lastStatus = span.status;
426
+ cur.turnIndexes.push(span.turnIndex);
427
+ cur.spanIndexes.push(i);
428
+ commandMap.set(key, cur);
429
+ }
430
+ const commandsRun = [...commandMap.values()]
431
+ .map(row => ({
432
+ command: row.command,
433
+ count: row.count,
434
+ failures: row.failures,
435
+ totalDurationMs: row.totalDurationMs,
436
+ ...(row.maxDurationMs !== undefined ? { maxDurationMs: row.maxDurationMs } : {}),
437
+ lastStatus: row.lastStatus,
438
+ turnIndexes: uniqueSortedNumbers(row.turnIndexes),
439
+ spanIndexes: uniqueSortedNumbers(row.spanIndexes),
440
+ }))
441
+ .sort((a, b) => b.failures - a.failures
442
+ || b.count - a.count
443
+ || b.totalDurationMs - a.totalDurationMs
444
+ || a.command.text.localeCompare(b.command.text))
445
+ .slice(0, 100);
446
+ return { fileChanges, commandsRun };
447
+ }
448
+ function spanKey(span) {
449
+ const intent = span.intent;
450
+ return `${intent?.kind ?? 'unknown'}:${intent?.subject ?? ''}:${intent?.detail ?? ''}`;
451
+ }
452
+ function tagsForVisibleSpans(spans, slowThresholdMs) {
453
+ const counts = new Map();
454
+ for (const span of spans) {
455
+ const key = spanKey(span);
456
+ if (!key.startsWith('unknown:'))
457
+ counts.set(key, (counts.get(key) ?? 0) + 1);
458
+ }
459
+ const turnCounts = new Map();
460
+ for (const span of spans) {
461
+ const cur = turnCounts.get(span.turnIndex) ?? { reads: 0, writes: 0, runs: 0 };
462
+ if (isReadPhase(span.phase))
463
+ cur.reads++;
464
+ if (isWritePhase(span.phase))
465
+ cur.writes++;
466
+ if (span.phase === 'run')
467
+ cur.runs++;
468
+ turnCounts.set(span.turnIndex, cur);
469
+ }
470
+ return spans.map(span => {
471
+ const tags = new Set();
472
+ if (span.status === 'error')
473
+ tags.add('failure');
474
+ if ((span.durationMs ?? 0) >= slowThresholdMs)
475
+ tags.add('slow');
476
+ const key = spanKey(span);
477
+ if (!key.startsWith('unknown:') && (counts.get(key) ?? 0) > 1)
478
+ tags.add('retry');
479
+ const tc = turnCounts.get(span.turnIndex);
480
+ if (tc && tc.reads === 0 && (tc.writes > 0 || tc.runs > 0))
481
+ tags.add('read_write_imbalance');
482
+ if (tags.size > 0)
483
+ tags.add('diagnostic');
484
+ else
485
+ tags.add('normal');
486
+ return [...tags];
487
+ });
488
+ }
489
+ function buildTurnDiagnostics(visibleSpans, slowThresholdMs) {
490
+ if (!visibleSpans)
491
+ return [];
492
+ const spanTags = tagsForVisibleSpans(visibleSpans, slowThresholdMs);
493
+ const byTurn = new Map();
494
+ for (let i = 0; i < visibleSpans.length; i++) {
495
+ const span = visibleSpans[i];
496
+ const acc = byTurn.get(span.turnIndex) ?? {
497
+ reads: 0,
498
+ edits: 0,
499
+ runs: 0,
500
+ failures: 0,
501
+ durationMs: 0,
502
+ spanIndexes: [],
503
+ tags: new Set(),
504
+ };
505
+ if (isReadPhase(span.phase))
506
+ acc.reads++;
507
+ if (isWritePhase(span.phase))
508
+ acc.edits++;
509
+ if (span.phase === 'run')
510
+ acc.runs++;
511
+ if (span.status === 'error')
512
+ acc.failures++;
513
+ if (span.durationMs !== undefined)
514
+ acc.durationMs += Math.max(0, Math.round(span.durationMs));
515
+ acc.spanIndexes.push(i);
516
+ for (const tag of spanTags[i] ?? []) {
517
+ if (tag !== 'normal')
518
+ acc.tags.add(tag);
519
+ }
520
+ byTurn.set(span.turnIndex, acc);
521
+ }
522
+ return [...byTurn.entries()]
523
+ .map(([turnIndex, acc]) => {
524
+ let severity = 'info';
525
+ let id = 'turn_normal';
526
+ const params = {
527
+ reads: acc.reads,
528
+ edits: acc.edits,
529
+ runs: acc.runs,
530
+ failures: acc.failures,
531
+ durationMs: acc.durationMs,
532
+ };
533
+ if (acc.failures > 0) {
534
+ severity = 'bad';
535
+ id = 'turn_has_failures';
536
+ }
537
+ else if (acc.tags.has('slow') || acc.durationMs >= slowThresholdMs) {
538
+ severity = 'warn';
539
+ id = 'turn_has_slow_spans';
540
+ }
541
+ else if (acc.tags.has('retry')) {
542
+ severity = 'warn';
543
+ id = 'turn_has_retries';
544
+ }
545
+ return {
546
+ turnIndex,
547
+ severity,
548
+ headline: { id, params },
549
+ metrics: {
550
+ reads: acc.reads,
551
+ edits: acc.edits,
552
+ runs: acc.runs,
553
+ failures: acc.failures,
554
+ durationMs: acc.durationMs,
555
+ },
556
+ spanIndexes: acc.spanIndexes.slice(0, DIAGNOSTIC_TARGET_CAP),
557
+ tags: acc.tags.size ? [...acc.tags] : ['normal'],
558
+ };
559
+ })
560
+ .filter(d => d.severity !== 'info')
561
+ .sort((a, b) => a.turnIndex - b.turnIndex);
562
+ }
563
+ function recordFromStats(stats) {
564
+ const out = {};
565
+ for (const [key, value] of Object.entries(stats ?? {})) {
566
+ out[key] = value;
567
+ }
568
+ return out;
569
+ }
570
+ function recommendationsForDiagnostics(diagnostics) {
571
+ return diagnostics
572
+ .filter(d => d.kind !== 'none')
573
+ .map(d => {
574
+ const stats = recordFromStats(d.stats);
575
+ const base = (id, impactId, whyId, actionIds) => ({
576
+ id,
577
+ diagnosticId: d.id,
578
+ severity: d.severity,
579
+ impact: { id: impactId, params: stats },
580
+ why: { id: whyId, params: stats },
581
+ nextActions: actionIds.map(actionId => ({ id: actionId, params: stats })),
582
+ evidence: {
583
+ spanIndexes: d.targets.spanIndexes,
584
+ turnIndexes: d.targets.turnIndexes,
585
+ counts: stats,
586
+ },
587
+ });
588
+ if (d.kind === 'tool_failure') {
589
+ return base('fix_repeated_tool_failures', 'impact_failed_spans', 'why_tool_failure_concentrated', ['inspect_failed_span_details', 'add_preflight_or_timeout']);
590
+ }
591
+ if (d.kind === 'slow_span') {
592
+ return base('split_slow_operation', 'impact_slow_spans', 'why_slow_span_dominates', ['narrow_or_split_slow_operation', 'surface_timeout_or_progress']);
593
+ }
594
+ if (d.kind === 'read_write_imbalance') {
595
+ return base('add_read_pass_before_edit', 'impact_low_read_write_ratio', 'why_edits_outpaced_reads', ['read_or_search_before_editing', 'verify_assumptions_before_running']);
596
+ }
597
+ return base('preserve_context_checkpoints', 'impact_context_compaction', 'why_context_compaction_happened', ['write_checkpoints_for_long_tasks']);
598
+ });
599
+ }
600
+ function spanDetailHeadline(span) {
601
+ return {
602
+ id: span.status === 'error'
603
+ ? 'span_failed'
604
+ : span.tags?.includes('slow')
605
+ ? 'span_slow'
606
+ : 'span_completed',
607
+ params: {
608
+ tool: span.tool,
609
+ intentKind: span.intent?.kind ?? 'unknown',
610
+ subject: span.intent?.subject ?? '',
611
+ resultCategory: span.result?.category ?? 'unknown',
612
+ durationMs: span.durationMs ?? 0,
613
+ },
614
+ };
615
+ }
616
+ function attachSpanDetails(spans) {
617
+ if (!spans)
618
+ return undefined;
619
+ return spans.map((span, index) => ({
620
+ ...span,
621
+ detail: {
622
+ headline: spanDetailHeadline(span),
623
+ phase: span.phase,
624
+ status: span.status,
625
+ ...(span.durationMs !== undefined ? { durationMs: span.durationMs } : {}),
626
+ turnIndex: span.turnIndex,
627
+ tags: span.tags ?? ['normal'],
628
+ ...(span.intent ? { intent: span.intent } : {}),
629
+ ...(span.result ? { result: span.result } : {}),
630
+ ...(span.evidence ? { evidence: span.evidence } : {}),
631
+ context: {
632
+ ...(spans[index - 1]?.intent ? { previousIntent: spans[index - 1].intent } : {}),
633
+ ...(spans[index + 1]?.intent ? { nextIntent: spans[index + 1].intent } : {}),
634
+ },
635
+ },
636
+ }));
637
+ }
638
+ function timelineKindForSpan(span) {
639
+ if (span.phase === 'research')
640
+ return 'read';
641
+ if (span.phase === 'edit')
642
+ return 'edit';
643
+ if (span.phase === 'run')
644
+ return span.status === 'error' ? 'result' : 'run';
645
+ if (span.phase === 'delegate')
646
+ return 'delegate';
647
+ return 'discuss';
648
+ }
649
+ function timelineLabelForSpan(span) {
650
+ return {
651
+ id: span.status === 'error' ? 'timeline_span_failed' : 'timeline_span_completed',
652
+ params: {
653
+ tool: span.tool,
654
+ intentKind: span.intent?.kind ?? 'unknown',
655
+ subject: span.intent?.subject ?? '',
656
+ resultCategory: span.result?.category ?? 'unknown',
657
+ durationMs: span.durationMs ?? 0,
658
+ },
659
+ };
660
+ }
661
+ function metricsForTimeline(spans) {
662
+ return spans.reduce((acc, span) => {
663
+ if (isReadPhase(span.phase))
664
+ acc.reads++;
665
+ if (isWritePhase(span.phase))
666
+ acc.edits++;
667
+ if (span.phase === 'run')
668
+ acc.runs++;
669
+ if (span.status === 'error')
670
+ acc.failures++;
671
+ if (span.durationMs !== undefined)
672
+ acc.durationMs += span.durationMs;
673
+ return acc;
674
+ }, { reads: 0, edits: 0, runs: 0, failures: 0, durationMs: 0 });
675
+ }
676
+ function buildTurnTimeline(spans, turnDiagnostics, turnPrompts, turnContext) {
677
+ if (!spans)
678
+ return [];
679
+ const diagByTurn = new Map(turnDiagnostics.map(d => [d.turnIndex, d]));
680
+ const grouped = new Map();
681
+ spans.forEach((span, index) => {
682
+ const row = grouped.get(span.turnIndex) ?? [];
683
+ row.push({ span, index });
684
+ grouped.set(span.turnIndex, row);
685
+ });
686
+ return [...grouped.entries()]
687
+ .sort((a, b) => a[0] - b[0])
688
+ .map(([turnIndex, rows]) => {
689
+ const rowSpans = rows.map(r => r.span);
690
+ const diag = diagByTurn.get(turnIndex);
691
+ const tags = [...new Set(rowSpans.flatMap(s => s.tags ?? ['normal']))];
692
+ const metrics = diag?.metrics ?? metricsForTimeline(rowSpans);
693
+ return {
694
+ turnIndex,
695
+ severity: diag?.severity ?? 'info',
696
+ ...(turnPrompts?.[turnIndex] ? { prompt: turnPrompts[turnIndex] } : {}),
697
+ ...(turnContext?.[turnIndex] ? { context: turnContext[turnIndex] } : {}),
698
+ headline: diag?.headline ?? { id: 'turn_normal', params: metrics },
699
+ metrics,
700
+ tags,
701
+ events: rows.map(({ span, index }) => ({
702
+ kind: timelineKindForSpan(span),
703
+ spanIndex: index,
704
+ label: timelineLabelForSpan(span),
705
+ phase: span.phase,
706
+ status: span.status,
707
+ ...(span.durationMs !== undefined ? { durationMs: span.durationMs } : {}),
708
+ ...(span.intent ? { intent: span.intent } : {}),
709
+ ...(span.result ? { result: span.result } : {}),
710
+ ...(span.tags ? { tags: span.tags } : {}),
711
+ ...(span.evidence ? { evidence: span.evidence } : {}),
712
+ })),
713
+ };
714
+ })
715
+ .sort((a, b) => {
716
+ const sev = SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity];
717
+ return sev || a.turnIndex - b.turnIndex;
718
+ });
719
+ }
720
+ function roleForPromptSource(source) {
721
+ if (source?.kind === 'a2a_agent')
722
+ return 'a2a_agent';
723
+ if (source?.kind === 'system')
724
+ return 'system';
725
+ return 'user';
726
+ }
727
+ function authorForPromptSource(source) {
728
+ if (source?.kind === 'a2a_agent')
729
+ return source.agentName ?? source.senderName;
730
+ if (source?.kind === 'system')
731
+ return source.senderName ?? 'system';
732
+ return source?.senderName;
733
+ }
734
+ function buildConversationProjection(turns) {
735
+ const messages = [];
736
+ for (const turn of turns.slice().sort((a, b) => a.turnIndex - b.turnIndex)) {
737
+ if (turn.prompt) {
738
+ messages.push({
739
+ id: `turn-${turn.turnIndex}-prompt`,
740
+ turnIndex: turn.turnIndex,
741
+ role: roleForPromptSource(turn.prompt.source),
742
+ severity: turn.severity,
743
+ tags: turn.tags,
744
+ ...(authorForPromptSource(turn.prompt.source) ? { author: authorForPromptSource(turn.prompt.source) } : {}),
745
+ text: turn.prompt.text,
746
+ truncated: turn.prompt.truncated,
747
+ ...(turn.prompt.source ? { source: turn.prompt.source } : {}),
748
+ });
749
+ }
750
+ for (let i = 0; i < turn.events.length; i++) {
751
+ const event = turn.events[i];
752
+ const tags = event.tags ?? ['normal'];
753
+ messages.push({
754
+ id: `turn-${turn.turnIndex}-event-${event.spanIndex}-${i}`,
755
+ turnIndex: turn.turnIndex,
756
+ role: 'agent',
757
+ severity: event.status === 'error' ? 'bad' : tags.includes('slow') ? 'warn' : turn.severity,
758
+ tags,
759
+ author: 'agent',
760
+ event,
761
+ });
762
+ }
763
+ }
764
+ return messages;
765
+ }
766
+ function conversationSearchText(message) {
767
+ const parts = [
768
+ message.author,
769
+ message.text,
770
+ message.role,
771
+ message.severity,
772
+ ...(message.tags ?? []),
773
+ message.source?.senderName,
774
+ message.source?.agentName,
775
+ message.event?.intent?.kind,
776
+ message.event?.intent?.subject,
777
+ message.event?.intent?.detail,
778
+ message.event?.result?.category,
779
+ message.event?.evidence?.command?.text,
780
+ message.event?.evidence?.output?.text,
781
+ ];
782
+ return parts.filter(Boolean).join('\n').toLowerCase();
783
+ }
784
+ function filterConversationMessages(messages, opts) {
785
+ const q = opts.q?.trim().toLowerCase();
786
+ const turns = opts.turnIndexes?.length ? new Set(opts.turnIndexes.map(x => Math.max(0, Math.floor(x)))) : undefined;
787
+ return messages.filter(message => {
788
+ if (turns && !turns.has(message.turnIndex))
789
+ return false;
790
+ if (opts.role && message.role !== opts.role)
791
+ return false;
792
+ if (opts.severity && message.severity !== opts.severity)
793
+ return false;
794
+ if (opts.tag && !(message.tags ?? []).includes(opts.tag))
795
+ return false;
796
+ if (q && !conversationSearchText(message).includes(q))
797
+ return false;
798
+ return true;
799
+ });
800
+ }
801
+ function buildDetailTimelineFromParsed(parsed, slowThresholdMs) {
802
+ const tags = tagsForVisibleSpans(parsed.spans, slowThresholdMs);
803
+ const safeSpans = attachSpanDetails(parsed.spans.map((span, index) => toSafeSpan(span, parsed.firstEventMs, tags[index]))) ?? [];
804
+ const turnDiagnostics = buildTurnDiagnostics(parsed.spans, slowThresholdMs);
805
+ return buildTurnTimeline(safeSpans, turnDiagnostics, parsed.turnPrompts, parsed.turnContext);
806
+ }
807
+ function resolveAndParseForPrompt(q, promptMax) {
808
+ if (q.cliId && !SUPPORTED_CLI_IDS.has(q.cliId))
809
+ return { error: 'unsupported_cli' };
810
+ const resolved = resolveSessionTranscriptPath(q);
811
+ if (!resolved)
812
+ return { error: 'transcript_missing' };
813
+ if (!isSupportedTranscriptKind(resolved.kind))
814
+ return { error: 'unsupported_cli' };
815
+ if (!existsSync(resolved.path))
816
+ return { error: 'transcript_missing' };
817
+ try {
818
+ const parsed = parseForKind(resolved.kind, resolved.path, { promptMax });
819
+ if (!parsed)
820
+ return { error: 'unsupported_cli' };
821
+ return { parsed, kind: resolved.kind };
822
+ }
823
+ catch {
824
+ return { error: 'parse_error' };
825
+ }
826
+ }
827
+ export function buildSafeInsightTurnDetail(q, turnIndexInput, opts = {}) {
828
+ const turnIndex = Math.max(0, Math.floor(turnIndexInput));
829
+ const offset = Math.max(0, Math.floor(opts.offset ?? 0));
830
+ const limit = Math.min(Math.max(Math.floor(opts.limit ?? 4000) || 4000, 1), 20_000);
831
+ const resolved = resolveAndParseForPrompt(q, Number.MAX_SAFE_INTEGER);
832
+ if ('error' in resolved)
833
+ return promptUnavailable(q, turnIndex, offset, limit, resolved.error);
834
+ const parsed = resolved.parsed;
835
+ const prompt = parsed.turnPrompts?.[turnIndex];
836
+ const total = prompt?.text.length ?? 0;
837
+ const text = prompt ? prompt.text.slice(offset, offset + limit) : '';
838
+ const nextOffset = offset + text.length;
839
+ const hasMore = nextOffset < total;
840
+ const turnTimeline = buildDetailTimelineFromParsed(parsed, DEFAULT_SLOW_THRESHOLD_MS);
841
+ const messages = buildConversationProjection(turnTimeline).filter(m => m.turnIndex === turnIndex && m.role === 'agent');
842
+ return {
843
+ sessionId: q.sessionId,
844
+ cliId: q.cliId ?? 'unknown',
845
+ status: 'ok',
846
+ turnIndex,
847
+ offset,
848
+ limit,
849
+ total,
850
+ ...(hasMore ? { nextOffset } : {}),
851
+ hasMore,
852
+ ...(prompt ? {
853
+ prompt: {
854
+ text,
855
+ truncated: hasMore,
856
+ ...(prompt.source ? { source: prompt.source } : {}),
857
+ },
858
+ } : {}),
859
+ messages,
860
+ };
861
+ }
862
+ export function buildSafeInsightConversation(q, opts = {}) {
863
+ const offset = Math.max(0, Math.floor(opts.offset ?? 0));
864
+ const limit = Math.min(Math.max(Math.floor(opts.limit ?? 50) || 50, 1), 200);
865
+ const resolved = resolveAndParseForPrompt(q, 400);
866
+ if ('error' in resolved)
867
+ return conversationUnavailable(q, offset, limit, resolved.error);
868
+ const turnTimeline = buildDetailTimelineFromParsed(resolved.parsed, DEFAULT_SLOW_THRESHOLD_MS);
869
+ const all = filterConversationMessages(buildConversationProjection(turnTimeline), opts);
870
+ const messages = all.slice(offset, offset + limit);
871
+ const nextOffset = offset + messages.length;
872
+ const hasMore = nextOffset < all.length;
873
+ return {
874
+ sessionId: q.sessionId,
875
+ cliId: q.cliId ?? 'unknown',
876
+ status: 'ok',
877
+ offset,
878
+ limit,
879
+ total: all.length,
880
+ ...(hasMore ? { nextOffset } : {}),
881
+ hasMore,
882
+ messages,
883
+ };
884
+ }
885
+ export function buildSafeInsightReport(q, opts = {}) {
886
+ const detail = opts.detail ?? 'summary';
887
+ const parsedAt = (opts.now?.() ?? new Date()).toISOString();
888
+ if (q.cliId && !SUPPORTED_CLI_IDS.has(q.cliId))
889
+ return unsupported(q, detail, parsedAt, 'unsupported_cli');
890
+ const resolved = resolveSessionTranscriptPath(q);
891
+ if (!resolved)
892
+ return unsupported(q, detail, parsedAt, 'transcript_missing');
893
+ if (!isSupportedTranscriptKind(resolved.kind))
894
+ return unsupported(q, detail, parsedAt, 'unsupported_cli');
895
+ if (!existsSync(resolved.path))
896
+ return unsupported(q, detail, parsedAt, 'transcript_missing');
897
+ let parsed;
898
+ try {
899
+ const maybe = cachedParseForKind(resolved.kind, resolved.path);
900
+ if (!maybe)
901
+ return unsupported(q, detail, parsedAt, 'unsupported_cli');
902
+ parsed = maybe;
903
+ }
904
+ catch {
905
+ return unsupported(q, detail, parsedAt, 'parse_error');
906
+ }
907
+ const slowThresholdMs = opts.slowThresholdMs ?? DEFAULT_SLOW_THRESHOLD_MS;
908
+ const maxSpans = opts.maxSpans ?? DEFAULT_MAX_SPANS;
909
+ const agg = aggregate(parsed.spans, parsed.compactions, slowThresholdMs);
910
+ const suggestions = suggestionsFor(agg, parsed.spans, slowThresholdMs);
911
+ const visibleRaw = detail === 'spans'
912
+ ? selectVisibleSpans(parsed.spans, maxSpans, slowThresholdMs)
913
+ : undefined;
914
+ const visibleTags = visibleRaw ? tagsForVisibleSpans(visibleRaw, slowThresholdMs) : undefined;
915
+ const visible = attachSpanDetails(visibleRaw?.map((s, index) => toSafeSpan(s, parsed.firstEventMs, visibleTags?.[index])));
916
+ const diagnostics = diagnosticsFor(suggestions, agg, parsed.spans, visibleRaw, slowThresholdMs);
917
+ const turnDiagnostics = buildTurnDiagnostics(visibleRaw, slowThresholdMs);
918
+ const turnTimeline = buildTurnTimeline(visible, turnDiagnostics, detail === 'spans' ? parsed.turnPrompts : undefined, detail === 'spans' ? parsed.turnContext : undefined);
919
+ const report = {
920
+ sessionId: q.sessionId,
921
+ cliId: q.cliId ?? 'unknown',
922
+ status: 'ok',
923
+ meta: {
924
+ parsedAt,
925
+ asOf: parsed.asOf,
926
+ partial: parsed.partial,
927
+ detail,
928
+ spansTotal: parsed.spans.length,
929
+ },
930
+ agg,
931
+ suggestions,
932
+ diagnostics,
933
+ recommendations: recommendationsForDiagnostics(diagnostics),
934
+ turnDiagnostics,
935
+ turnTimeline,
936
+ };
937
+ if (detail === 'spans' && visible) {
938
+ report.spans = visible;
939
+ report.meta.spansReturned = visible.length;
940
+ report.meta.capped = parsed.spans.length > visible.length;
941
+ report.workSummary = buildWorkSummary(visibleRaw ?? [], visible, q.cwd);
942
+ }
943
+ return report;
944
+ }
945
+ function mergeAgg(into, agg) {
946
+ // Defensive: in the cross-daemon merge, `agg` comes from a peer daemon's HTTP
947
+ // response, so don't assume its shape — a malformed chunk must not throw and
948
+ // 500 the whole aggregated overview (it should just contribute nothing).
949
+ if (!agg || typeof agg !== 'object')
950
+ return;
951
+ into.totalSpans += agg.totalSpans ?? 0;
952
+ into.failedSpans += agg.failedSpans ?? 0;
953
+ into.slowSpans += agg.slowSpans ?? 0;
954
+ into.compactions += agg.compactions ?? 0;
955
+ for (const [tool, count] of Object.entries(agg.failByTool ?? {})) {
956
+ into.failByTool[tool] = (into.failByTool[tool] ?? 0) + (count ?? 0);
957
+ }
958
+ for (const phase of INSIGHT_PHASES) {
959
+ into.phase[phase].count += agg.phase?.[phase]?.count ?? 0;
960
+ into.phase[phase].ms += agg.phase?.[phase]?.ms ?? 0;
961
+ }
962
+ }
963
+ function overviewSuggestions(reports) {
964
+ const byId = new Map();
965
+ for (const report of reports) {
966
+ if (report.status !== 'ok')
967
+ continue;
968
+ for (const s of report.suggestions) {
969
+ const cur = byId.get(s.id);
970
+ if (cur) {
971
+ cur.count += 1;
972
+ for (const e of s.evidence) {
973
+ if (cur.evidence.length >= 4)
974
+ break;
975
+ if (!cur.evidence.includes(e))
976
+ cur.evidence.push(e);
977
+ }
978
+ }
979
+ else {
980
+ byId.set(s.id, {
981
+ id: s.id,
982
+ title: s.title,
983
+ severity: s.severity,
984
+ count: 1,
985
+ evidence: s.evidence.slice(0, 4),
986
+ action: s.action,
987
+ });
988
+ }
989
+ }
990
+ }
991
+ return [...byId.values()]
992
+ .sort((a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity] || b.count - a.count || a.id.localeCompare(b.id))
993
+ .slice(0, 12);
994
+ }
995
+ /** Yield to the event loop so a large overview (up to MAX_OVERVIEW_LIMIT sessions,
996
+ * each a synchronous transcript read+parse) can't monopolize the single-threaded
997
+ * daemon — Lark dispatch / PTY IO get a slot between batches. */
998
+ const OVERVIEW_YIELD_EVERY = 16;
999
+ function yieldToEventLoop() {
1000
+ return new Promise(resolve => setImmediate(resolve));
1001
+ }
1002
+ export async function buildSafeInsightOverview(sessions, opts = {}) {
1003
+ const parsedAt = (opts.now?.() ?? new Date()).toISOString();
1004
+ const requestedLimit = opts.limit ?? DEFAULT_OVERVIEW_LIMIT;
1005
+ const limit = Math.min(Math.max(Math.floor(requestedLimit) || DEFAULT_OVERVIEW_LIMIT, 1), MAX_OVERVIEW_LIMIT);
1006
+ const input = sessions
1007
+ .slice()
1008
+ .sort((a, b) => Number(b.lastMessageAt ?? 0) - Number(a.lastMessageAt ?? 0));
1009
+ const selected = input.slice(0, limit);
1010
+ const rows = [];
1011
+ for (let i = 0; i < selected.length; i++) {
1012
+ const s = selected[i];
1013
+ rows.push({
1014
+ sessionId: s.sessionId,
1015
+ cliId: s.cliId ?? 'unknown',
1016
+ cliSessionId: s.cliSessionId,
1017
+ title: s.title,
1018
+ botName: s.botName,
1019
+ larkAppId: s.larkAppId,
1020
+ workingDir: s.workingDir ?? s.cwd,
1021
+ status: s.status,
1022
+ lastMessageAt: s.lastMessageAt,
1023
+ report: buildSafeInsightReport(s, { ...opts, detail: 'summary', now: () => new Date(parsedAt) }),
1024
+ });
1025
+ if ((i + 1) % OVERVIEW_YIELD_EVERY === 0 && i + 1 < selected.length)
1026
+ await yieldToEventLoop();
1027
+ }
1028
+ const reports = rows.map(r => r.report);
1029
+ const okReports = reports.filter(r => r.status === 'ok');
1030
+ const agg = emptyAgg();
1031
+ let rwSum = 0;
1032
+ let rwN = 0;
1033
+ for (const report of okReports) {
1034
+ mergeAgg(agg, report.agg);
1035
+ if (report.agg.readWriteRatio !== null) {
1036
+ rwSum += report.agg.readWriteRatio;
1037
+ rwN++;
1038
+ }
1039
+ }
1040
+ agg.readWriteRatio = rwN > 0 ? Math.round((rwSum / rwN) * 100) / 100 : null;
1041
+ return {
1042
+ generatedAt: parsedAt,
1043
+ meta: {
1044
+ totalSessions: input.length,
1045
+ returnedSessions: rows.length,
1046
+ analyzedSessions: okReports.length,
1047
+ unsupportedSessions: reports.filter(r => r.status === 'unsupported_cli').length,
1048
+ missingTranscriptSessions: reports.filter(r => r.status === 'transcript_missing').length,
1049
+ parseErrorSessions: reports.filter(r => r.status === 'parse_error').length,
1050
+ capped: input.length > selected.length,
1051
+ limit,
1052
+ },
1053
+ agg,
1054
+ suggestions: overviewSuggestions(reports),
1055
+ topFailedTools: Object.entries(agg.failByTool)
1056
+ .map(([tool, count]) => ({ tool, count }))
1057
+ .sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool))
1058
+ .slice(0, 12),
1059
+ topSlowSessions: rows
1060
+ .filter(r => r.report.status === 'ok' && r.report.agg.slowSpans > 0)
1061
+ .map(r => ({
1062
+ sessionId: r.sessionId,
1063
+ title: r.title,
1064
+ cliId: r.cliId,
1065
+ slowSpans: r.report.agg.slowSpans,
1066
+ totalSpans: r.report.agg.totalSpans,
1067
+ }))
1068
+ .sort((a, b) => b.slowSpans - a.slowSpans || b.totalSpans - a.totalSpans)
1069
+ .slice(0, 12),
1070
+ sessions: rows,
1071
+ };
1072
+ }
1073
+ export function mergeSafeInsightOverviews(overviews, opts = {}) {
1074
+ const generatedAt = opts.generatedAt ?? new Date().toISOString();
1075
+ const requestedLimit = opts.limit ?? DEFAULT_OVERVIEW_LIMIT;
1076
+ const limit = Math.min(Math.max(Math.floor(requestedLimit) || DEFAULT_OVERVIEW_LIMIT, 1), MAX_OVERVIEW_LIMIT);
1077
+ const allRows = overviews
1078
+ .flatMap(o => o.sessions)
1079
+ .sort((a, b) => Number(b.lastMessageAt ?? 0) - Number(a.lastMessageAt ?? 0));
1080
+ const rows = allRows.slice(0, limit);
1081
+ const reports = rows.map(r => r.report);
1082
+ const okReports = reports.filter(r => r.status === 'ok');
1083
+ const agg = emptyAgg();
1084
+ let rwSum = 0;
1085
+ let rwN = 0;
1086
+ for (const report of okReports) {
1087
+ mergeAgg(agg, report.agg);
1088
+ if (report.agg.readWriteRatio !== null) {
1089
+ rwSum += report.agg.readWriteRatio;
1090
+ rwN++;
1091
+ }
1092
+ }
1093
+ agg.readWriteRatio = rwN > 0 ? Math.round((rwSum / rwN) * 100) / 100 : null;
1094
+ return {
1095
+ generatedAt,
1096
+ meta: {
1097
+ totalSessions: overviews.reduce((sum, o) => sum + o.meta.totalSessions, 0),
1098
+ returnedSessions: rows.length,
1099
+ analyzedSessions: okReports.length,
1100
+ unsupportedSessions: reports.filter(r => r.status === 'unsupported_cli').length,
1101
+ missingTranscriptSessions: reports.filter(r => r.status === 'transcript_missing').length,
1102
+ parseErrorSessions: reports.filter(r => r.status === 'parse_error').length,
1103
+ capped: allRows.length > rows.length || overviews.some(o => o.meta.capped),
1104
+ limit,
1105
+ },
1106
+ agg,
1107
+ suggestions: overviewSuggestions(reports),
1108
+ topFailedTools: Object.entries(agg.failByTool)
1109
+ .map(([tool, count]) => ({ tool, count }))
1110
+ .sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool))
1111
+ .slice(0, 12),
1112
+ topSlowSessions: rows
1113
+ .filter((r) => r.report.status === 'ok' && r.report.agg.slowSpans > 0)
1114
+ .map(r => ({
1115
+ sessionId: r.sessionId,
1116
+ title: r.title,
1117
+ cliId: r.cliId,
1118
+ slowSpans: r.report.agg.slowSpans,
1119
+ totalSpans: r.report.agg.totalSpans,
1120
+ }))
1121
+ .sort((a, b) => b.slowSpans - a.slowSpans || b.totalSpans - a.totalSpans)
1122
+ .slice(0, 12),
1123
+ sessions: rows,
1124
+ };
1125
+ }
1126
+ //# sourceMappingURL=report.js.map