mixdog 0.9.0 → 0.9.2

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 (240) hide show
  1. package/package.json +10 -3
  2. package/scripts/_bench-cwc.json +20 -0
  3. package/scripts/agent-loop-policy-test.mjs +37 -0
  4. package/scripts/agent-parallel-smoke.mjs +54 -10
  5. package/scripts/background-task-meta-smoke.mjs +1 -1
  6. package/scripts/bench-run.mjs +262 -0
  7. package/scripts/compact-smoke.mjs +12 -0
  8. package/scripts/compact-trigger-migration-smoke.mjs +67 -1
  9. package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
  10. package/scripts/internal-comms-bench.mjs +727 -0
  11. package/scripts/internal-comms-smoke.mjs +75 -0
  12. package/scripts/lead-workflow-smoke.mjs +4 -4
  13. package/scripts/live-worker-smoke.mjs +9 -9
  14. package/scripts/output-style-bench.mjs +285 -0
  15. package/scripts/output-style-smoke.mjs +13 -10
  16. package/scripts/patch-replay.mjs +90 -0
  17. package/scripts/provider-stream-stall-test.mjs +276 -0
  18. package/scripts/provider-toolcall-test.mjs +599 -1
  19. package/scripts/routing-corpus.mjs +281 -0
  20. package/scripts/session-bench.mjs +1526 -0
  21. package/scripts/session-diag.mjs +595 -0
  22. package/scripts/session-ingest-smoke.mjs +2 -2
  23. package/scripts/task-bench.mjs +207 -0
  24. package/scripts/tool-failures.mjs +6 -6
  25. package/scripts/tool-smoke.mjs +306 -66
  26. package/scripts/toolcall-args-test.mjs +81 -0
  27. package/src/agents/debugger/AGENT.md +4 -4
  28. package/src/agents/heavy-worker/AGENT.md +4 -2
  29. package/src/agents/reviewer/AGENT.md +4 -4
  30. package/src/agents/worker/AGENT.md +4 -2
  31. package/src/app.mjs +10 -6
  32. package/src/defaults/{hidden-roles.json → agents.json} +7 -7
  33. package/src/examples/schedules/SCHEDULE.example.md +32 -0
  34. package/src/examples/webhooks/WEBHOOK.example.md +40 -0
  35. package/src/headless-role.mjs +14 -14
  36. package/src/help.mjs +1 -0
  37. package/src/lib/mixdog-debug.cjs +0 -22
  38. package/src/lib/plugin-paths.cjs +1 -7
  39. package/src/lib/rules-builder.cjs +34 -56
  40. package/src/mixdog-session-runtime.mjs +710 -319
  41. package/src/output-styles/default.md +12 -7
  42. package/src/output-styles/minimal.md +25 -0
  43. package/src/output-styles/oneline.md +21 -0
  44. package/src/output-styles/simple.md +10 -9
  45. package/src/repl.mjs +12 -4
  46. package/src/rules/agent/00-common.md +7 -5
  47. package/src/rules/agent/30-explorer.md +7 -8
  48. package/src/rules/lead/01-general.md +3 -1
  49. package/src/rules/lead/lead-tool.md +7 -0
  50. package/src/rules/shared/01-tool.md +17 -12
  51. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
  52. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
  53. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
  54. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
  55. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
  56. package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
  57. package/src/runtime/agent/orchestrator/config.mjs +3 -0
  58. package/src/runtime/agent/orchestrator/context/collect.mjs +131 -67
  59. package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
  60. package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
  61. package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -16
  62. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
  63. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
  64. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
  65. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +359 -106
  66. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +63 -51
  67. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +27 -20
  68. package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
  69. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
  70. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
  71. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +210 -21
  72. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +86 -30
  73. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +254 -280
  74. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +191 -50
  75. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
  76. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +11 -5
  77. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
  78. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +265 -1
  79. package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
  80. package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
  81. package/src/runtime/agent/orchestrator/session/loop.mjs +394 -132
  82. package/src/runtime/agent/orchestrator/session/manager.mjs +217 -170
  83. package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
  84. package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
  85. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
  86. package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
  87. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +1 -1
  88. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -32
  89. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
  90. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +1 -44
  91. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
  92. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +0 -0
  93. package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
  94. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
  95. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
  96. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
  97. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
  98. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +13 -4
  99. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
  100. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +18 -2
  101. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
  102. package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
  103. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -1
  104. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
  105. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4076 -3985
  106. package/src/runtime/agent/orchestrator/tools/patch.mjs +116 -2
  107. package/src/runtime/channels/backends/discord.mjs +99 -9
  108. package/src/runtime/channels/backends/telegram.mjs +501 -0
  109. package/src/runtime/channels/index.mjs +441 -1254
  110. package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
  111. package/src/runtime/channels/lib/config.mjs +54 -3
  112. package/src/runtime/channels/lib/drop-trace.mjs +1 -1
  113. package/src/runtime/channels/lib/executor.mjs +0 -3
  114. package/src/runtime/channels/lib/format.mjs +4 -2
  115. package/src/runtime/channels/lib/memory-client.mjs +0 -38
  116. package/src/runtime/channels/lib/output-forwarder.mjs +77 -71
  117. package/src/runtime/channels/lib/runtime-paths.mjs +29 -6
  118. package/src/runtime/channels/lib/scheduler.mjs +1 -1
  119. package/src/runtime/channels/lib/session-discovery.mjs +0 -4
  120. package/src/runtime/channels/lib/telegram-format.mjs +283 -0
  121. package/src/runtime/channels/lib/tool-format.mjs +1 -2
  122. package/src/runtime/channels/lib/transcript-discovery.mjs +20 -11
  123. package/src/runtime/channels/lib/webhook.mjs +59 -31
  124. package/src/runtime/channels/tool-defs.mjs +1 -1
  125. package/src/runtime/lib/keychain-cjs.cjs +0 -1
  126. package/src/runtime/memory/data/runtime-manifest.json +6 -7
  127. package/src/runtime/memory/index.mjs +187 -43
  128. package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
  129. package/src/runtime/memory/lib/core-memory-store.mjs +1 -1
  130. package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
  131. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
  132. package/src/runtime/memory/lib/memory-cycle2.mjs +9 -6
  133. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  134. package/src/runtime/memory/lib/memory-ops-policy.mjs +0 -1
  135. package/src/runtime/memory/lib/memory.mjs +101 -4
  136. package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
  137. package/src/runtime/memory/lib/runtime-fetcher.mjs +43 -18
  138. package/src/runtime/memory/lib/session-ingest.mjs +116 -7
  139. package/src/runtime/memory/lib/trace-store.mjs +69 -22
  140. package/src/runtime/memory/tool-defs.mjs +6 -3
  141. package/src/runtime/search/index.mjs +2 -7
  142. package/src/runtime/search/lib/config.mjs +0 -4
  143. package/src/runtime/search/lib/state.mjs +1 -15
  144. package/src/runtime/search/lib/web-tools.mjs +0 -1
  145. package/src/runtime/shared/channel-notification-routing.mjs +12 -0
  146. package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
  147. package/src/runtime/shared/child-spawn-gate.mjs +0 -6
  148. package/src/runtime/shared/config.mjs +9 -0
  149. package/src/runtime/shared/llm/http-agent.mjs +12 -5
  150. package/src/runtime/shared/schedules-store.mjs +21 -19
  151. package/src/runtime/shared/tool-surface.mjs +98 -13
  152. package/src/runtime/shared/transcript-writer.mjs +129 -0
  153. package/src/runtime/shared/update-checker.mjs +214 -0
  154. package/src/standalone/agent-tool.mjs +255 -109
  155. package/src/standalone/channel-admin.mjs +133 -40
  156. package/src/standalone/channel-worker.mjs +8 -291
  157. package/src/standalone/explore-tool.mjs +2 -2
  158. package/src/standalone/memory-runtime-proxy.mjs +3 -1
  159. package/src/standalone/provider-admin.mjs +11 -0
  160. package/src/standalone/seeds.mjs +1 -11
  161. package/src/standalone/usage-dashboard.mjs +1 -1
  162. package/src/tui/App.jsx +2137 -750
  163. package/src/tui/components/ConfirmBar.jsx +47 -0
  164. package/src/tui/components/ContextPanel.jsx +5 -3
  165. package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
  166. package/src/tui/components/Markdown.jsx +22 -98
  167. package/src/tui/components/Message.jsx +14 -35
  168. package/src/tui/components/Picker.jsx +87 -12
  169. package/src/tui/components/PromptInput.jsx +146 -9
  170. package/src/tui/components/QueuedCommands.jsx +1 -1
  171. package/src/tui/components/SlashCommandPalette.jsx +8 -5
  172. package/src/tui/components/Spinner.jsx +7 -7
  173. package/src/tui/components/StatusLine.jsx +40 -21
  174. package/src/tui/components/TextEntryPanel.jsx +51 -7
  175. package/src/tui/components/ToolExecution.jsx +177 -100
  176. package/src/tui/components/TurnDone.jsx +4 -4
  177. package/src/tui/components/UsagePanel.jsx +1 -1
  178. package/src/tui/components/tool-output-format.mjs +312 -40
  179. package/src/tui/components/tool-output-format.test.mjs +180 -1
  180. package/src/tui/display-width.mjs +69 -0
  181. package/src/tui/display-width.test.mjs +35 -0
  182. package/src/tui/dist/index.mjs +7324 -2393
  183. package/src/tui/engine.mjs +287 -126
  184. package/src/tui/index.jsx +117 -7
  185. package/src/tui/keyboard-protocol.mjs +42 -0
  186. package/src/tui/lib/voice-recorder.mjs +453 -0
  187. package/src/tui/markdown/format-token.mjs +354 -142
  188. package/src/tui/markdown/format-token.test.mjs +155 -17
  189. package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
  190. package/src/tui/markdown/render-ansi.test.mjs +1 -1
  191. package/src/tui/markdown/streaming-markdown.mjs +167 -0
  192. package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
  193. package/src/tui/markdown/table-layout.mjs +9 -9
  194. package/src/tui/paste-attachments.mjs +0 -11
  195. package/src/tui/prompt-history-store.mjs +129 -0
  196. package/src/tui/prompt-history-store.test.mjs +52 -0
  197. package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
  198. package/src/tui/theme.mjs +41 -647
  199. package/src/tui/themes/base.mjs +86 -0
  200. package/src/tui/themes/basic.mjs +85 -0
  201. package/src/tui/themes/catppuccin.mjs +72 -0
  202. package/src/tui/themes/dracula.mjs +70 -0
  203. package/src/tui/themes/everforest.mjs +71 -0
  204. package/src/tui/themes/gruvbox.mjs +71 -0
  205. package/src/tui/themes/index.mjs +71 -0
  206. package/src/tui/themes/indigo.mjs +78 -0
  207. package/src/tui/themes/kanagawa.mjs +80 -0
  208. package/src/tui/themes/light.mjs +81 -0
  209. package/src/tui/themes/nord.mjs +72 -0
  210. package/src/tui/themes/onedark.mjs +16 -0
  211. package/src/tui/themes/rosepine.mjs +70 -0
  212. package/src/tui/themes/teal.mjs +81 -0
  213. package/src/tui/themes/tokyonight.mjs +79 -0
  214. package/src/tui/themes/utils.mjs +106 -0
  215. package/src/tui/themes/warm.mjs +79 -0
  216. package/src/tui/transcript-tool-failures.mjs +13 -2
  217. package/src/ui/markdown.mjs +1 -1
  218. package/src/ui/model-display.mjs +2 -2
  219. package/src/ui/statusline.mjs +26 -27
  220. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -623
  221. package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
  222. package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
  223. package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
  224. package/src/workflows/default/WORKFLOW.md +39 -12
  225. package/src/workflows/sequential/WORKFLOW.md +46 -0
  226. package/src/workflows/solo/WORKFLOW.md +7 -0
  227. package/vendor/ink/build/display-width.js +62 -0
  228. package/vendor/ink/build/ink.js +154 -20
  229. package/vendor/ink/build/measure-text.js +4 -1
  230. package/vendor/ink/build/output.js +115 -9
  231. package/vendor/ink/build/render-node-to-output.js +4 -1
  232. package/vendor/ink/build/render.js +4 -0
  233. package/src/hooks/lib/permission-rules.cjs +0 -170
  234. package/src/hooks/lib/settings-loader.cjs +0 -112
  235. package/src/lib/hook-pipe-path.cjs +0 -10
  236. package/src/output-styles/extreme-simple.md +0 -20
  237. package/src/rules/lead/04-workflow.md +0 -51
  238. package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
  239. package/src/workflows/default/workflow.json +0 -13
  240. package/src/workflows/solo/workflow.json +0 -7
@@ -0,0 +1,1526 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { createHash } from 'node:crypto';
4
+ import { homedir } from 'node:os';
5
+ import { dirname, resolve } from 'node:path';
6
+ import { resolvePluginData } from '../src/runtime/shared/plugin-paths.mjs';
7
+
8
+ function argValue(name, fallback = null) {
9
+ const idx = process.argv.indexOf(name);
10
+ if (idx >= 0 && idx + 1 < process.argv.length) return process.argv[idx + 1];
11
+ const pref = `${name}=`;
12
+ const hit = process.argv.find((arg) => arg.startsWith(pref));
13
+ return hit ? hit.slice(pref.length) : fallback;
14
+ }
15
+
16
+ function hasFlag(name) {
17
+ return process.argv.includes(name);
18
+ }
19
+
20
+ function parseDuration(value) {
21
+ const raw = String(value || '').trim();
22
+ if (!raw) return null;
23
+ if (/^\d+$/.test(raw)) {
24
+ const n = Number(raw);
25
+ return n > 10_000_000_000 ? n : n * 1000;
26
+ }
27
+ const rel = raw.match(/^(\d+(?:\.\d+)?)(ms|s|m|h|d)$/i);
28
+ if (rel) {
29
+ const n = Number(rel[1]);
30
+ const unit = rel[2].toLowerCase();
31
+ const mult = unit === 'ms' ? 1 : unit === 's' ? 1000 : unit === 'm' ? 60_000 : unit === 'h' ? 3_600_000 : 86_400_000;
32
+ return Date.now() - n * mult;
33
+ }
34
+ const parsed = Date.parse(raw);
35
+ return Number.isFinite(parsed) ? parsed : null;
36
+ }
37
+
38
+ const opts = {
39
+ trace: argValue('--trace', null),
40
+ session: argValue('--session', 'current'),
41
+ since: parseDuration(argValue('--since', null)),
42
+ agent: argValue('--agent', null),
43
+ limit: Number.parseInt(argValue('--limit', '50'), 10) || 50,
44
+ json: hasFlag('--json'),
45
+ cacheOnly: hasFlag('--cache'),
46
+ toolsOnly: hasFlag('--tools'),
47
+ issuesOnly: hasFlag('--issues'),
48
+ failuresOnly: hasFlag('--failures'),
49
+ compactOnly: hasFlag('--compact'),
50
+ tokensOnly: hasFlag('--tokens'),
51
+ slowOnly: hasFlag('--slow'),
52
+ };
53
+
54
+ function defaultTracePath() {
55
+ const data = process.env.MIXDOG_DATA_DIR || resolvePluginData() || resolve(homedir(), '.mixdog', 'data');
56
+ return resolve(data, 'history', 'agent-trace.jsonl');
57
+ }
58
+
59
+ function defaultToolFailurePath(tracePath = null) {
60
+ if (process.env.MIXDOG_TOOL_FAILURE_LOG_PATH) return process.env.MIXDOG_TOOL_FAILURE_LOG_PATH;
61
+ const base = tracePath ? dirname(resolve(tracePath)) : resolve(process.env.MIXDOG_DATA_DIR || resolvePluginData() || resolve(homedir(), '.mixdog', 'data'), 'history');
62
+ return resolve(base, 'tool-failures.jsonl');
63
+ }
64
+
65
+ function readRows(path) {
66
+ if (!existsSync(path)) return [];
67
+ const rows = [];
68
+ const lines = readFileSync(path, 'utf8').split(/\r?\n/);
69
+ for (let i = 0; i < lines.length; i += 1) {
70
+ const line = lines[i];
71
+ if (!line) continue;
72
+ try {
73
+ const row = JSON.parse(line);
74
+ rows.push({ ...row, _line: i + 1 });
75
+ } catch {
76
+ // Keep JSONL parsing best-effort; a partially-written tail must not break diagnostics.
77
+ }
78
+ }
79
+ return rows;
80
+ }
81
+
82
+ function payload(row) {
83
+ return row?.payload && typeof row.payload === 'object' ? row.payload : {};
84
+ }
85
+
86
+ function field(row, name) {
87
+ if (row && row[name] != null) return row[name];
88
+ const p = payload(row);
89
+ return p[name] != null ? p[name] : null;
90
+ }
91
+
92
+ function num(row, name) {
93
+ const n = Number(field(row, name));
94
+ return Number.isFinite(n) ? n : null;
95
+ }
96
+
97
+ function sessionId(row) {
98
+ return String(row?.session_id || row?.sessionId || field(row, 'session_id') || '');
99
+ }
100
+
101
+ function baseSessionId(id) {
102
+ return String(id || '').replace(/:compact$/, '');
103
+ }
104
+
105
+ function isCompactSessionId(id) {
106
+ return String(id || '').endsWith(':compact');
107
+ }
108
+
109
+ function stableStringify(value) {
110
+ if (value === null || value === undefined) return JSON.stringify(value);
111
+ if (typeof value !== 'object') return JSON.stringify(value);
112
+ if (Array.isArray(value)) return '[' + value.map(stableStringify).join(',') + ']';
113
+ const keys = Object.keys(value).sort();
114
+ return '{' + keys.map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(',') + '}';
115
+ }
116
+
117
+ function hashValue(value) {
118
+ try { return createHash('sha256').update(stableStringify(value)).digest('hex').slice(0, 16); }
119
+ catch { return null; }
120
+ }
121
+
122
+ function toolArgs(row) {
123
+ return field(row, 'tool_args_summary') || field(row, 'tool_args') || null;
124
+ }
125
+
126
+ function toolArgsHash(row) {
127
+ const args = toolArgs(row);
128
+ if (!args || typeof args !== 'object' || Object.keys(args).length === 0) return null;
129
+ return field(row, 'tool_args_hash') || hashValue(args);
130
+ }
131
+
132
+ function grepPatternTerms(pattern) {
133
+ const raw = Array.isArray(pattern) ? pattern.join('|') : String(pattern || '');
134
+ return [...new Set(raw
135
+ .split(/[|,\s()"'`[\]{}.*+?^$\\:-]+/g)
136
+ .map((s) => s.trim().toLowerCase())
137
+ .filter((s) => s.length >= 4))];
138
+ }
139
+
140
+ function normalizePathForCompare(pathValue) {
141
+ return String(pathValue || '.')
142
+ .replace(/\\/g, '/')
143
+ .replace(/^([A-Za-z]):/, (_, d) => d.toLowerCase() + ':')
144
+ .replace(/\/+/g, '/')
145
+ .replace(/\/$/, '') || '.';
146
+ }
147
+
148
+ function grepSweepKey(row) {
149
+ const args = toolArgs(row) || {};
150
+ const path = normalizePathForCompare(String(args.path || '.'));
151
+ const glob = Array.isArray(args.glob) ? args.glob.join(',') : String(args.glob || '');
152
+ const terms = grepPatternTerms(args.pattern).slice(0, 3).join('|');
153
+ return `${path}::${glob}::${terms}`;
154
+ }
155
+
156
+ const GREP_SWEEP_MIN_CALLS = 20;
157
+ const GREP_SWEEP_MIN_UNIQUE = 16;
158
+
159
+ function fmtMs(ms) {
160
+ const n = Number(ms);
161
+ if (!Number.isFinite(n)) return '-';
162
+ if (n >= 1000) return `${(n / 1000).toFixed(n >= 10_000 ? 0 : 1)}s`;
163
+ return `${Math.round(n)}ms`;
164
+ }
165
+
166
+ function fmtSec(ms) {
167
+ const n = Number(ms);
168
+ if (!Number.isFinite(n)) return '-';
169
+ return `${(n / 1000).toFixed(n >= 10_000 ? 0 : 1)}s`;
170
+ }
171
+
172
+ function fmtTok(n) {
173
+ const v = Number(n);
174
+ if (!Number.isFinite(v)) return '-';
175
+ if (v >= 1_000_000) return `${(v / 1_000_000).toFixed(2)}M`;
176
+ if (v >= 1000) return `${(v / 1000).toFixed(1)}k`;
177
+ return String(Math.round(v));
178
+ }
179
+
180
+ function fmtPct(n) {
181
+ const v = Number(n);
182
+ return Number.isFinite(v) ? `${Math.round(v)}%` : '-';
183
+ }
184
+
185
+ function fmtTime(ts) {
186
+ const n = Number(ts);
187
+ if (!Number.isFinite(n) || n <= 0) return '-';
188
+ return new Date(n).toISOString().replace('T', ' ').replace(/\.\d+Z$/, 'Z');
189
+ }
190
+
191
+ function compactText(value, max = 140) {
192
+ const s = String(value || '').replace(/\s+/g, ' ').trim();
193
+ return s.length > max ? `${s.slice(0, Math.max(0, max - 1))}…` : s;
194
+ }
195
+
196
+ function percentile(values, p) {
197
+ const arr = values.filter((v) => Number.isFinite(v)).sort((a, b) => a - b);
198
+ if (!arr.length) return null;
199
+ const idx = Math.min(arr.length - 1, Math.max(0, Math.ceil((p / 100) * arr.length) - 1));
200
+ return arr[idx];
201
+ }
202
+
203
+ function sum(values) {
204
+ return values.reduce((s, v) => s + (Number.isFinite(Number(v)) ? Number(v) : 0), 0);
205
+ }
206
+
207
+ function cacheDenom(row) {
208
+ const prompt = num(row, 'prompt_tokens');
209
+ const input = num(row, 'input_tokens');
210
+ return prompt || input || 0;
211
+ }
212
+
213
+ function cacheRatioFromUsage(rows) {
214
+ let cached = 0;
215
+ let total = 0;
216
+ for (const row of rows) {
217
+ cached += num(row, 'cached_tokens') || 0;
218
+ total += cacheDenom(row);
219
+ }
220
+ return total > 0 ? cached / total : null;
221
+ }
222
+
223
+ function cacheBreakExplanation(reason) {
224
+ const r = String(reason || '');
225
+ if (r === 'no_anchor') return 'delta 기준점/previous response 없음';
226
+ if (r === 'input_prefix_mismatch') return '요청 prefix가 이전 turn과 달라짐';
227
+ if (r.startsWith('response_output_mismatch')) return '이전 응답 output 체인이 기대값과 다름';
228
+ if (r === 'cache_key_changed') return 'cache key 변경';
229
+ return r ? '원인 미분류' : '원인 기록 없음';
230
+ }
231
+
232
+ function classifyCacheBreakPhase(row, usageRows, transportRows) {
233
+ const reason = field(row, 'reason') || field(row, 'chain_delta_reason') || field(row, 'payload')?.reason || null;
234
+ const sid = sessionId(row);
235
+ if (isCompactSessionId(sid)) return 'intentional_compact';
236
+ const ts = Number(row.ts || 0);
237
+ const priorUsage = usageRows.filter((r) => sessionId(r) === sid && Number(r.ts || 0) < ts)
238
+ .sort((a, b) => Number(b.ts || 0) - Number(a.ts || 0));
239
+ const priorTransport = transportRows.filter((r) => sessionId(r) === sid && Number(r.ts || 0) < ts)
240
+ .sort((a, b) => Number(b.ts || 0) - Number(a.ts || 0));
241
+ const priorCalls = priorUsage.length + priorTransport.length;
242
+ if (String(reason || '') === 'no_anchor') {
243
+ if (priorCalls === 0) return 'cold_start';
244
+ return 'mid_chain_reset';
245
+ }
246
+ if (priorCalls === 0) return 'first_call_mismatch';
247
+ return 'mid_chain_break';
248
+ }
249
+
250
+ function shortId(id) {
251
+ const s = String(id || '');
252
+ if (s.length <= 18) return s;
253
+ return `${s.slice(0, 10)}…${s.slice(-6)}`;
254
+ }
255
+
256
+ function groupBy(rows, keyFn) {
257
+ const map = new Map();
258
+ for (const row of rows) {
259
+ const key = keyFn(row);
260
+ if (!map.has(key)) map.set(key, []);
261
+ map.get(key).push(row);
262
+ }
263
+ return map;
264
+ }
265
+
266
+ function padTable(rows) {
267
+ if (!rows.length) return [];
268
+ const widths = [];
269
+ for (const row of rows) {
270
+ row.forEach((cell, i) => { widths[i] = Math.max(widths[i] || 0, String(cell ?? '').length); });
271
+ }
272
+ return rows.map((row) => row.map((cell, i) => String(cell ?? '').padEnd(widths[i])).join(' '));
273
+ }
274
+
275
+ function inferSessionMeta(rows) {
276
+ const sorted = [...rows].sort((a, b) => Number(a.ts || 0) - Number(b.ts || 0));
277
+ const preset = sorted.find((r) => r.kind === 'preset_assign');
278
+ const usage = [...sorted].reverse().find((r) => r.kind === 'usage_raw' || r.kind === 'usage');
279
+ const tool = sorted.find((r) => r.kind === 'tool');
280
+ const context = sorted.find((r) => r.kind === 'context');
281
+ const last = sorted[sorted.length - 1] || {};
282
+ const tsValues = sorted.map((r) => Number(r.ts || 0)).filter((n) => n > 0);
283
+ return {
284
+ session_id: sessionId(last),
285
+ parent_session_id: field(preset, 'parent_session_id') || field(preset, 'parentSessionId') || null,
286
+ agent: field(preset, 'agent') || field(tool, 'agent') || field(usage, 'sourceName') || field(last, 'sourceName') || null,
287
+ preset: field(preset, 'preset_name') || field(last, 'preset') || null,
288
+ provider: field(preset, 'provider') || field(usage, 'provider') || field(context, 'provider') || null,
289
+ model: field(preset, 'model') || field(usage, 'model') || field(context, 'model') || null,
290
+ min_ts: tsValues.length ? Math.min(...tsValues) : null,
291
+ max_ts: tsValues.length ? Math.max(...tsValues) : null,
292
+ rows: sorted.length,
293
+ };
294
+ }
295
+
296
+ function chooseCurrentSession(sessionMetas) {
297
+ const sorted = [...sessionMetas].sort((a, b) => Number(b.max_ts || 0) - Number(a.max_ts || 0));
298
+ // "current" should mean the freshest active route, not necessarily the
299
+ // oldest root workflow that still has background child events. Prefer recent
300
+ // workflow-lead/main rows, then fall back to newest activity.
301
+ const main = sorted.find((m) => {
302
+ const agent = String(m.agent || '').toLowerCase();
303
+ const preset = String(m.preset || '').toLowerCase();
304
+ return agent === 'main' || agent === 'lead' || preset.includes('workflow lead');
305
+ });
306
+ return main || sorted.find((m) => !m.parent_session_id && m.model) || sorted[0] || null;
307
+ }
308
+
309
+ function sessionMatches(id, query) {
310
+ const q = String(query || '').trim();
311
+ if (!q || q === 'current') return false;
312
+ return id === q || id.startsWith(q) || shortId(id).startsWith(q);
313
+ }
314
+
315
+ function selectSessionIds(sessionMetas, query) {
316
+ let selected;
317
+ if (!query || query === 'current') selected = chooseCurrentSession(sessionMetas);
318
+ else selected = sessionMetas.find((m) => sessionMatches(m.session_id, query));
319
+ if (!selected) return [];
320
+ const root = selected.parent_session_id
321
+ ? sessionMetas.find((m) => m.session_id === selected.parent_session_id) || selected
322
+ : selected;
323
+ const ids = new Set([root.session_id]);
324
+ for (const meta of sessionMetas) {
325
+ if (meta.parent_session_id === root.session_id) ids.add(meta.session_id);
326
+ }
327
+ if (selected.session_id) ids.add(selected.session_id);
328
+ return [...ids];
329
+ }
330
+
331
+ function filterAgent(rows, agent) {
332
+ if (!agent) return rows;
333
+ const q = String(agent).toLowerCase();
334
+ return rows.filter((r) => {
335
+ const values = [
336
+ field(r, 'agent'),
337
+ field(r, 'agent'),
338
+ field(r, 'sourceName'),
339
+ field(r, 'preset'),
340
+ field(r, 'preset_name'),
341
+ ].filter(Boolean).map((v) => String(v).toLowerCase());
342
+ return values.some((v) => v.includes(q));
343
+ });
344
+ }
345
+
346
+ function buildRouteGroups(rows) {
347
+ const bySid = groupBy(rows, sessionId);
348
+ const groups = [];
349
+ for (const [sid, srows] of bySid.entries()) {
350
+ const meta = inferSessionMeta(srows);
351
+ const usageRows = srows.filter((r) => r.kind === 'usage_raw');
352
+ const toolRows = srows.filter((r) => r.kind === 'tool');
353
+ const sseRows = srows.filter((r) => r.kind === 'sse');
354
+ const fetchRows = srows.filter((r) => r.kind === 'fetch');
355
+ const transportRows = srows.filter((r) => r.kind === 'transport');
356
+ const turns = usageRows.length || new Set(transportRows.map((r) => num(r, 'iteration')).filter((n) => n != null)).size;
357
+ const promptTokens = sum(usageRows.map((r) => num(r, 'prompt_tokens')));
358
+ const outputTokens = sum(usageRows.map((r) => num(r, 'output_tokens')));
359
+ const cachedTokens = sum(usageRows.map((r) => num(r, 'cached_tokens')));
360
+ const cacheRatio = cacheRatioFromUsage(usageRows);
361
+ groups.push({
362
+ ...meta,
363
+ session_id: sid,
364
+ turns,
365
+ tool_calls: toolRows.length,
366
+ tool_ms: sum(toolRows.map((r) => num(r, 'tool_ms'))),
367
+ llm_stream_ms: sum(sseRows.map((r) => num(r, 'stream_total_ms') ?? num(r, 'sse_parse_ms'))),
368
+ headers_ms: sum(fetchRows.map((r) => num(r, 'headers_ms'))),
369
+ ttft_p50_ms: percentile(sseRows.map((r) => num(r, 'ttft_ms')).filter((n) => n != null), 50),
370
+ prompt_tokens: promptTokens,
371
+ output_tokens: outputTokens,
372
+ cached_tokens: cachedTokens,
373
+ cache_ratio: cacheRatio,
374
+ ws_full: transportRows.filter((r) => field(r, 'ws_mode') === 'full').length,
375
+ ws_delta: transportRows.filter((r) => field(r, 'ws_mode') === 'delta').length,
376
+ reused_connection: transportRows.filter((r) => field(r, 'reused_connection') === true).length,
377
+ transport_rows: transportRows.length,
378
+ });
379
+ }
380
+ return groups.sort((a, b) => Number(a.min_ts || 0) - Number(b.min_ts || 0));
381
+ }
382
+
383
+ function buildCacheDiagnostics(rows) {
384
+ const transport = rows.filter((r) => r.kind === 'transport');
385
+ const usage = rows.filter((r) => r.kind === 'usage_raw');
386
+ const breaks = rows.filter((r) => r.kind === 'cache_break');
387
+ const keyCounts = new Map();
388
+ for (const r of transport) {
389
+ const key = field(r, 'cache_key_hash');
390
+ if (key) keyCounts.set(key, (keyCounts.get(key) || 0) + 1);
391
+ }
392
+ const downgrades = transport.filter((r) => {
393
+ const requested = field(r, 'requested_service_tier');
394
+ const response = field(r, 'response_service_tier');
395
+ return requested && response && requested !== response;
396
+ }).map((r) => ({
397
+ session_id: sessionId(r),
398
+ iteration: num(r, 'iteration'),
399
+ requested: field(r, 'requested_service_tier'),
400
+ response: field(r, 'response_service_tier'),
401
+ model: field(r, 'model'),
402
+ ts: r.ts,
403
+ }));
404
+ return {
405
+ usage_cache_ratio: cacheRatioFromUsage(usage),
406
+ cached_tokens: sum(usage.map((r) => num(r, 'cached_tokens'))),
407
+ prompt_tokens: sum(usage.map((r) => num(r, 'prompt_tokens'))),
408
+ transport_count: transport.length,
409
+ ws_full: transport.filter((r) => field(r, 'ws_mode') === 'full').length,
410
+ ws_delta: transport.filter((r) => field(r, 'ws_mode') === 'delta').length,
411
+ reused_connection: transport.filter((r) => field(r, 'reused_connection') === true).length,
412
+ previous_response_id: transport.filter((r) => field(r, 'request_has_previous_response_id') === true).length,
413
+ cache_key_hashes: [...keyCounts.entries()].map(([key, count]) => ({ key, count })).sort((a, b) => b.count - a.count),
414
+ cache_breaks: breaks.map((r) => {
415
+ const reason = field(r, 'reason') || field(r, 'chain_delta_reason') || field(r, 'payload')?.reason || null;
416
+ const relatedUsage = nearestRowAround(
417
+ usage,
418
+ r.ts,
419
+ 5_000,
420
+ (u) => sessionId(u) === sessionId(r) && num(u, 'iteration') === num(r, 'iteration'),
421
+ ) || nearestRowBefore(
422
+ usage.filter((u) => sessionId(u) === sessionId(r)),
423
+ r.ts,
424
+ 30_000,
425
+ );
426
+ const promptTokens = relatedUsage ? cacheDenom(relatedUsage) : 0;
427
+ const cachedTokens = relatedUsage ? (num(relatedUsage, 'cached_tokens') || 0) : 0;
428
+ return {
429
+ session_id: sessionId(r),
430
+ iteration: num(r, 'iteration'),
431
+ reason,
432
+ phase: classifyCacheBreakPhase(r, usage, transport),
433
+ explanation: cacheBreakExplanation(reason),
434
+ ws_mode: field(r, 'ws_mode'),
435
+ cache_key_hash: field(r, 'cache_key_hash'),
436
+ request_has_previous_response_id: field(r, 'request_has_previous_response_id'),
437
+ body_input_items: field(r, 'body_input_items'),
438
+ frame_input_items: field(r, 'frame_input_items'),
439
+ prompt_tokens: promptTokens,
440
+ cached_tokens: cachedTokens,
441
+ cache_ratio: promptTokens > 0 ? cachedTokens / promptTokens : null,
442
+ output_tokens: relatedUsage ? (num(relatedUsage, 'output_tokens') || 0) : 0,
443
+ ts: r.ts,
444
+ };
445
+ }),
446
+ service_tier_downgrades: downgrades,
447
+ low_cache_turns: usage.map((r) => {
448
+ const denom = cacheDenom(r);
449
+ const ratio = denom > 0 ? (num(r, 'cached_tokens') || 0) / denom : null;
450
+ return { session_id: sessionId(r), iteration: num(r, 'iteration'), ratio, cached_tokens: num(r, 'cached_tokens') || 0, prompt_tokens: denom, ts: r.ts };
451
+ }).filter((x) => x.prompt_tokens >= 1000 && (x.ratio == null || x.ratio < 0.25)),
452
+ };
453
+ }
454
+
455
+ function summarizeToolTarget(row) {
456
+ const args = toolArgs(row) || {};
457
+ const name = String(field(row, 'tool_name') || '');
458
+ if (name === 'read') return `${args.path || '?'}:${args.line || args.offset || ''}`;
459
+ if (name === 'grep') return `${args.path || '?'} :: ${String(args.pattern || '').slice(0, 80)}`;
460
+ if (name === 'code_graph') return `${args.mode || '?'} ${args.file || ''} ${args.symbol || ''}`.trim();
461
+ if (name === 'find') return `${args.path || '?'} :: ${args.query || ''}`;
462
+ if (name === 'glob') return `${args.path || '?'} :: ${args.pattern || ''}`;
463
+ if (name === 'list') return String(args.path || '?');
464
+ if (name === 'shell') return compactText(args.command || args.cmd || hashValue(args) || '-', 140);
465
+ if (name === 'agent') return compactText(`${args.agent || args.type || 'agent'} ${args.tag || args.task_id || ''} ${args.prompt || args.message || ''}`, 140);
466
+ if (name === 'apply_patch') return 'patch';
467
+ return hashValue(args) || '-';
468
+ }
469
+
470
+ function toolResultKind(row) {
471
+ return String(field(row, 'result_kind') || 'unknown').toLowerCase();
472
+ }
473
+
474
+ function toolFailureReason(row) {
475
+ const candidates = [
476
+ field(row, 'error'),
477
+ field(row, 'message'),
478
+ field(row, 'result_error'),
479
+ field(row, 'result_preview'),
480
+ field(row, 'result_excerpt'),
481
+ field(row, 'error_first_line'),
482
+ field(row, 'error_preview'),
483
+ field(row, 'stderr'),
484
+ field(row, 'stdout'),
485
+ ];
486
+ const hit = candidates.find((v) => v != null && String(v).trim());
487
+ return hit ? compactText(hit, 180) : null;
488
+ }
489
+
490
+ function toolFailureCategory(row) {
491
+ return field(row, 'category') || null;
492
+ }
493
+
494
+ function findFailureDetail(row, failureRows = []) {
495
+ const sid = sessionId(row);
496
+ const it = num(row, 'iteration');
497
+ const tool = field(row, 'tool_name');
498
+ const ts = Number(row.ts || 0);
499
+ let best = null;
500
+ let bestDelta = Infinity;
501
+ for (const f of failureRows) {
502
+ if (sessionId(f) !== sid) continue;
503
+ if (tool && field(f, 'tool_name') !== tool) continue;
504
+ if (it != null && num(f, 'iteration') !== it) continue;
505
+ const fts = Number(f.ts || 0);
506
+ const delta = Number.isFinite(ts) && Number.isFinite(fts) ? Math.abs(fts - ts) : 0;
507
+ if (delta <= 120_000 && delta < bestDelta) {
508
+ best = f;
509
+ bestDelta = delta;
510
+ }
511
+ }
512
+ return best;
513
+ }
514
+
515
+ function summarizeKindCounts(rows) {
516
+ const counts = new Map();
517
+ for (const row of rows) {
518
+ const kind = toolResultKind(row);
519
+ counts.set(kind, (counts.get(kind) || 0) + 1);
520
+ }
521
+ return [...counts.entries()]
522
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
523
+ .map(([kind, count]) => ({ kind, count }));
524
+ }
525
+
526
+ function fmtKindCounts(counts, limit = 3) {
527
+ const shown = (counts || []).slice(0, limit).map((x) => `${x.kind}×${x.count}`);
528
+ const hidden = Math.max(0, (counts || []).length - shown.length);
529
+ return `${shown.join(', ')}${hidden ? ` +${hidden}` : ''}` || '-';
530
+ }
531
+
532
+ function buildToolDiagnostics(rows, failureRows = []) {
533
+ const tools = rows.filter((r) => r.kind === 'tool');
534
+ const byName = [];
535
+ for (const [name, trows] of groupBy(tools, (r) => String(field(r, 'tool_name') || '(unknown)')).entries()) {
536
+ const kindCounts = summarizeKindCounts(trows);
537
+ const errors = trows.filter((r) => toolResultKind(r) === 'error').length;
538
+ byName.push({
539
+ tool: name,
540
+ count: trows.length,
541
+ ok: trows.length - errors,
542
+ errors,
543
+ result_kinds: kindCounts,
544
+ total_ms: sum(trows.map((r) => num(r, 'tool_ms'))),
545
+ p50_ms: percentile(trows.map((r) => num(r, 'tool_ms')).filter((n) => n != null), 50),
546
+ p95_ms: percentile(trows.map((r) => num(r, 'tool_ms')).filter((n) => n != null), 95),
547
+ bytes: sum(trows.map((r) => num(r, 'result_bytes_est'))),
548
+ lines: sum(trows.map((r) => num(r, 'result_lines_est'))),
549
+ });
550
+ }
551
+ byName.sort((a, b) => b.total_ms - a.total_ms || b.count - a.count);
552
+
553
+ const duplicates = [];
554
+ for (const [key, trows] of groupBy(tools, (r) => `${field(r, 'tool_name') || ''}:${toolArgsHash(r) || ''}`).entries()) {
555
+ if (!key.endsWith(':') && trows.length >= 2) {
556
+ const sorted = [...trows].sort((a, b) => Number(a.ts || 0) - Number(b.ts || 0));
557
+ for (let i = 1; i < sorted.length; i += 1) {
558
+ if (Number(sorted[i].ts || 0) - Number(sorted[i - 1].ts || 0) <= 120_000) {
559
+ duplicates.push({
560
+ tool: field(sorted[i], 'tool_name'),
561
+ count: trows.length,
562
+ result_kinds: summarizeKindCounts(trows),
563
+ first_ts: sorted[0].ts,
564
+ last_ts: sorted[sorted.length - 1].ts,
565
+ target: summarizeToolTarget(sorted[i]),
566
+ args_hash: toolArgsHash(sorted[i]),
567
+ });
568
+ break;
569
+ }
570
+ }
571
+ }
572
+ }
573
+ duplicates.sort((a, b) => b.count - a.count);
574
+
575
+ const failedRepeats = [];
576
+ for (const dup of duplicates) {
577
+ const matches = tools.filter((r) => field(r, 'tool_name') === dup.tool && toolArgsHash(r) === dup.args_hash);
578
+ const errors = matches.filter((r) => toolResultKind(r) === 'error');
579
+ if (errors.length >= 2) failedRepeats.push({ ...dup, error_count: errors.length });
580
+ }
581
+
582
+ const failures = tools
583
+ .filter((r) => toolResultKind(r) === 'error')
584
+ .map((r) => {
585
+ const detail = findFailureDetail(r, failureRows);
586
+ return {
587
+ tool: field(r, 'tool_name'),
588
+ session_id: sessionId(r),
589
+ agent: field(r, 'agent') || field(r, 'sourceName') || field(detail, 'agent') || null,
590
+ iteration: num(r, 'iteration'),
591
+ tool_ms: num(r, 'tool_ms') || num(detail, 'tool_ms') || 0,
592
+ bytes: num(r, 'result_bytes_est') || num(detail, 'result_bytes_est') || 0,
593
+ lines: num(r, 'result_lines_est') || num(detail, 'result_lines_est') || 0,
594
+ category: toolFailureCategory(detail) || toolFailureCategory(r),
595
+ reason: toolFailureReason(detail) || toolFailureReason(r),
596
+ preview: field(detail, 'error_preview') || null,
597
+ target: summarizeToolTarget(detail || r),
598
+ ts: r.ts,
599
+ };
600
+ })
601
+ .sort((a, b) => Number(b.ts || 0) - Number(a.ts || 0));
602
+
603
+ const successes = tools
604
+ .filter((r) => toolResultKind(r) !== 'error')
605
+ .map((r) => ({
606
+ tool: field(r, 'tool_name'),
607
+ session_id: sessionId(r),
608
+ agent: field(r, 'agent') || field(r, 'sourceName') || null,
609
+ iteration: num(r, 'iteration'),
610
+ tool_ms: num(r, 'tool_ms') || 0,
611
+ bytes: num(r, 'result_bytes_est') || 0,
612
+ lines: num(r, 'result_lines_est') || 0,
613
+ result_kind: toolResultKind(r),
614
+ target: summarizeToolTarget(r),
615
+ ts: r.ts,
616
+ }))
617
+ .sort((a, b) => Number(b.ts || 0) - Number(a.ts || 0));
618
+
619
+ const broadResults = tools.filter((r) => {
620
+ const bytes = num(r, 'result_bytes_est') || 0;
621
+ const lines = num(r, 'result_lines_est') || 0;
622
+ return bytes >= 64 * 1024 || lines >= 1000 || String(field(r, 'result_kind') || '').includes('offload');
623
+ }).map((r) => ({
624
+ tool: field(r, 'tool_name'),
625
+ session_id: sessionId(r),
626
+ iteration: num(r, 'iteration'),
627
+ bytes: num(r, 'result_bytes_est') || 0,
628
+ lines: num(r, 'result_lines_est') || 0,
629
+ target: summarizeToolTarget(r),
630
+ ts: r.ts,
631
+ })).sort((a, b) => b.bytes - a.bytes);
632
+
633
+ const readRows = tools.filter((r) => field(r, 'tool_name') === 'read');
634
+ const readFragmentation = [];
635
+ for (const [path, rrows] of groupBy(readRows, (r) => String((toolArgs(r) || {}).path || '')).entries()) {
636
+ if (!path || path.includes(',')) continue;
637
+ const lineReads = rrows.map((r) => ({ row: r, line: Number((toolArgs(r) || {}).line || (toolArgs(r) || {}).offset || 0), ts: Number(r.ts || 0) }))
638
+ .filter((x) => Number.isFinite(x.line) && x.line > 0)
639
+ .sort((a, b) => a.line - b.line);
640
+ if (lineReads.length >= 3) {
641
+ const span = lineReads[lineReads.length - 1].line - lineReads[0].line;
642
+ const timeSpan = Math.max(...lineReads.map((x) => x.ts)) - Math.min(...lineReads.map((x) => x.ts));
643
+ if (span <= 800 && timeSpan <= 10 * 60_000) {
644
+ readFragmentation.push({ path, count: lineReads.length, line_span: span, time_span_ms: timeSpan });
645
+ }
646
+ }
647
+ }
648
+ readFragmentation.sort((a, b) => b.count - a.count);
649
+
650
+ const grepSweeps = [];
651
+ for (const [sid, srows] of groupBy(tools.filter((r) => field(r, 'tool_name') === 'grep'), sessionId).entries()) {
652
+ const sorted = [...srows].sort((a, b) => Number(a.ts || 0) - Number(b.ts || 0));
653
+ let cluster = [];
654
+ const flush = () => {
655
+ if (cluster.length < GREP_SWEEP_MIN_CALLS) { cluster = []; return; }
656
+ const unique = new Set(cluster.map(grepSweepKey));
657
+ const spanMs = Number(cluster[cluster.length - 1].ts || 0) - Number(cluster[0].ts || 0);
658
+ if (unique.size >= GREP_SWEEP_MIN_UNIQUE && spanMs <= 10 * 60_000) {
659
+ const pathCounts = countBy(cluster, (r) => normalizePathForCompare(String((toolArgs(r) || {}).path || '.'))).slice(0, 3);
660
+ const patterns = cluster.slice(0, 5).map((r) => compactText(String((toolArgs(r) || {}).pattern || ''), 70));
661
+ grepSweeps.push({
662
+ session_id: sid,
663
+ count: cluster.length,
664
+ unique_queries: unique.size,
665
+ span_ms: spanMs,
666
+ paths: pathCounts.map(([path, count]) => ({ path, count })),
667
+ examples: patterns,
668
+ });
669
+ }
670
+ cluster = [];
671
+ };
672
+ for (const row of sorted) {
673
+ const prev = cluster[cluster.length - 1];
674
+ if (!prev || Number(row.ts || 0) - Number(prev.ts || 0) <= 45_000) cluster.push(row);
675
+ else { flush(); cluster = [row]; }
676
+ }
677
+ flush();
678
+ }
679
+ grepSweeps.sort((a, b) => b.count - a.count || b.unique_queries - a.unique_queries);
680
+
681
+ const singleToolBatches = rows.filter((r) => r.kind === 'batch' && Number(field(r, 'tool_call_count')) === 1)
682
+ .sort((a, b) => Number(a.ts || 0) - Number(b.ts || 0));
683
+ let missedParallelism = 0;
684
+ const singleToolBatchDetails = singleToolBatches.map((batch) => {
685
+ const bts = Number(batch.ts || 0);
686
+ const sid = sessionId(batch);
687
+ const tool = tools
688
+ .filter((r) => sessionId(r) === sid && Number(r.ts || 0) >= bts - 100 && Number(r.ts || 0) <= bts + 15_000)
689
+ .sort((a, b) => Number(a.ts || 0) - Number(b.ts || 0))[0] || null;
690
+ return {
691
+ batch,
692
+ ts: bts,
693
+ session_id: sid,
694
+ agent: field(batch, 'agent') || field(tool, 'agent') || null,
695
+ iteration: num(tool || batch, 'iteration'),
696
+ tool: field(tool, 'tool_name') || '(unknown)',
697
+ tool_ms: num(tool, 'tool_ms') || 0,
698
+ result_kind: tool ? toolResultKind(tool) : 'unknown',
699
+ target: tool ? summarizeToolTarget(tool) : '-',
700
+ };
701
+ });
702
+ const sequentialClusters = [];
703
+ let cluster = [];
704
+ for (let i = 1; i < singleToolBatches.length; i += 1) {
705
+ if (Number(singleToolBatches[i].ts || 0) - Number(singleToolBatches[i - 1].ts || 0) <= 15_000) missedParallelism += 1;
706
+ }
707
+ for (const detail of singleToolBatchDetails) {
708
+ const prev = cluster[cluster.length - 1];
709
+ if (prev && detail.session_id === prev.session_id && detail.ts - prev.ts <= 15_000) {
710
+ cluster.push(detail);
711
+ } else {
712
+ if (cluster.length >= 3) sequentialClusters.push(cluster);
713
+ cluster = [detail];
714
+ }
715
+ }
716
+ if (cluster.length >= 3) sequentialClusters.push(cluster);
717
+ const sequentialToolClusters = sequentialClusters.map((items) => {
718
+ const toolCounts = countBy(items, (x) => x.tool).slice(0, 5).map(([tool, count]) => ({ tool, count }));
719
+ const errorCount = items.filter((x) => x.result_kind === 'error').length;
720
+ return {
721
+ session_id: items[0].session_id,
722
+ agent: items[0].agent || null,
723
+ count: items.length,
724
+ span_ms: items[items.length - 1].ts - items[0].ts,
725
+ tool_ms: sum(items.map((x) => x.tool_ms)),
726
+ errors: errorCount,
727
+ tools: toolCounts,
728
+ start_it: items[0].iteration,
729
+ end_it: items[items.length - 1].iteration,
730
+ examples: items.slice(0, 3).map((x) => `${x.tool}:${compactText(x.target, 80)}`),
731
+ };
732
+ }).sort((a, b) => b.count - a.count || b.tool_ms - a.tool_ms).slice(0, 20);
733
+
734
+ return {
735
+ total_tool_calls: tools.length,
736
+ total_tool_ms: sum(tools.map((r) => num(r, 'tool_ms'))),
737
+ result_kinds: summarizeKindCounts(tools),
738
+ by_name: byName,
739
+ failures: failures.slice(0, 20),
740
+ recent_successes: successes.slice(0, 20),
741
+ duplicates: duplicates.slice(0, 20),
742
+ failed_repeats: failedRepeats.slice(0, 20),
743
+ broad_results: broadResults.slice(0, 20),
744
+ read_fragmentation: readFragmentation.slice(0, 20),
745
+ grep_sweeps: grepSweeps.slice(0, 20),
746
+ missed_parallelism_heuristic: {
747
+ consecutive_single_tool_batches: missedParallelism,
748
+ single_tool_batches: singleToolBatches.length,
749
+ },
750
+ sequential_tool_clusters: sequentialToolClusters,
751
+ };
752
+ }
753
+
754
+ function nearestRowBefore(rows, ts, maxBeforeMs = 120_000) {
755
+ const t = Number(ts);
756
+ if (!Number.isFinite(t)) return null;
757
+ let best = null;
758
+ let bestDelta = Infinity;
759
+ for (const row of rows) {
760
+ const rts = Number(row.ts || 0);
761
+ if (!Number.isFinite(rts) || rts > t) continue;
762
+ const delta = t - rts;
763
+ if (delta <= maxBeforeMs && delta < bestDelta) {
764
+ best = row;
765
+ bestDelta = delta;
766
+ }
767
+ }
768
+ return best;
769
+ }
770
+
771
+ function nearestRowAround(rows, ts, maxDeltaMs = 1_000, predicate = null) {
772
+ const t = Number(ts);
773
+ if (!Number.isFinite(t)) return null;
774
+ let best = null;
775
+ let bestDelta = Infinity;
776
+ for (const row of rows) {
777
+ if (predicate && !predicate(row)) continue;
778
+ const rts = Number(row.ts || 0);
779
+ if (!Number.isFinite(rts)) continue;
780
+ const delta = Math.abs(rts - t);
781
+ if (delta <= maxDeltaMs && delta < bestDelta) {
782
+ best = row;
783
+ bestDelta = delta;
784
+ }
785
+ }
786
+ return best;
787
+ }
788
+
789
+ function buildTurnDiagnostics(rows, routeGroups) {
790
+ const sessionMeta = new Map(routeGroups.map((g) => [g.session_id, g]));
791
+ const bySid = groupBy(rows, sessionId);
792
+ const turns = [];
793
+ for (const [sid, srows] of bySid.entries()) {
794
+ const meta = sessionMeta.get(sid) || inferSessionMeta(srows);
795
+ const usageRows = srows.filter((r) => r.kind === 'usage_raw');
796
+ const fetchRows = srows.filter((r) => r.kind === 'fetch');
797
+ const sseRows = srows.filter((r) => r.kind === 'sse');
798
+ const transportRows = srows.filter((r) => r.kind === 'transport');
799
+ const cacheBreakRows = srows.filter((r) => r.kind === 'cache_break');
800
+ const toolRows = srows.filter((r) => r.kind === 'tool');
801
+ const usageSorted = [...usageRows].sort((a, b) => Number(a.ts || 0) - Number(b.ts || 0));
802
+ const seenByIteration = new Map();
803
+
804
+ const pushTurn = ({ usage = null, transport = null, tools = [], cacheBreaks = [], nextTs = Infinity }) => {
805
+ const iteration = num(usage || transport || tools[0] || cacheBreaks[0], 'iteration');
806
+ const occurrence = (seenByIteration.get(iteration) || 0) + 1;
807
+ seenByIteration.set(iteration, occurrence);
808
+ const tRef = Number(usage?.ts || transport?.ts || tools.at(-1)?.ts || 0);
809
+ const sse = usage ? nearestRowBefore(sseRows, usage.ts, 10_000) : nearestRowBefore(sseRows, tRef, 10_000);
810
+ const streamMs = sse ? (num(sse, 'stream_total_ms') ?? num(sse, 'sse_parse_ms') ?? 0) : 0;
811
+ const fetchWindowMs = Math.max(30_000, streamMs + 10_000);
812
+ const fetch = transport ? nearestRowBefore(fetchRows, transport.ts, fetchWindowMs) : nearestRowBefore(fetchRows, tRef, fetchWindowMs);
813
+ const promptTokens = usage ? cacheDenom(usage) : 0;
814
+ const cachedTokens = usage ? (num(usage, 'cached_tokens') || 0) : 0;
815
+ const cacheRatio = promptTokens > 0 ? cachedTokens / promptTokens : null;
816
+ const headersMs = fetch ? (num(fetch, 'headers_ms') || 0) : 0;
817
+ const toolMs = sum(tools.map((r) => num(r, 'tool_ms')));
818
+ const outputTokens = usage ? (num(usage, 'output_tokens') || 0) : 0;
819
+ const thinkingTokens = usage ? (num(usage, 'thinking_tokens') || 0) : 0;
820
+ const serviceRequested = transport ? field(transport, 'requested_service_tier') : null;
821
+ const serviceResponse = transport ? field(transport, 'response_service_tier') : field(usage, 'service_tier');
822
+ const flags = [];
823
+ if (field(transport, 'ws_mode') === 'full') flags.push('full_ws');
824
+ if (cacheBreaks.length) flags.push(`cache_break:${cacheBreaks.map((r) => field(r, 'reason') || field(r, 'chain_delta_reason') || field(r, 'payload')?.reason || 'unknown').join('|')}`);
825
+ if (serviceRequested && serviceResponse && serviceRequested !== serviceResponse) flags.push(`tier:${serviceRequested}->${serviceResponse}`);
826
+ if (streamMs >= 15_000) flags.push('slow_stream');
827
+ if (headersMs >= 5_000) flags.push('slow_headers');
828
+ if (toolMs >= 5_000) flags.push('slow_tools');
829
+ if (promptTokens >= 50_000 && cacheRatio != null && cacheRatio < 0.8) flags.push('low_cache_large_prompt');
830
+ turns.push({
831
+ session_id: sid,
832
+ agent: meta.agent || null,
833
+ model: meta.model || null,
834
+ provider: meta.provider || null,
835
+ iteration,
836
+ occurrence,
837
+ turn_label: occurrence > 1 ? `${iteration ?? '-'}#${occurrence}` : String(iteration ?? '-'),
838
+ ts: tRef || null,
839
+ headers_ms: headersMs,
840
+ stream_ms: streamMs,
841
+ tool_ms: toolMs,
842
+ approx_active_ms: headersMs + streamMs + toolMs,
843
+ tool_calls: tools.length,
844
+ prompt_tokens: promptTokens,
845
+ output_tokens: outputTokens,
846
+ thinking_tokens: thinkingTokens,
847
+ cached_tokens: cachedTokens,
848
+ cache_ratio: cacheRatio,
849
+ ws_mode: transport ? field(transport, 'ws_mode') : null,
850
+ reused_connection: transport ? field(transport, 'reused_connection') : null,
851
+ has_previous_response_id: transport ? field(transport, 'request_has_previous_response_id') : null,
852
+ service_requested: serviceRequested,
853
+ service_response: serviceResponse,
854
+ cache_breaks: cacheBreaks.map((r) => field(r, 'reason') || field(r, 'chain_delta_reason') || field(r, 'payload')?.reason || 'unknown'),
855
+ top_tools: Object.values(tools.reduce((acc, r) => {
856
+ const name = String(field(r, 'tool_name') || '(unknown)');
857
+ if (!acc[name]) acc[name] = { tool: name, count: 0, ms: 0 };
858
+ acc[name].count += 1;
859
+ acc[name].ms += num(r, 'tool_ms') || 0;
860
+ return acc;
861
+ }, {})).sort((a, b) => b.ms - a.ms || b.count - a.count).slice(0, 3),
862
+ flags,
863
+ });
864
+ };
865
+
866
+ for (let idx = 0; idx < usageSorted.length; idx += 1) {
867
+ const usage = usageSorted[idx];
868
+ const iteration = num(usage, 'iteration');
869
+ const ts = Number(usage.ts || 0);
870
+ const nextTs = Number(usageSorted[idx + 1]?.ts || Infinity);
871
+ const inWindow = (r) => {
872
+ const rts = Number(r.ts || 0);
873
+ return Number.isFinite(rts) && rts >= ts - 100 && rts < nextTs;
874
+ };
875
+ const sameIteration = (r) => num(r, 'iteration') === iteration;
876
+ const transport = nearestRowAround(transportRows, ts, 1_000, sameIteration);
877
+ const tools = toolRows.filter((r) => sameIteration(r) && inWindow(r));
878
+ const cacheBreaks = cacheBreakRows.filter((r) => sameIteration(r) && inWindow(r));
879
+ pushTurn({ usage, transport, tools, cacheBreaks, nextTs });
880
+ }
881
+
882
+ const usageMatchedTransports = new Set(usageSorted
883
+ .map((usage) => nearestRowAround(transportRows, usage.ts, 1_000, (r) => num(r, 'iteration') === num(usage, 'iteration')))
884
+ .filter(Boolean));
885
+ for (const transport of transportRows.filter((r) => !usageMatchedTransports.has(r)).sort((a, b) => Number(a.ts || 0) - Number(b.ts || 0))) {
886
+ const iteration = num(transport, 'iteration');
887
+ const ts = Number(transport.ts || 0);
888
+ const nextUsage = usageSorted.find((r) => Number(r.ts || 0) > ts);
889
+ const nextTs = Number(nextUsage?.ts || Infinity);
890
+ const tools = toolRows.filter((r) => num(r, 'iteration') === iteration && Number(r.ts || 0) >= ts - 100 && Number(r.ts || 0) < nextTs);
891
+ const cacheBreaks = cacheBreakRows.filter((r) => num(r, 'iteration') === iteration && Number(r.ts || 0) >= ts - 100 && Number(r.ts || 0) < nextTs);
892
+ pushTurn({ transport, tools, cacheBreaks, nextTs });
893
+ }
894
+ }
895
+ const slowestActive = [...turns].sort((a, b) => b.approx_active_ms - a.approx_active_ms).slice(0, 20);
896
+ const slowestStream = [...turns].sort((a, b) => b.stream_ms - a.stream_ms).slice(0, 20);
897
+ const slowestTools = [...turns].sort((a, b) => b.tool_ms - a.tool_ms).slice(0, 20);
898
+ const cacheBreakTurns = turns.filter((t) => t.cache_breaks.length > 0);
899
+ return { turns, slowest_active: slowestActive, slowest_stream: slowestStream, slowest_tools: slowestTools, cache_break_turns: cacheBreakTurns };
900
+ }
901
+
902
+ function buildTokenDiagnostics(turns) {
903
+ const bySession = groupBy([...turns].sort((a, b) => Number(a.ts || 0) - Number(b.ts || 0)), (t) => t.session_id);
904
+ const sessions = [];
905
+ const growthTurns = [];
906
+ const outputHeavy = [];
907
+ const cacheMissCost = [];
908
+
909
+ for (const [sid, trows] of bySession.entries()) {
910
+ const useful = trows.filter((t) => Number(t.prompt_tokens || 0) > 0);
911
+ if (!useful.length) continue;
912
+ const first = useful[0];
913
+ const last = useful[useful.length - 1];
914
+ const maxPromptTurn = useful.reduce((best, t) => (Number(t.prompt_tokens || 0) > Number(best.prompt_tokens || 0) ? t : best), useful[0]);
915
+ const totalOutput = sum(useful.map((t) => t.output_tokens));
916
+ const totalThinking = sum(useful.map((t) => t.thinking_tokens));
917
+ const uncachedTokens = sum(useful.map((t) => Math.max(0, (t.prompt_tokens || 0) - (t.cached_tokens || 0))));
918
+ sessions.push({
919
+ session_id: sid,
920
+ agent: last.agent || first.agent || null,
921
+ turns: useful.length,
922
+ first_prompt: first.prompt_tokens || 0,
923
+ last_prompt: last.prompt_tokens || 0,
924
+ max_prompt: maxPromptTurn.prompt_tokens || 0,
925
+ max_prompt_it: maxPromptTurn.turn_label || maxPromptTurn.iteration,
926
+ prompt_growth: Math.max(0, (last.prompt_tokens || 0) - (first.prompt_tokens || 0)),
927
+ total_output: totalOutput,
928
+ total_thinking: totalThinking,
929
+ uncached_tokens: uncachedTokens,
930
+ cache_ratio: cacheRatioFromUsage(useful.map((t) => ({
931
+ prompt_tokens: t.prompt_tokens,
932
+ cached_tokens: t.cached_tokens,
933
+ }))),
934
+ });
935
+
936
+ for (let i = 0; i < useful.length; i += 1) {
937
+ const t = useful[i];
938
+ const prev = useful[i - 1] || null;
939
+ const promptDelta = prev ? (t.prompt_tokens || 0) - (prev.prompt_tokens || 0) : 0;
940
+ const out = t.output_tokens || 0;
941
+ const uncached = Math.max(0, (t.prompt_tokens || 0) - (t.cached_tokens || 0));
942
+ const growthPerOutput = out > 0 ? promptDelta / out : null;
943
+ const tokenRow = {
944
+ session_id: sid,
945
+ agent: t.agent || null,
946
+ iteration: t.iteration,
947
+ turn_label: t.turn_label || (t.iteration ?? '-'),
948
+ prompt_tokens: t.prompt_tokens || 0,
949
+ prompt_delta: promptDelta,
950
+ output_tokens: out,
951
+ thinking_tokens: t.thinking_tokens || 0,
952
+ cached_tokens: t.cached_tokens || 0,
953
+ cache_ratio: t.cache_ratio,
954
+ uncached_tokens: uncached,
955
+ growth_per_output: growthPerOutput,
956
+ ts: t.ts,
957
+ };
958
+ if (promptDelta >= 5_000 || (growthPerOutput != null && growthPerOutput >= 10)) growthTurns.push(tokenRow);
959
+ if (out >= 1_000 || (t.thinking_tokens || 0) >= 1_000) outputHeavy.push(tokenRow);
960
+ if (uncached >= 10_000 || ((t.cache_ratio ?? 1) < 0.8 && (t.prompt_tokens || 0) >= 20_000)) cacheMissCost.push(tokenRow);
961
+ }
962
+ }
963
+
964
+ sessions.sort((a, b) => b.prompt_growth - a.prompt_growth || b.total_output - a.total_output);
965
+ growthTurns.sort((a, b) => b.prompt_delta - a.prompt_delta);
966
+ outputHeavy.sort((a, b) => (b.output_tokens + b.thinking_tokens) - (a.output_tokens + a.thinking_tokens));
967
+ cacheMissCost.sort((a, b) => b.uncached_tokens - a.uncached_tokens);
968
+ return {
969
+ sessions,
970
+ growth_turns: growthTurns.slice(0, 20),
971
+ output_heavy_turns: outputHeavy.slice(0, 20),
972
+ cache_miss_cost_turns: cacheMissCost.slice(0, 20),
973
+ };
974
+ }
975
+
976
+ function buildCompactDiagnostics(rows, selectedIds) {
977
+ const selectedBase = new Set(selectedIds.map(baseSessionId));
978
+ const compactRows = rows.filter((r) => {
979
+ const sid = sessionId(r);
980
+ return isCompactSessionId(sid) && selectedBase.has(baseSessionId(sid));
981
+ });
982
+ const metaRows = rows.filter((r) => {
983
+ if (r.kind !== 'compact_meta') return false;
984
+ return selectedBase.has(baseSessionId(sessionId(r)));
985
+ }).sort((a, b) => Number(b.ts || 0) - Number(a.ts || 0));
986
+ const cache = buildCacheDiagnostics(compactRows);
987
+ const usageRows = compactRows.filter((r) => r.kind === 'usage_raw');
988
+ const transportRows = compactRows.filter((r) => r.kind === 'transport');
989
+ const sessions = [];
990
+ for (const [sid, srows] of groupBy(compactRows, sessionId).entries()) {
991
+ const usage = srows.filter((r) => r.kind === 'usage_raw');
992
+ const transports = srows.filter((r) => r.kind === 'transport');
993
+ const breaks = srows.filter((r) => r.kind === 'cache_break');
994
+ const sseRows = srows.filter((r) => r.kind === 'sse');
995
+ sessions.push({
996
+ session_id: sid,
997
+ base_session_id: baseSessionId(sid),
998
+ turns: usage.length,
999
+ prompt_tokens: sum(usage.map((r) => cacheDenom(r))),
1000
+ cached_tokens: sum(usage.map((r) => num(r, 'cached_tokens'))),
1001
+ output_tokens: sum(usage.map((r) => num(r, 'output_tokens'))),
1002
+ thinking_tokens: sum(usage.map((r) => num(r, 'thinking_tokens'))),
1003
+ stream_ms: sum(sseRows.map((r) => num(r, 'stream_total_ms') ?? num(r, 'sse_parse_ms'))),
1004
+ cache_ratio: cacheRatioFromUsage(usage),
1005
+ breaks: breaks.length,
1006
+ no_anchor: breaks.filter((r) => String(field(r, 'reason') || field(r, 'chain_delta_reason') || '').includes('no_anchor')).length,
1007
+ full_ws: transports.filter((r) => field(r, 'ws_mode') === 'full').length,
1008
+ delta_ws: transports.filter((r) => field(r, 'ws_mode') === 'delta').length,
1009
+ });
1010
+ }
1011
+ sessions.sort((a, b) => b.stream_ms - a.stream_ms || b.prompt_tokens - a.prompt_tokens);
1012
+ return {
1013
+ rows: compactRows.length,
1014
+ sessions,
1015
+ meta_rows: metaRows.length,
1016
+ recent_meta: metaRows.slice(0, 10).map((r) => {
1017
+ const details = field(r, 'details') || {};
1018
+ const semantic = details?.semantic || null;
1019
+ const recall = details?.recallFastTrack || null;
1020
+ const pipe = recall?.pipeline || null;
1021
+ return {
1022
+ session_id: sessionId(r),
1023
+ ts: r.ts || null,
1024
+ iteration: num(r, 'iteration'),
1025
+ stage: field(r, 'stage'),
1026
+ trigger: field(r, 'trigger'),
1027
+ compact_type: field(r, 'compact_type'),
1028
+ changed: field(r, 'compact_changed') === true,
1029
+ before_tokens: num(r, 'message_tokens_est'),
1030
+ pressure_tokens: num(r, 'pressure_tokens'),
1031
+ trigger_tokens: num(r, 'trigger_tokens'),
1032
+ boundary_tokens: num(r, 'boundary_tokens') ?? num(r, 'budget_tokens'),
1033
+ target_budget_tokens: num(r, 'target_budget_tokens'),
1034
+ after_tokens: recall?.finalTokens ?? semantic?.finalTokens ?? null,
1035
+ before_messages: num(r, 'before_count'),
1036
+ after_messages: num(r, 'after_count'),
1037
+ duration_ms: num(r, 'duration_ms'),
1038
+ error: field(r, 'error'),
1039
+ recall_pipeline: pipe ? {
1040
+ ingest_ms: pipe.ingestMs ?? null,
1041
+ initial_dump_ms: pipe.initialDumpMs ?? null,
1042
+ initial_raw_pending: pipe.initialRawPending ?? null,
1043
+ cycle1_ms: pipe.cycle1Ms ?? null,
1044
+ cycle1_passes: pipe.cycle1Passes ?? null,
1045
+ cycle1_raw_remaining: pipe.cycle1RawRemaining ?? null,
1046
+ final_recall_kb: pipe.finalRecallBytes != null ? Math.round(pipe.finalRecallBytes / 1024) : null,
1047
+ } : null,
1048
+ fit: recall || semantic ? {
1049
+ head_messages: (recall || semantic).headMessages ?? null,
1050
+ tail_messages: (recall || semantic).tailMessages ?? null,
1051
+ mandatory_cost: (recall || semantic).mandatoryCost ?? null,
1052
+ remaining_tokens: (recall || semantic).remainingTokens ?? null,
1053
+ budget_raised: (recall || semantic).budgetRaised === true,
1054
+ final_tokens: (recall || semantic).finalTokens ?? null,
1055
+ recall_chars: recall?.recallChars ?? null,
1056
+ prior_chars: recall?.priorChars ?? null,
1057
+ tail_truncated: recall?.tailTruncated ?? null,
1058
+ summary_repaired: semantic?.summaryRepaired ?? null,
1059
+ } : null,
1060
+ };
1061
+ }),
1062
+ cache_breaks: cache.cache_breaks,
1063
+ usage_rows: usageRows.length,
1064
+ transport_rows: transportRows.length,
1065
+ };
1066
+ }
1067
+
1068
+ function buildIssues(routeGroups, cache, tools) {
1069
+ const issues = [];
1070
+ for (const g of routeGroups) {
1071
+ if (g.ws_full > 0 && g.ws_delta === 0 && g.transport_rows > 0) {
1072
+ issues.push({ severity: 'high', type: 'cache', message: `${g.agent || shortId(g.session_id)} stayed full WS (${g.ws_full} full, 0 delta)`, session_id: g.session_id });
1073
+ } else if (g.ws_full > 0) {
1074
+ issues.push({ severity: 'medium', type: 'cache', message: `${g.agent || shortId(g.session_id)} had ${g.ws_full} full WS turn(s) before delta`, session_id: g.session_id });
1075
+ }
1076
+ if (g.cache_ratio != null && g.prompt_tokens > 5000 && g.cache_ratio < 0.25) {
1077
+ issues.push({ severity: 'high', type: 'cache', message: `${g.agent || shortId(g.session_id)} low cache ratio ${fmtPct(g.cache_ratio * 100)}`, session_id: g.session_id });
1078
+ }
1079
+ }
1080
+ for (const b of cache.cache_breaks.slice(0, 10)) {
1081
+ issues.push({ severity: b.reason === 'no_anchor' ? 'medium' : 'high', type: 'cache_break', message: `cache_break ${b.reason || 'unknown'} at it=${b.iteration ?? '-'}`, session_id: b.session_id });
1082
+ }
1083
+ if (cache.service_tier_downgrades.length) {
1084
+ const first = cache.service_tier_downgrades[0];
1085
+ issues.push({ severity: 'medium', type: 'service_tier', message: `${cache.service_tier_downgrades.length} service tier downgrade(s), e.g. ${first.requested}->${first.response}`, session_id: first.session_id });
1086
+ }
1087
+ if (tools.failures.length) {
1088
+ const byTool = new Map();
1089
+ for (const f of tools.failures) byTool.set(f.tool || '(unknown)', (byTool.get(f.tool || '(unknown)') || 0) + 1);
1090
+ const summary = [...byTool.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([tool, count]) => `${tool}×${count}`).join(', ');
1091
+ issues.push({ severity: 'medium', type: 'tool_error', message: `${tools.failures.length} failed tool call(s): ${summary}` });
1092
+ }
1093
+ for (const dup of tools.duplicates.slice(0, 5)) {
1094
+ issues.push({ severity: 'low', type: 'duplicate_tool', message: `duplicate ${dup.tool} x${dup.count}: ${dup.target}`, args_hash: dup.args_hash });
1095
+ }
1096
+ for (const fail of tools.failed_repeats.slice(0, 5)) {
1097
+ issues.push({ severity: 'medium', type: 'failed_tool_repeat', message: `repeated failing ${fail.tool} x${fail.error_count}: ${fail.target}`, args_hash: fail.args_hash });
1098
+ }
1099
+ for (const broad of tools.broad_results.slice(0, 5)) {
1100
+ issues.push({ severity: 'medium', type: 'broad_tool_result', message: `${broad.tool} returned ${Math.round(broad.bytes / 1024)}KB/${broad.lines} lines: ${broad.target}`, session_id: broad.session_id });
1101
+ }
1102
+ for (const frag of tools.read_fragmentation.slice(0, 5)) {
1103
+ issues.push({ severity: 'low', type: 'read_fragmentation', message: `read fragmentation x${frag.count} within ${frag.line_span} lines: ${frag.path}` });
1104
+ }
1105
+ for (const sweep of (tools.grep_sweeps || []).slice(0, 3)) {
1106
+ const scope = sweep.paths?.[0]?.path || '-';
1107
+ issues.push({ severity: 'low', type: 'grep_sweep', message: `grep sweep x${sweep.count}/${sweep.unique_queries} over ${fmtMs(sweep.span_ms)} in ${scope}`, session_id: sweep.session_id });
1108
+ }
1109
+ if (tools.missed_parallelism_heuristic.consecutive_single_tool_batches >= 3) {
1110
+ issues.push({ severity: 'low', type: 'missed_parallelism', message: `${tools.missed_parallelism_heuristic.consecutive_single_tool_batches} close consecutive single-tool batches` });
1111
+ }
1112
+ if (tools.sequential_tool_clusters?.length) {
1113
+ const c = tools.sequential_tool_clusters[0];
1114
+ const toolSummary = c.tools.map((x) => `${x.tool}×${x.count}`).join(', ');
1115
+ issues.push({ severity: 'low', type: 'tool_churn_cluster', message: `sequential single-tool cluster x${c.count} over ${fmtMs(c.span_ms)}: ${toolSummary}` });
1116
+ }
1117
+ const rank = { high: 0, medium: 1, low: 2 };
1118
+ return issues.sort((a, b) => (rank[a.severity] ?? 9) - (rank[b.severity] ?? 9));
1119
+ }
1120
+
1121
+ function countBy(items, keyFn) {
1122
+ const map = new Map();
1123
+ for (const item of items || []) {
1124
+ const key = keyFn(item);
1125
+ map.set(key, (map.get(key) || 0) + 1);
1126
+ }
1127
+ return [...map.entries()].sort((a, b) => b[1] - a[1]);
1128
+ }
1129
+
1130
+ function buildWhySlowRankings(report) {
1131
+ const ranks = [];
1132
+ const topStream = report.stages.slowest_stream[0];
1133
+ if (topStream && topStream.stream_ms > 0) {
1134
+ ranks.push({ score: topStream.stream_ms / 1000, type: 'slow_stream', message: `${topStream.agent || '-'} it=${topStream.turn_label || topStream.iteration} stream=${fmtMs(topStream.stream_ms)} prompt=${fmtTok(topStream.prompt_tokens)} out=${fmtTok(topStream.output_tokens + topStream.thinking_tokens)}` });
1135
+ }
1136
+ const topTool = report.stages.slowest_tools[0];
1137
+ if (topTool && topTool.tool_ms > 0) {
1138
+ const label = topTool.top_tools.map((x) => `${x.tool}×${x.count}/${fmtMs(x.ms)}`).join(', ');
1139
+ ranks.push({ score: topTool.tool_ms / 1000, type: 'slow_tools', message: `${topTool.agent || '-'} it=${topTool.turn_label || topTool.iteration} tools=${fmtMs(topTool.tool_ms)} ${label}` });
1140
+ }
1141
+ if (report.tools.failures.length) {
1142
+ const summary = countBy(report.tools.failures, (f) => f.category || f.tool || 'unknown').slice(0, 3).map(([k, v]) => `${k}×${v}`).join(', ');
1143
+ ranks.push({ score: 50 + report.tools.failures.length, type: 'tool_failures', message: `${report.tools.failures.length} failed tool call(s): ${summary}` });
1144
+ }
1145
+ if (report.cache.cache_breaks.length) {
1146
+ const summary = countBy(report.cache.cache_breaks, (b) => `${b.reason || 'unknown'}/${b.phase || 'unknown'}`).slice(0, 3).map(([k, v]) => `${k}×${v}`).join(', ');
1147
+ ranks.push({ score: 40 + report.cache.cache_breaks.length, type: 'cache_breaks', message: `${report.cache.cache_breaks.length} cache break(s): ${summary}` });
1148
+ }
1149
+ const topGrowth = report.tokens.growth_turns[0];
1150
+ if (topGrowth) {
1151
+ ranks.push({ score: Math.max(0, topGrowth.prompt_delta) / 1000, type: 'prompt_growth', message: `${topGrowth.agent || '-'} it=${topGrowth.turn_label} prompt Δ=${fmtTok(topGrowth.prompt_delta)} out=${fmtTok(topGrowth.output_tokens + topGrowth.thinking_tokens)} cache=${fmtPct((topGrowth.cache_ratio ?? 0) * 100)}` });
1152
+ }
1153
+ const topUncached = report.tokens.cache_miss_cost_turns[0];
1154
+ if (topUncached) {
1155
+ ranks.push({ score: topUncached.uncached_tokens / 1000, type: 'uncached_prompt', message: `${topUncached.agent || '-'} it=${topUncached.turn_label} uncached=${fmtTok(topUncached.uncached_tokens)} prompt=${fmtTok(topUncached.prompt_tokens)} cache=${fmtPct((topUncached.cache_ratio ?? 0) * 100)}` });
1156
+ }
1157
+ if (report.tools.duplicates.length) {
1158
+ const d = report.tools.duplicates[0];
1159
+ ranks.push({ score: 10 + d.count, type: 'tool_churn', message: `${d.tool} repeated x${d.count} kinds=${fmtKindCounts(d.result_kinds)}: ${d.target}` });
1160
+ }
1161
+ return ranks.sort((a, b) => b.score - a.score).slice(0, 10);
1162
+ }
1163
+
1164
+ function buildExecutiveSummary(report) {
1165
+ const lines = [];
1166
+ const headers = report.summary.headers_ms || 0;
1167
+ const stream = report.summary.llm_stream_ms || 0;
1168
+ const tools = report.summary.total_tool_ms || 0;
1169
+ const total = headers + stream + tools;
1170
+ const dominant = [['stream', stream], ['tools', tools], ['headers', headers]].sort((a, b) => b[1] - a[1])[0];
1171
+ lines.push(`주 병목=${dominant[0]} ${fmtMs(dominant[1])}/${fmtMs(total)}; cache=${fmtPct((report.summary.cache_ratio ?? 0) * 100)}; turns=${report.summary.turns}; tools=${report.summary.tool_calls}`);
1172
+ if (report.cache.cache_breaks.length) {
1173
+ const summary = countBy(report.cache.cache_breaks, (b) => `${b.reason || 'unknown'}/${b.phase || 'unknown'}`).slice(0, 3).map(([k, v]) => `${k}×${v}`).join(', ');
1174
+ lines.push(`캐시 깨짐=${report.cache.cache_breaks.length} (${summary})`);
1175
+ }
1176
+ if (report.compact?.cache_breaks?.length) {
1177
+ const summary = countBy(report.compact.cache_breaks, (b) => `${b.reason || 'unknown'}/${b.phase || 'unknown'}`).slice(0, 3).map(([k, v]) => `${k}×${v}`).join(', ');
1178
+ lines.push(`컴팩트 호출=${report.compact.sessions.length} session, cache reset=${report.compact.cache_breaks.length} (${summary})`);
1179
+ }
1180
+ if (report.tools.failures.length) {
1181
+ const summary = countBy(report.tools.failures, (f) => f.category || f.tool || 'unknown').slice(0, 3).map(([k, v]) => `${k}×${v}`).join(', ');
1182
+ lines.push(`툴 실패=${report.tools.failures.length} (${summary})`);
1183
+ }
1184
+ const topGrowth = report.tokens.growth_turns[0];
1185
+ if (topGrowth) lines.push(`토큰 증폭=${topGrowth.agent || '-'} it=${topGrowth.turn_label} promptΔ=${fmtTok(topGrowth.prompt_delta)} out=${fmtTok(topGrowth.output_tokens + topGrowth.thinking_tokens)}`);
1186
+ const topChurn = report.tools.duplicates[0];
1187
+ if (topChurn) lines.push(`툴 헛돎=${topChurn.tool}×${topChurn.count} ${compactText(topChurn.target, 90)}`);
1188
+ return lines;
1189
+ }
1190
+
1191
+ function buildReport(rows, selectedIds, failureRows = []) {
1192
+ const selected = rows.filter((r) => selectedIds.includes(sessionId(r)));
1193
+ const selectedFailures = failureRows.filter((r) => selectedIds.includes(sessionId(r)));
1194
+ const routeGroups = buildRouteGroups(selected);
1195
+ const cache = buildCacheDiagnostics(selected);
1196
+ const tools = buildToolDiagnostics(selected, selectedFailures);
1197
+ const stages = buildTurnDiagnostics(selected, routeGroups);
1198
+ const tokens = buildTokenDiagnostics(stages.turns);
1199
+ const compact = buildCompactDiagnostics(rows, selectedIds);
1200
+ const issues = buildIssues(routeGroups, cache, tools);
1201
+ const tsValues = selected.map((r) => Number(r.ts || 0)).filter((n) => n > 0);
1202
+ const report = {
1203
+ trace: opts.trace || defaultTracePath(),
1204
+ selected_sessions: selectedIds,
1205
+ time_range: {
1206
+ start_ts: tsValues.length ? Math.min(...tsValues) : null,
1207
+ end_ts: tsValues.length ? Math.max(...tsValues) : null,
1208
+ start: tsValues.length ? fmtTime(Math.min(...tsValues)) : null,
1209
+ end: tsValues.length ? fmtTime(Math.max(...tsValues)) : null,
1210
+ span_ms: tsValues.length ? Math.max(...tsValues) - Math.min(...tsValues) : 0,
1211
+ },
1212
+ summary: {
1213
+ row_count: selected.length,
1214
+ sessions: selectedIds.length,
1215
+ turns: sum(routeGroups.map((g) => g.turns)),
1216
+ tool_calls: tools.total_tool_calls,
1217
+ total_tool_ms: tools.total_tool_ms,
1218
+ llm_stream_ms: sum(routeGroups.map((g) => g.llm_stream_ms)),
1219
+ headers_ms: sum(routeGroups.map((g) => g.headers_ms)),
1220
+ cache_ratio: cache.usage_cache_ratio,
1221
+ issues: issues.length,
1222
+ },
1223
+ routeGroups,
1224
+ cache,
1225
+ tools,
1226
+ stages,
1227
+ tokens,
1228
+ compact,
1229
+ issues,
1230
+ selected_tool_failures: selectedFailures.length,
1231
+ };
1232
+ report.rankings = buildWhySlowRankings(report);
1233
+ report.executive_summary = buildExecutiveSummary(report);
1234
+ return report;
1235
+ }
1236
+
1237
+ function renderText(report) {
1238
+ const lines = [];
1239
+ const focused = opts.failuresOnly || opts.compactOnly || opts.tokensOnly || opts.slowOnly;
1240
+ lines.push(`Session bench (${report.selected_sessions.map(shortId).join(', ')})`);
1241
+ lines.push(`range: ${report.time_range.start || '-'} → ${report.time_range.end || '-'} (${fmtSec(report.time_range.span_ms)})`);
1242
+ lines.push(`turns=${report.summary.turns} tools=${report.summary.tool_calls} llm_stream=${fmtMs(report.summary.llm_stream_ms)} tool_time=${fmtMs(report.summary.total_tool_ms)} cache=${fmtPct((report.summary.cache_ratio ?? 0) * 100)}`);
1243
+ lines.push('');
1244
+
1245
+ if (!opts.issuesOnly && !opts.cacheOnly && !opts.toolsOnly && !opts.compactOnly && !opts.tokensOnly && !opts.failuresOnly) {
1246
+ lines.push('Executive summary');
1247
+ for (const line of report.executive_summary || []) lines.push(`- ${line}`);
1248
+ if (report.rankings?.length) {
1249
+ lines.push('why slow / risk ranking:');
1250
+ for (const r of report.rankings.slice(0, Math.min(opts.limit, 8))) lines.push(`- [${r.type}] ${r.message}`);
1251
+ }
1252
+ lines.push('');
1253
+ }
1254
+
1255
+ const pushCompactDiagnostics = () => {
1256
+ if (!(report.compact?.sessions?.length || report.compact?.recent_meta?.length || report.compact?.cache_breaks?.length)) {
1257
+ lines.push('compact diagnostics: none');
1258
+ return;
1259
+ }
1260
+ lines.push('compact diagnostics:');
1261
+ if (report.compact.sessions.length) {
1262
+ const ctable = [['session', 'turns', 'stream', 'prompt', 'out', 'cache', 'breaks', 'WS']];
1263
+ for (const c of report.compact.sessions.slice(0, 5)) {
1264
+ ctable.push([
1265
+ shortId(c.session_id),
1266
+ c.turns,
1267
+ fmtMs(c.stream_ms),
1268
+ fmtTok(c.prompt_tokens),
1269
+ fmtTok(c.output_tokens + c.thinking_tokens),
1270
+ fmtPct((c.cache_ratio ?? 0) * 100),
1271
+ `${c.breaks}${c.no_anchor ? `/no_anchor×${c.no_anchor}` : ''}`,
1272
+ `${c.delta_ws}Δ/${c.full_ws}F`,
1273
+ ]);
1274
+ }
1275
+ lines.push(...padTable(ctable));
1276
+ }
1277
+ if (report.compact.recent_meta?.length) {
1278
+ lines.push('compact meta:');
1279
+ for (const m of report.compact.recent_meta.slice(0, Math.min(opts.limit, 10))) {
1280
+ const pipe = m.recall_pipeline;
1281
+ const fit = m.fit;
1282
+ const parts = [
1283
+ `- ${shortId(m.session_id)} it=${m.iteration ?? '-'} ${m.compact_type || '-'} ${fmtMs(m.duration_ms)} ${fmtTok(m.before_tokens)}→${fmtTok(m.after_tokens)}`,
1284
+ `pressure=${fmtTok(m.pressure_tokens)}/${fmtTok(m.trigger_tokens)}/${fmtTok(m.boundary_tokens)}`,
1285
+ `target=${fmtTok(m.target_budget_tokens)} changed=${m.changed}`,
1286
+ ];
1287
+ if (m.error) parts.push(`error=${compactText(m.error, 120)}`);
1288
+ lines.push(parts.join(' '));
1289
+ if (pipe) {
1290
+ lines.push(` recall pipeline: ingest=${fmtMs(pipe.ingest_ms)} dump=${fmtMs(pipe.initial_dump_ms)} raw=${pipe.initial_raw_pending ?? '-'} cycle1=${fmtMs(pipe.cycle1_ms)} passes=${pipe.cycle1_passes ?? '-'} rawLeft=${pipe.cycle1_raw_remaining ?? '-'} recall=${pipe.final_recall_kb ?? '-'}KB`);
1291
+ }
1292
+ if (fit) {
1293
+ const recallPart = fit.recall_chars != null
1294
+ ? ` recallChars=${fit.recall_chars} priorChars=${fit.prior_chars ?? 0} tailTrunc=${fit.tail_truncated}`
1295
+ : '';
1296
+ const semanticPart = fit.summary_repaired != null ? ` repaired=${fit.summary_repaired}` : '';
1297
+ lines.push(` fit: head=${fit.head_messages ?? '-'} tail=${fit.tail_messages ?? '-'} mandatory=${fmtTok(fit.mandatory_cost)} remain=${fmtTok(fit.remaining_tokens)} final=${fmtTok(fit.final_tokens)} raised=${fit.budget_raised}${recallPart}${semanticPart}`);
1298
+ }
1299
+ }
1300
+ }
1301
+ if (report.compact.cache_breaks.length) {
1302
+ lines.push('compact cache resets (intentional):');
1303
+ for (const b of report.compact.cache_breaks.slice(0, 5)) {
1304
+ lines.push(`- ${shortId(b.session_id)} it=${b.iteration ?? '-'} phase=${b.phase || '-'} reason=${b.reason || '-'} prompt=${fmtTok(b.prompt_tokens)} out=${fmtTok(b.output_tokens)}`);
1305
+ }
1306
+ }
1307
+ };
1308
+
1309
+ if (opts.compactOnly) {
1310
+ pushCompactDiagnostics();
1311
+ return lines.join('\n');
1312
+ }
1313
+
1314
+ if (!focused && !opts.toolsOnly && !opts.issuesOnly) {
1315
+ lines.push('Route timeline');
1316
+ const table = [['agent', 'model', 'turns', 'wall', 'LLM', 'tools', 'tool_ms', 'cache', 'WS', 'reused', 'session']];
1317
+ for (const g of report.routeGroups) {
1318
+ table.push([
1319
+ g.agent || '-',
1320
+ `${g.provider || '?'}:${g.model || '?'}`,
1321
+ g.turns,
1322
+ fmtSec((g.max_ts || 0) - (g.min_ts || 0)),
1323
+ fmtMs(g.llm_stream_ms),
1324
+ g.tool_calls,
1325
+ fmtMs(g.tool_ms),
1326
+ fmtPct((g.cache_ratio ?? 0) * 100),
1327
+ `${g.ws_delta}Δ/${g.ws_full}F`,
1328
+ `${g.reused_connection}/${g.transport_rows}`,
1329
+ shortId(g.session_id),
1330
+ ]);
1331
+ }
1332
+ lines.push(...padTable(table));
1333
+ lines.push('');
1334
+ }
1335
+
1336
+ if (!focused && !opts.toolsOnly && !opts.issuesOnly) {
1337
+ lines.push('Cache / transport');
1338
+ lines.push(`cache: ${fmtTok(report.cache.cached_tokens)} / ${fmtTok(report.cache.prompt_tokens)} (${fmtPct((report.cache.usage_cache_ratio ?? 0) * 100)})`);
1339
+ lines.push(`ws: delta=${report.cache.ws_delta}, full=${report.cache.ws_full}, previous_response_id=${report.cache.previous_response_id}, reused=${report.cache.reused_connection}/${report.cache.transport_count}`);
1340
+ if (report.cache.cache_key_hashes.length) lines.push(`cache keys: ${report.cache.cache_key_hashes.slice(0, 5).map((k) => `${k.key}×${k.count}`).join(', ')}`);
1341
+ if (report.cache.cache_breaks.length) {
1342
+ lines.push('cache breaks:');
1343
+ for (const b of report.cache.cache_breaks.slice(0, 10)) {
1344
+ lines.push(`- ${shortId(b.session_id)} it=${b.iteration ?? '-'} phase=${b.phase || '-'} reason=${b.reason || '-'} (${b.explanation || '-'}) ws=${b.ws_mode || '-'} prev=${b.request_has_previous_response_id} cache=${fmtPct((b.cache_ratio ?? 0) * 100)} prompt=${fmtTok(b.prompt_tokens)} out=${fmtTok(b.output_tokens)} body/frame=${b.body_input_items ?? '-'}/${b.frame_input_items ?? '-'}`);
1345
+ }
1346
+ }
1347
+ if (report.cache.service_tier_downgrades.length) {
1348
+ lines.push('service tier downgrades:');
1349
+ for (const d of report.cache.service_tier_downgrades.slice(0, 10)) lines.push(`- ${shortId(d.session_id)} it=${d.iteration ?? '-'} ${d.requested}->${d.response}`);
1350
+ }
1351
+ if (report.compact?.sessions?.length || report.compact?.recent_meta?.length) {
1352
+ pushCompactDiagnostics();
1353
+ }
1354
+ lines.push('');
1355
+ }
1356
+
1357
+ if ((opts.slowOnly || !focused) && !opts.cacheOnly && !opts.toolsOnly && !opts.issuesOnly && !opts.failuresOnly && !opts.tokensOnly) {
1358
+ lines.push('Slow turns / stage breakdown');
1359
+ const table = [['agent', 'it', 'active', 'headers', 'stream', 'tools', 'cache', 'ws', 'prompt', 'out', 'flags']];
1360
+ for (const t of report.stages.slowest_active.slice(0, Math.min(opts.limit, 12))) {
1361
+ table.push([
1362
+ t.agent || '-',
1363
+ t.turn_label || (t.iteration ?? '-'),
1364
+ fmtMs(t.approx_active_ms),
1365
+ fmtMs(t.headers_ms),
1366
+ fmtMs(t.stream_ms),
1367
+ `${fmtMs(t.tool_ms)}/${t.tool_calls}`,
1368
+ fmtPct((t.cache_ratio ?? 0) * 100),
1369
+ `${t.ws_mode || '-'}${t.reused_connection === true ? '+reuse' : ''}`,
1370
+ fmtTok(t.prompt_tokens),
1371
+ fmtTok(t.output_tokens),
1372
+ t.flags.join(',') || '-',
1373
+ ]);
1374
+ }
1375
+ lines.push(...padTable(table));
1376
+ if (report.stages.slowest_tools.some((t) => t.tool_ms > 0)) {
1377
+ lines.push('slow tool turns:');
1378
+ for (const t of report.stages.slowest_tools.slice(0, 5)) {
1379
+ const toolLabel = t.top_tools.map((x) => `${x.tool}×${x.count}/${fmtMs(x.ms)}`).join(', ') || '-';
1380
+ lines.push(`- ${t.agent || '-'} it=${t.turn_label || (t.iteration ?? '-')} tools=${fmtMs(t.tool_ms)} calls=${t.tool_calls}: ${toolLabel}`);
1381
+ }
1382
+ }
1383
+ lines.push('');
1384
+ }
1385
+
1386
+ if ((opts.tokensOnly || !focused) && !opts.cacheOnly && !opts.toolsOnly && !opts.issuesOnly && !opts.failuresOnly && !opts.slowOnly) {
1387
+ lines.push('Token amplification');
1388
+ const sessionTable = [['agent', 'turns', 'prompt first→last', 'max', 'Δprompt', 'out', 'uncached', 'cache', 'session']];
1389
+ for (const s of report.tokens.sessions.slice(0, Math.min(opts.limit, 8))) {
1390
+ sessionTable.push([
1391
+ s.agent || '-',
1392
+ s.turns,
1393
+ `${fmtTok(s.first_prompt)}→${fmtTok(s.last_prompt)}`,
1394
+ `${fmtTok(s.max_prompt)}@${s.max_prompt_it}`,
1395
+ fmtTok(s.prompt_growth),
1396
+ fmtTok(s.total_output + s.total_thinking),
1397
+ fmtTok(s.uncached_tokens),
1398
+ fmtPct((s.cache_ratio ?? 0) * 100),
1399
+ shortId(s.session_id),
1400
+ ]);
1401
+ }
1402
+ lines.push(...padTable(sessionTable));
1403
+ if (report.tokens.growth_turns.length) {
1404
+ lines.push('prompt growth spikes:');
1405
+ for (const t of report.tokens.growth_turns.slice(0, 8)) {
1406
+ const ratio = t.growth_per_output == null ? '-' : `${t.growth_per_output.toFixed(t.growth_per_output >= 10 ? 0 : 1)}x/out`;
1407
+ lines.push(`- ${t.agent || '-'} it=${t.turn_label} prompt=${fmtTok(t.prompt_tokens)} Δ=${fmtTok(t.prompt_delta)} out=${fmtTok(t.output_tokens + t.thinking_tokens)} ${ratio} cache=${fmtPct((t.cache_ratio ?? 0) * 100)}`);
1408
+ }
1409
+ }
1410
+ if (report.tokens.output_heavy_turns.length) {
1411
+ lines.push('large output turns:');
1412
+ for (const t of report.tokens.output_heavy_turns.slice(0, 5)) {
1413
+ lines.push(`- ${t.agent || '-'} it=${t.turn_label} out=${fmtTok(t.output_tokens)} think=${fmtTok(t.thinking_tokens)} prompt=${fmtTok(t.prompt_tokens)} cache=${fmtPct((t.cache_ratio ?? 0) * 100)}`);
1414
+ }
1415
+ }
1416
+ if (report.tokens.cache_miss_cost_turns.length) {
1417
+ lines.push('uncached prompt cost:');
1418
+ for (const t of report.tokens.cache_miss_cost_turns.slice(0, 5)) {
1419
+ lines.push(`- ${t.agent || '-'} it=${t.turn_label} uncached=${fmtTok(t.uncached_tokens)} prompt=${fmtTok(t.prompt_tokens)} cache=${fmtPct((t.cache_ratio ?? 0) * 100)}`);
1420
+ }
1421
+ }
1422
+ lines.push('');
1423
+ }
1424
+
1425
+ if ((opts.toolsOnly || opts.failuresOnly || !focused) && !opts.cacheOnly && !opts.issuesOnly && !opts.tokensOnly && !opts.slowOnly) {
1426
+ lines.push('Tool diagnostics');
1427
+ lines.push(`tool result kinds: ${fmtKindCounts(report.tools.result_kinds, 6)}`);
1428
+ const table = [['tool', 'count', 'ok/err', 'total', 'p50', 'p95', 'kinds', 'result']];
1429
+ for (const t of report.tools.by_name.slice(0, opts.limit)) {
1430
+ table.push([
1431
+ t.tool,
1432
+ t.count,
1433
+ `${t.ok}/${t.errors}`,
1434
+ fmtMs(t.total_ms),
1435
+ fmtMs(t.p50_ms),
1436
+ fmtMs(t.p95_ms),
1437
+ fmtKindCounts(t.result_kinds),
1438
+ `${Math.round(t.bytes / 1024)}KB/${t.lines}l`,
1439
+ ]);
1440
+ }
1441
+ lines.push(...padTable(table));
1442
+ if (report.tools.failures.length) {
1443
+ lines.push('tool failures:');
1444
+ for (const f of report.tools.failures.slice(0, 10)) {
1445
+ lines.push(`- ${f.agent || '-'} it=${f.iteration ?? '-'} ${f.tool || '-'} ${fmtMs(f.tool_ms)} category=${f.category || '-'} reason=${f.reason || 'trace에 상세 stderr/예외 미저장'} result=${Math.round(f.bytes / 1024)}KB/${f.lines}l: ${f.target}`);
1446
+ if (opts.failuresOnly && f.preview) {
1447
+ const preview = compactText(f.preview, 700);
1448
+ lines.push(` preview: ${preview}`);
1449
+ }
1450
+ }
1451
+ }
1452
+ if (report.tools.recent_successes.length) {
1453
+ lines.push('recent successful tools:');
1454
+ for (const s of report.tools.recent_successes.slice(0, 5)) {
1455
+ lines.push(`- ${s.agent || '-'} it=${s.iteration ?? '-'} ${s.tool || '-'} ${fmtMs(s.tool_ms)} kind=${s.result_kind}: ${s.target}`);
1456
+ }
1457
+ }
1458
+ if (report.tools.duplicates.length) {
1459
+ lines.push('tool churn / duplicates:');
1460
+ for (const d of report.tools.duplicates.slice(0, 10)) lines.push(`- ${d.tool} x${d.count} kinds=${fmtKindCounts(d.result_kinds)}: ${d.target}`);
1461
+ }
1462
+ if (report.tools.broad_results.length) {
1463
+ lines.push('broad/offloaded results:');
1464
+ for (const b of report.tools.broad_results.slice(0, 10)) lines.push(`- ${b.tool} ${Math.round(b.bytes / 1024)}KB/${b.lines}l: ${b.target}`);
1465
+ }
1466
+ if (report.tools.read_fragmentation.length) {
1467
+ lines.push('read fragmentation:');
1468
+ for (const f of report.tools.read_fragmentation.slice(0, 10)) lines.push(`- x${f.count} span=${f.line_span} lines: ${f.path}`);
1469
+ }
1470
+ if (report.tools.grep_sweeps?.length) {
1471
+ lines.push('grep sweeps:');
1472
+ for (const s of report.tools.grep_sweeps.slice(0, 8)) {
1473
+ const scope = s.paths?.map((p) => `${p.count}×${p.path}`).join(', ') || '-';
1474
+ lines.push(`- x${s.count}/${s.unique_queries} span=${fmtMs(s.span_ms)} scope=${scope}`);
1475
+ if (s.examples?.length) lines.push(` e.g. ${s.examples.join(' | ')}`);
1476
+ }
1477
+ }
1478
+ if (report.tools.sequential_tool_clusters?.length) {
1479
+ lines.push('sequential single-tool clusters:');
1480
+ for (const c of report.tools.sequential_tool_clusters.slice(0, 8)) {
1481
+ const toolSummary = c.tools.map((x) => `${x.tool}×${x.count}`).join(', ');
1482
+ lines.push(`- ${c.agent || '-'} it=${c.start_it ?? '-'}→${c.end_it ?? '-'} x${c.count} span=${fmtMs(c.span_ms)} tool_ms=${fmtMs(c.tool_ms)} errors=${c.errors}: ${toolSummary}`);
1483
+ if (c.examples?.length) lines.push(` e.g. ${c.examples.join(' | ')}`);
1484
+ }
1485
+ }
1486
+ lines.push(`missed parallelism heuristic: ${report.tools.missed_parallelism_heuristic.consecutive_single_tool_batches} close single-tool batches`);
1487
+ lines.push('');
1488
+ }
1489
+
1490
+ if (!opts.cacheOnly && !opts.toolsOnly) {
1491
+ lines.push('Issues');
1492
+ if (!report.issues.length) lines.push('- none detected by current heuristics');
1493
+ for (const issue of report.issues.slice(0, opts.limit)) lines.push(`- [${issue.severity}] ${issue.type}: ${issue.message}`);
1494
+ }
1495
+ return lines.join('\n');
1496
+ }
1497
+
1498
+ const tracePath = opts.trace ? resolve(opts.trace) : defaultTracePath();
1499
+ const failurePath = defaultToolFailurePath(tracePath);
1500
+ let rows = readRows(tracePath);
1501
+ if (opts.since != null) rows = rows.filter((r) => Number(r.ts || 0) >= opts.since);
1502
+ rows = filterAgent(rows, opts.agent);
1503
+
1504
+ let failureRows = readRows(failurePath);
1505
+ if (opts.since != null) failureRows = failureRows.filter((r) => Number(r.ts || 0) >= opts.since);
1506
+ failureRows = filterAgent(failureRows, opts.agent);
1507
+
1508
+ const bySession = groupBy(rows.filter((r) => sessionId(r)), sessionId);
1509
+ const metas = [...bySession.entries()].map(([sid, srows]) => ({ ...inferSessionMeta(srows), session_id: sid }))
1510
+ .filter((m) => m.max_ts != null)
1511
+ .sort((a, b) => Number(b.max_ts || 0) - Number(a.max_ts || 0));
1512
+
1513
+ const selectedIds = selectSessionIds(metas, opts.session);
1514
+ if (!selectedIds.length) {
1515
+ const fallback = { error: `No session matched ${opts.session}`, trace: tracePath, sessions_seen: metas.slice(0, 10).map((m) => ({ session_id: m.session_id, agent: m.agent, model: m.model, last: fmtTime(m.max_ts) })) };
1516
+ if (opts.json) console.log(JSON.stringify(fallback, null, 2));
1517
+ else {
1518
+ console.error(fallback.error);
1519
+ for (const s of fallback.sessions_seen) console.error(`- ${shortId(s.session_id)} ${s.agent || '-'} ${s.model || '-'} ${s.last}`);
1520
+ }
1521
+ process.exitCode = 1;
1522
+ } else {
1523
+ const report = buildReport(rows, selectedIds, failureRows);
1524
+ if (opts.json) console.log(JSON.stringify(report, null, 2));
1525
+ else console.log(renderText(report));
1526
+ }