mixdog 0.9.63 → 0.9.65

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 (408) hide show
  1. package/README.md +3 -0
  2. package/package.json +9 -2
  3. package/scripts/code-graph-description-contract.mjs +3 -3
  4. package/scripts/verify-release-assets.mjs +2 -2
  5. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +11 -14
  6. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +6 -6
  7. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -2
  8. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +1 -0
  9. package/src/runtime/agent/orchestrator/config.mjs +19 -124
  10. package/src/runtime/agent/orchestrator/context/collect.mjs +6 -6
  11. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +2 -2
  12. package/src/runtime/agent/orchestrator/internal-agents.mjs +2 -2
  13. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +1 -1
  14. package/src/runtime/agent/orchestrator/mcp/client.mjs +4 -4
  15. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +2 -2
  16. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +4 -4
  17. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +2 -2
  18. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +4 -4
  19. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +333 -0
  20. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +2 -2
  21. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +4 -4
  22. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +1 -1
  23. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +3 -277
  24. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +2 -2
  25. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +1 -1
  26. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +2 -2
  27. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +6 -6
  28. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +196 -0
  29. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +460 -0
  30. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +14 -593
  31. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +1 -1
  32. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +1 -1
  33. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +1 -1
  34. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +4 -4
  35. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +2 -2
  36. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +5 -280
  37. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +357 -0
  38. package/src/runtime/agent/orchestrator/providers/openai-ws-headers.mjs +216 -0
  39. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +2 -188
  40. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -2
  41. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -2
  42. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +1 -1
  43. package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +1 -1
  44. package/src/runtime/agent/orchestrator/providers/registry.mjs +1 -1
  45. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +2 -2
  46. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +1 -1
  47. package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +1 -1
  48. package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +3 -3
  49. package/src/runtime/agent/orchestrator/session/compact/budget.mjs +2 -2
  50. package/src/runtime/agent/orchestrator/session/compact/file-reattach.mjs +2 -2
  51. package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +1 -1
  52. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +1 -1
  53. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +2 -2
  54. package/src/runtime/agent/orchestrator/session/context-utils.mjs +4 -4
  55. package/src/runtime/agent/orchestrator/session/loop/deferred-call-through.mjs +1 -1
  56. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +17 -0
  57. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +1 -1
  58. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +18 -26
  59. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +1 -1
  60. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +3 -3
  61. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +1 -0
  62. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +1 -1
  63. package/src/runtime/agent/orchestrator/session/manager/delivered-completions.mjs +1 -1
  64. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +1 -1
  65. package/src/runtime/agent/orchestrator/session/store/live-state.mjs +49 -0
  66. package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +49 -0
  67. package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +260 -0
  68. package/src/runtime/agent/orchestrator/session/store/serialize.mjs +72 -0
  69. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +163 -0
  70. package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +1 -1
  71. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +1 -1
  72. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +4 -1
  73. package/src/runtime/agent/orchestrator/session/store.mjs +9 -561
  74. package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +1 -1
  75. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  76. package/src/runtime/agent/orchestrator/stall-policy.mjs +11 -11
  77. package/src/runtime/agent/orchestrator/tools/bash-policy-scan.mjs +1 -1
  78. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
  79. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
  80. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +2 -2
  81. package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +1 -1
  82. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +2 -154
  83. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +1 -1
  84. package/src/runtime/agent/orchestrator/tools/builtin/cwd-utils.mjs +1 -1
  85. package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +1 -1
  86. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
  87. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -1
  88. package/src/runtime/agent/orchestrator/tools/builtin/path-locks.mjs +1 -1
  89. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -2
  91. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +2 -3
  92. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +4 -4
  93. package/src/runtime/agent/orchestrator/tools/builtin/read-open.mjs +1 -1
  94. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +1 -1
  95. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +2 -2
  96. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -5
  97. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +1 -1
  98. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +2 -2
  99. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +2 -2
  100. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +519 -0
  101. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +5 -449
  102. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +1 -1
  103. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +212 -0
  104. package/src/runtime/agent/orchestrator/tools/builtin.mjs +1 -1
  105. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +1 -7
  106. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +9 -36
  107. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +15 -3
  108. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +429 -0
  109. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +2 -395
  110. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +2 -2
  111. package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +1 -1
  112. package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +1 -1
  113. package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +1 -1
  114. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +3 -3
  115. package/src/runtime/agent/orchestrator/tools/mutation-planner.mjs +5 -5
  116. package/src/runtime/agent/orchestrator/tools/next-call-utils.mjs +1 -1
  117. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +1 -1
  118. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +7 -7
  119. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +72 -9
  120. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +4 -4
  121. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +1 -1
  122. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +1 -1
  123. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +4 -4
  124. package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
  125. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +3 -3
  126. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +2 -444
  127. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +505 -0
  128. package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +2 -2
  129. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +1 -1
  130. package/src/runtime/channels/lib/backend-dispatch.mjs +20 -5
  131. package/src/runtime/channels/lib/scheduler.mjs +1 -1
  132. package/src/runtime/channels/lib/telegram-format.mjs +1 -1
  133. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +4 -4
  134. package/src/runtime/channels/tool-defs.mjs +2 -3
  135. package/src/runtime/memory/index.mjs +1 -5
  136. package/src/runtime/memory/lib/core-memory-candidates.mjs +357 -0
  137. package/src/runtime/memory/lib/core-memory-store.mjs +7 -357
  138. package/src/runtime/memory/lib/cycle-signatures.mjs +1 -1
  139. package/src/runtime/memory/lib/embedding-model-config.mjs +1 -1
  140. package/src/runtime/memory/lib/embedding-provider.mjs +1 -5
  141. package/src/runtime/memory/lib/embedding-worker.mjs +1 -5
  142. package/src/runtime/memory/lib/ko-morph.mjs +2 -2
  143. package/src/runtime/memory/lib/memory-config-flags.mjs +2 -5
  144. package/src/runtime/memory/lib/memory-cycle-requests.mjs +3 -7
  145. package/src/runtime/memory/lib/memory-cycle1.mjs +4 -8
  146. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +1 -1
  147. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -1
  148. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +2 -5
  149. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -5
  150. package/src/runtime/memory/lib/memory-embed.mjs +2 -6
  151. package/src/runtime/memory/lib/memory-log.mjs +8 -0
  152. package/src/runtime/memory/lib/memory-ops-policy.mjs +4 -8
  153. package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +1 -1
  154. package/src/runtime/memory/lib/memory-recall-store.mjs +2 -193
  155. package/src/runtime/memory/lib/memory-score.mjs +1 -1
  156. package/src/runtime/memory/lib/memory-text-utils.mjs +2 -2
  157. package/src/runtime/memory/lib/memory.mjs +2 -6
  158. package/src/runtime/memory/lib/pg/adapter.mjs +2 -6
  159. package/src/runtime/memory/lib/pg/process.mjs +1 -5
  160. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -5
  161. package/src/runtime/memory/lib/recall-format.mjs +2 -2
  162. package/src/runtime/memory/lib/recall-scoring.mjs +195 -0
  163. package/src/runtime/memory/lib/runtime-fetcher.mjs +1 -5
  164. package/src/runtime/memory/lib/trace-store.mjs +2 -6
  165. package/src/runtime/search/lib/http-fetch.mjs +6 -6
  166. package/src/runtime/search/lib/web-tools.mjs +2 -2
  167. package/src/runtime/shared/buffered-appender.mjs +1 -1
  168. package/src/runtime/shared/child-guardian.mjs +1 -1
  169. package/src/runtime/shared/child-spawn-gate.mjs +1 -1
  170. package/src/runtime/shared/config.mjs +4 -4
  171. package/src/runtime/shared/launcher-control.mjs +9 -10
  172. package/src/runtime/shared/llm/index.mjs +5 -20
  173. package/src/runtime/shared/markdown-frontmatter.mjs +1 -1
  174. package/src/runtime/shared/memory-snapshot.mjs +4 -4
  175. package/src/runtime/shared/pristine-execution.mjs +1 -1
  176. package/src/runtime/shared/process-lifecycle.mjs +0 -1
  177. package/src/runtime/shared/process-shutdown.mjs +1 -1
  178. package/src/runtime/shared/resource-admission.mjs +2 -2
  179. package/src/runtime/shared/schedule-session-run.mjs +1 -1
  180. package/src/runtime/shared/schedules-db.mjs +1 -1
  181. package/src/runtime/shared/service-discovery.mjs +2 -2
  182. package/src/runtime/shared/staged-update.mjs +6 -6
  183. package/src/runtime/shared/tool-execution-contract.mjs +1 -1
  184. package/src/runtime/shared/tool-primitives.mjs +1 -1
  185. package/src/runtime/shared/tool-surface.mjs +2 -2
  186. package/src/runtime/shared/turn-snapshot.mjs +184 -0
  187. package/src/runtime/shared/update-checker.mjs +1 -1
  188. package/src/runtime/shared/user-cwd.mjs +3 -3
  189. package/src/runtime/shared/user-data-guard.mjs +4 -4
  190. package/src/runtime/shared/workspace-router.mjs +5 -5
  191. package/src/session-runtime/config-helpers.mjs +2 -3
  192. package/src/session-runtime/config-lifecycle.mjs +1 -1
  193. package/src/session-runtime/hitch-profile.mjs +1 -1
  194. package/src/session-runtime/lifecycle-api.mjs +6 -4
  195. package/src/session-runtime/model-recency.mjs +5 -5
  196. package/src/session-runtime/plugin-mcp.mjs +1 -1
  197. package/src/session-runtime/provider-request-snapshot.mjs +338 -0
  198. package/src/session-runtime/runtime-core.mjs +4 -0
  199. package/src/session-runtime/session-text.mjs +4 -4
  200. package/src/session-runtime/session-turn-api.mjs +7 -0
  201. package/src/session-runtime/tool-catalog-schema.mjs +197 -0
  202. package/src/session-runtime/tool-catalog.mjs +6 -483
  203. package/src/session-runtime/tool-defs.mjs +1 -1
  204. package/src/standalone/agent-task-status.mjs +1 -1
  205. package/src/standalone/agent-tool/helpers.mjs +2 -2
  206. package/src/standalone/channel-admin.mjs +4 -4
  207. package/src/standalone/explore-tool.mjs +3 -3
  208. package/src/standalone/hook-bus/config.mjs +1 -1
  209. package/src/standalone/hook-bus/constants.mjs +1 -1
  210. package/src/standalone/hook-bus/rules.mjs +1 -1
  211. package/src/standalone/projects.mjs +1 -1
  212. package/src/standalone/provider-admin.mjs +2 -2
  213. package/src/tui/App.jsx +1 -1
  214. package/src/tui/app/app-format.mjs +2 -2
  215. package/src/tui/app/clipboard.mjs +3 -3
  216. package/src/tui/app/input-parsers.mjs +1 -1
  217. package/src/tui/app/text-layout.mjs +1 -1
  218. package/src/tui/app/transcript-row-estimate.mjs +360 -0
  219. package/src/tui/app/transcript-window.mjs +5 -347
  220. package/src/tui/app/use-mouse-input.mjs +1 -1
  221. package/src/tui/app/use-prompt-handlers.mjs +1 -1
  222. package/src/tui/components/tool-execution/surface-detail.mjs +9 -9
  223. package/src/tui/components/tool-execution/text-format.mjs +2 -2
  224. package/src/tui/components/tool-output-format.mjs +7 -7
  225. package/src/tui/display-width.mjs +4 -4
  226. package/src/tui/dist/index.mjs +187 -99
  227. package/src/tui/engine/agent-envelope.mjs +5 -5
  228. package/src/tui/engine/live-share.mjs +100 -0
  229. package/src/tui/engine/notification-plan.mjs +1 -1
  230. package/src/tui/engine/queue-helpers.mjs +3 -3
  231. package/src/tui/engine/render-timing.mjs +4 -5
  232. package/src/tui/engine/session-api-ext.mjs +6 -3
  233. package/src/tui/engine/tool-result-status.mjs +5 -5
  234. package/src/tui/engine/tool-result-text.mjs +1 -1
  235. package/src/tui/engine/transcript-spill.mjs +585 -0
  236. package/src/tui/engine/tui-steering-persist.mjs +1 -1
  237. package/src/tui/engine.mjs +29 -474
  238. package/src/tui/lib/voice-setup.mjs +1 -1
  239. package/src/tui/markdown/format-token.mjs +1 -1
  240. package/src/tui/markdown/render-ansi.mjs +1 -1
  241. package/src/tui/markdown/table-layout.mjs +4 -4
  242. package/src/tui/paste-attachments.mjs +1 -1
  243. package/src/tui/prompt-history-store.mjs +3 -3
  244. package/src/tui/statusline-ansi-bridge.mjs +2 -2
  245. package/src/tui/theme.mjs +1 -1
  246. package/src/tui/transcript-tool-failures.mjs +2 -2
  247. package/src/ui/ansi.mjs +0 -9
  248. package/src/ui/statusline-format.mjs +2 -4
  249. package/scripts/_bench-cwc.json +0 -20
  250. package/scripts/_jitter-fuzz.jsx +0 -42
  251. package/scripts/_jitter-fuzz2.jsx +0 -30
  252. package/scripts/_jitter-probe.jsx +0 -35
  253. package/scripts/_jp2.jsx +0 -16
  254. package/scripts/_smoke_wd.mjs +0 -7
  255. package/scripts/abort-recovery-test.mjs +0 -175
  256. package/scripts/agent-dispatch-abort-compose-test.mjs +0 -31
  257. package/scripts/agent-live-arg-guard-smoke.mjs +0 -20
  258. package/scripts/agent-live-path-suffix-smoke.mjs +0 -34
  259. package/scripts/agent-live-toolcall-args-smoke.mjs +0 -45
  260. package/scripts/agent-loop-policy-test.mjs +0 -37
  261. package/scripts/agent-model-liveness-test.mjs +0 -763
  262. package/scripts/agent-parallel-smoke.mjs +0 -435
  263. package/scripts/agent-route-batch-test.mjs +0 -40
  264. package/scripts/agent-tag-reuse-smoke.mjs +0 -441
  265. package/scripts/agent-terminal-reap-test.mjs +0 -252
  266. package/scripts/agent-trace-io-test.mjs +0 -133
  267. package/scripts/ansi-color-capability-test.mjs +0 -90
  268. package/scripts/anthropic-admission-retry-integration-test.mjs +0 -119
  269. package/scripts/anthropic-maxtokens-test.mjs +0 -119
  270. package/scripts/anthropic-oauth-refresh-race-test.mjs +0 -397
  271. package/scripts/anthropic-transport-policy-test.mjs +0 -726
  272. package/scripts/apply-patch-edit-smoke.mjs +0 -71
  273. package/scripts/arg-guard-test.mjs +0 -93
  274. package/scripts/async-notify-settlement-test.mjs +0 -99
  275. package/scripts/atomic-lock-tryonce-test.mjs +0 -125
  276. package/scripts/background-task-meta-smoke.mjs +0 -38
  277. package/scripts/bench-run.mjs +0 -508
  278. package/scripts/boot-smoke.mjs +0 -137
  279. package/scripts/build-runtime-windows.ps1 +0 -242
  280. package/scripts/channel-daemon-smoke.mjs +0 -1103
  281. package/scripts/code-graph-aggregate-cwd-test.mjs +0 -158
  282. package/scripts/code-graph-disk-hit-test.mjs +0 -290
  283. package/scripts/code-graph-dispatch-test.mjs +0 -96
  284. package/scripts/code-graph-root-federation-test.mjs +0 -273
  285. package/scripts/compact-active-turn-test.mjs +0 -68
  286. package/scripts/compact-file-reattach-test.mjs +0 -88
  287. package/scripts/compact-pressure-test.mjs +0 -788
  288. package/scripts/compact-prior-context-flatten-test.mjs +0 -252
  289. package/scripts/compact-recall-digest-test.mjs +0 -57
  290. package/scripts/compact-smoke.mjs +0 -1017
  291. package/scripts/compact-trigger-migration-smoke.mjs +0 -261
  292. package/scripts/compacted-placeholder-scrub-test.mjs +0 -63
  293. package/scripts/context-mcp-metering-test.mjs +0 -1406
  294. package/scripts/debounced-skills-async-save-test.mjs +0 -57
  295. package/scripts/deferred-tool-loading-test.mjs +0 -250
  296. package/scripts/desktop-session-bridge-test.mjs +0 -1013
  297. package/scripts/dispatch-persist-recovery-test.mjs +0 -141
  298. package/scripts/embedding-worker-exit-test.mjs +0 -76
  299. package/scripts/execution-completion-dedup-test.mjs +0 -205
  300. package/scripts/execution-pending-resume-kick-test.mjs +0 -151
  301. package/scripts/execution-resume-esc-integration-test.mjs +0 -176
  302. package/scripts/explore-bench-tmp.mjs +0 -36
  303. package/scripts/explore-bench.mjs +0 -248
  304. package/scripts/explore-prompt-policy-test.mjs +0 -256
  305. package/scripts/explore-timeout-cancel-test.mjs +0 -345
  306. package/scripts/find-fuzzy-hidden-test.mjs +0 -267
  307. package/scripts/forwarder-rebind-tail-test.mjs +0 -67
  308. package/scripts/freevar-smoke.mjs +0 -98
  309. package/scripts/gemini-provider-test.mjs +0 -1448
  310. package/scripts/generate-oc-icons.mjs +0 -11
  311. package/scripts/grok-oauth-refresh-race-test.mjs +0 -273
  312. package/scripts/headless-pristine-execution-test.mjs +0 -614
  313. package/scripts/hook-bus-test.mjs +0 -552
  314. package/scripts/ingest-pure-conversation-smoke.mjs +0 -175
  315. package/scripts/internal-comms-bench-test.mjs +0 -226
  316. package/scripts/internal-comms-bench.mjs +0 -853
  317. package/scripts/internal-comms-smoke.mjs +0 -277
  318. package/scripts/internal-tools-normalization-test.mjs +0 -62
  319. package/scripts/interrupted-turn-history-test.mjs +0 -399
  320. package/scripts/lead-workflow-smoke.mjs +0 -592
  321. package/scripts/lifecycle-api-test.mjs +0 -137
  322. package/scripts/live-share-test.mjs +0 -148
  323. package/scripts/live-worker-smoke.mjs +0 -333
  324. package/scripts/log-writer-guard-smoke.mjs +0 -131
  325. package/scripts/maintenance-default-routes-test.mjs +0 -164
  326. package/scripts/max-output-recovery-persist-test.mjs +0 -81
  327. package/scripts/max-output-recovery-test.mjs +0 -368
  328. package/scripts/mcp-client-normalization-test.mjs +0 -45
  329. package/scripts/mcp-grace-deferred-test.mjs +0 -225
  330. package/scripts/memory-core-input-test.mjs +0 -167
  331. package/scripts/memory-cycle-routing-test.mjs +0 -111
  332. package/scripts/memory-meta-concurrency-test.mjs +0 -20
  333. package/scripts/memory-pg-recovery-test.mjs +0 -59
  334. package/scripts/memory-rule-contract-test.mjs +0 -93
  335. package/scripts/model-list-sanitize-test.mjs +0 -37
  336. package/scripts/mouse-tracking-restore-smoke.mjs +0 -107
  337. package/scripts/native-edit-wire-test.mjs +0 -152
  338. package/scripts/notify-completion-mirror-test.mjs +0 -73
  339. package/scripts/openai-oauth-refresh-race-test.mjs +0 -120
  340. package/scripts/openai-oauth-ws-1006-retry-test.mjs +0 -954
  341. package/scripts/openai-ws-early-settle-test.mjs +0 -216
  342. package/scripts/output-style-bench.mjs +0 -292
  343. package/scripts/output-style-smoke.mjs +0 -145
  344. package/scripts/parent-abort-link-test.mjs +0 -90
  345. package/scripts/patch-binary-cache-test.mjs +0 -181
  346. package/scripts/path-suffix-test.mjs +0 -57
  347. package/scripts/pending-completion-drop-test.mjs +0 -239
  348. package/scripts/pending-messages-lock-nonblocking-test.mjs +0 -62
  349. package/scripts/pretool-ask-runtime-test.mjs +0 -54
  350. package/scripts/process-lifecycle-test.mjs +0 -461
  351. package/scripts/prompt-immediate-render-test.mjs +0 -89
  352. package/scripts/prompt-input-parity-test.mjs +0 -145
  353. package/scripts/provider-admission-scheduler-test.mjs +0 -681
  354. package/scripts/provider-contract-test.mjs +0 -632
  355. package/scripts/provider-stream-stall-test.mjs +0 -276
  356. package/scripts/provider-toolcall-test.mjs +0 -3063
  357. package/scripts/reactive-compact-persist-smoke.mjs +0 -203
  358. package/scripts/recall-bench.mjs +0 -359
  359. package/scripts/resource-admission-test.mjs +0 -792
  360. package/scripts/result-classification-test.mjs +0 -75
  361. package/scripts/rg-runner-test.mjs +0 -240
  362. package/scripts/routing-corpus-test.mjs +0 -349
  363. package/scripts/sanitize-tool-pairs-test.mjs +0 -260
  364. package/scripts/session-bench-cache-break-test.mjs +0 -102
  365. package/scripts/session-bench.mjs +0 -1708
  366. package/scripts/session-context-bench.mjs +0 -344
  367. package/scripts/session-heartbeat-lifecycle-test.mjs +0 -68
  368. package/scripts/session-ingest-compaction-smoke.mjs +0 -279
  369. package/scripts/session-ingest-smoke.mjs +0 -177
  370. package/scripts/session-orphan-sweep-test.mjs +0 -109
  371. package/scripts/set-effort-config-test.mjs +0 -41
  372. package/scripts/shell-failure-diagnostics-test.mjs +0 -280
  373. package/scripts/shell-hardening-test.mjs +0 -209
  374. package/scripts/shell-jobs-windows-hide-test.mjs +0 -164
  375. package/scripts/ship-mode-test.mjs +0 -98
  376. package/scripts/smoke-loop-failure-summary-test.mjs +0 -38
  377. package/scripts/smoke-loop-failure-summary.mjs +0 -16
  378. package/scripts/smoke-loop-report.mjs +0 -221
  379. package/scripts/smoke-loop.mjs +0 -202
  380. package/scripts/smoke-runtime-negative.ps1 +0 -106
  381. package/scripts/statusline-quota-hysteresis-test.mjs +0 -62
  382. package/scripts/steering-drain-buckets-test.mjs +0 -467
  383. package/scripts/steering-fold-provenance-test.mjs +0 -71
  384. package/scripts/streaming-tail-window-test.mjs +0 -175
  385. package/scripts/submit-commandbusy-race-test.mjs +0 -257
  386. package/scripts/task-bench.mjs +0 -205
  387. package/scripts/terminal-bench-isolation-guards-test.mjs +0 -102
  388. package/scripts/termio-input-smoke.mjs +0 -199
  389. package/scripts/tool-result-hook-test.mjs +0 -48
  390. package/scripts/tool-smoke.mjs +0 -2453
  391. package/scripts/tool-tui-presentation-test.mjs +0 -286
  392. package/scripts/toolcall-args-test.mjs +0 -239
  393. package/scripts/tui-ambiguous-width-test.mjs +0 -113
  394. package/scripts/tui-background-failure-smoke.mjs +0 -73
  395. package/scripts/tui-perf-run.ps1 +0 -26
  396. package/scripts/tui-render-smoke.mjs +0 -90
  397. package/scripts/tui-runtime-load-bench-entry.jsx +0 -608
  398. package/scripts/tui-runtime-load-bench.mjs +0 -50
  399. package/scripts/tui-store-frame-batch-test.mjs +0 -99
  400. package/scripts/tui-transcript-jitter-harness-entry.jsx +0 -383
  401. package/scripts/tui-transcript-perf-test.mjs +0 -934
  402. package/scripts/usage-metrics-epoch-smoke.mjs +0 -114
  403. package/scripts/verify-release-assets-test.mjs +0 -281
  404. package/scripts/web-fetch-routing-test.mjs +0 -158
  405. package/scripts/webhook-smoke.mjs +0 -201
  406. package/scripts/windows-hide-spawn-options-test.mjs +0 -19
  407. package/scripts/worker-notify-rejection-test.mjs +0 -51
  408. package/scripts/write-backpressure-test.mjs +0 -147
@@ -1,1708 +0,0 @@
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
- if (process.env.MIXDOG_AGENT_TRACE_PATH) {
56
- return resolve(process.env.MIXDOG_AGENT_TRACE_PATH);
57
- }
58
- const data = process.env.MIXDOG_DATA_DIR || resolvePluginData() || resolve(homedir(), '.mixdog', 'data');
59
- return resolve(data, 'history', 'agent-trace.jsonl');
60
- }
61
-
62
- function defaultToolFailurePath(tracePath = null) {
63
- if (process.env.MIXDOG_TOOL_FAILURE_LOG_PATH) return process.env.MIXDOG_TOOL_FAILURE_LOG_PATH;
64
- const base = tracePath ? dirname(resolve(tracePath)) : resolve(process.env.MIXDOG_DATA_DIR || resolvePluginData() || resolve(homedir(), '.mixdog', 'data'), 'history');
65
- return resolve(base, 'tool-failures.jsonl');
66
- }
67
-
68
- function readRows(path) {
69
- if (!existsSync(path)) return [];
70
- const rows = [];
71
- const lines = readFileSync(path, 'utf8').split(/\r?\n/);
72
- for (let i = 0; i < lines.length; i += 1) {
73
- const line = lines[i];
74
- if (!line) continue;
75
- try {
76
- const row = JSON.parse(line);
77
- rows.push({ ...row, _line: i + 1 });
78
- } catch {
79
- // Keep JSONL parsing best-effort; a partially-written tail must not break diagnostics.
80
- }
81
- }
82
- return rows;
83
- }
84
-
85
- function payload(row) {
86
- return row?.payload && typeof row.payload === 'object' ? row.payload : {};
87
- }
88
-
89
- function field(row, name) {
90
- if (row && row[name] != null) return row[name];
91
- const p = payload(row);
92
- return p[name] != null ? p[name] : null;
93
- }
94
-
95
- function num(row, name) {
96
- const n = Number(field(row, name));
97
- return Number.isFinite(n) ? n : null;
98
- }
99
-
100
- function sessionId(row) {
101
- return String(row?.session_id || row?.sessionId || field(row, 'session_id') || '');
102
- }
103
-
104
- function baseSessionId(id) {
105
- return String(id || '').replace(/:compact$/, '');
106
- }
107
-
108
- function isCompactSessionId(id) {
109
- return String(id || '').endsWith(':compact');
110
- }
111
-
112
- function stableStringify(value) {
113
- if (value === null || value === undefined) return JSON.stringify(value);
114
- if (typeof value !== 'object') return JSON.stringify(value);
115
- if (Array.isArray(value)) return '[' + value.map(stableStringify).join(',') + ']';
116
- const keys = Object.keys(value).sort();
117
- return '{' + keys.map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(',') + '}';
118
- }
119
-
120
- function hashValue(value) {
121
- try { return createHash('sha256').update(stableStringify(value)).digest('hex').slice(0, 16); }
122
- catch { return null; }
123
- }
124
-
125
- function toolArgs(row) {
126
- return field(row, 'tool_args_summary') || field(row, 'tool_args') || null;
127
- }
128
-
129
- function toolArgsHash(row) {
130
- const args = toolArgs(row);
131
- if (!args || typeof args !== 'object' || Object.keys(args).length === 0) return null;
132
- return field(row, 'tool_args_hash') || hashValue(args);
133
- }
134
-
135
- const READONLY_TOOL_NAMES = new Set(['read', 'grep', 'glob', 'list', 'find', 'code_graph', 'recall', 'explore', 'search', 'web_fetch']);
136
- const READONLY_STALL_MIN_RUN = 8;
137
- const READONLY_ROLE_AGENTS = new Set(['reviewer', 'explore', 'explorer', 'debugger']);
138
- const IDENTICAL_CALL_MIN_COUNT = 3;
139
-
140
- function fmtMs(ms) {
141
- const n = Number(ms);
142
- if (!Number.isFinite(n)) return '-';
143
- if (n >= 1000) return `${(n / 1000).toFixed(n >= 10_000 ? 0 : 1)}s`;
144
- return `${Math.round(n)}ms`;
145
- }
146
-
147
- function fmtSec(ms) {
148
- const n = Number(ms);
149
- if (!Number.isFinite(n)) return '-';
150
- return `${(n / 1000).toFixed(n >= 10_000 ? 0 : 1)}s`;
151
- }
152
-
153
- function fmtTok(n) {
154
- const v = Number(n);
155
- if (!Number.isFinite(v)) return '-';
156
- if (v >= 1_000_000) return `${(v / 1_000_000).toFixed(2)}M`;
157
- if (v >= 1000) return `${(v / 1000).toFixed(1)}k`;
158
- return String(Math.round(v));
159
- }
160
-
161
- function fmtPct(n) {
162
- const v = Number(n);
163
- return Number.isFinite(v) ? `${Math.round(v)}%` : '-';
164
- }
165
-
166
- function fmtTime(ts) {
167
- const n = Number(ts);
168
- if (!Number.isFinite(n) || n <= 0) return '-';
169
- return new Date(n).toISOString().replace('T', ' ').replace(/\.\d+Z$/, 'Z');
170
- }
171
-
172
- function compactText(value, max = 140) {
173
- const s = String(value || '').replace(/\s+/g, ' ').trim();
174
- return s.length > max ? `${s.slice(0, Math.max(0, max - 1))}…` : s;
175
- }
176
-
177
- function percentile(values, p) {
178
- const arr = values.filter((v) => Number.isFinite(v)).sort((a, b) => a - b);
179
- if (!arr.length) return null;
180
- const idx = Math.min(arr.length - 1, Math.max(0, Math.ceil((p / 100) * arr.length) - 1));
181
- return arr[idx];
182
- }
183
-
184
- function sum(values) {
185
- return values.reduce((s, v) => s + (Number.isFinite(Number(v)) ? Number(v) : 0), 0);
186
- }
187
-
188
- function cacheDenom(row) {
189
- const prompt = num(row, 'prompt_tokens');
190
- const input = num(row, 'input_tokens');
191
- return prompt || input || 0;
192
- }
193
-
194
- function cacheRatioFromUsage(rows) {
195
- let cached = 0;
196
- let total = 0;
197
- for (const row of rows) {
198
- cached += num(row, 'cached_tokens') || 0;
199
- total += cacheDenom(row);
200
- }
201
- return total > 0 ? cached / total : null;
202
- }
203
-
204
- function cacheBreakExplanation(reason) {
205
- const r = String(reason || '');
206
- if (r === 'no_anchor') return 'delta \uAE30\uC900\uC810/previous response \uC5C6\uC74C';
207
- if (r === 'input_prefix_mismatch') return '\uC694\uCCAD prefix\uAC00 \uC774\uC804 turn\uACFC \uB2EC\uB77C\uC9D0';
208
- if (r.startsWith('response_output_mismatch')) return '\uC774\uC804 \uC751\uB2F5 output \uCCB4\uC778\uC774 \uAE30\uB300\uAC12\uACFC \uB2E4\uB984';
209
- if (r === 'cache_key_changed') return 'cache key \uBCC0\uACBD';
210
- return r ? '\uC6D0\uC778 \uBBF8\uBD84\uB958' : '\uC6D0\uC778 \uAE30\uB85D \uC5C6\uC74C';
211
- }
212
-
213
- function cacheBreakTransitionTag(row) {
214
- const transition = field(row, 'intentional_transition');
215
- return typeof transition === 'string' && transition ? transition : null;
216
- }
217
-
218
- function cacheBreakIntentionalTransition(row) {
219
- const transition = cacheBreakTransitionTag(row);
220
- const reason = field(row, 'reason') || field(row, 'chain_delta_reason') || null;
221
- if (transition === 'automatic_compaction' && reason === 'input_prefix_mismatch') return transition;
222
- if (transition === 'transcript_rebuild' && reason === 'input_prefix_mismatch') return transition;
223
- if (transition === 'explorer_hard_cap_final_tool_choice_none'
224
- && reason === 'request_properties_changed'
225
- && field(row, 'request_tool_choice') === 'none') return transition;
226
- return null;
227
- }
228
-
229
- function classifyCacheBreakPhase(row, usageRows, transportRows) {
230
- const reason = field(row, 'reason') || field(row, 'chain_delta_reason') || field(row, 'payload')?.reason || null;
231
- const sid = sessionId(row);
232
- const intentionalTransition = cacheBreakIntentionalTransition(row);
233
- if (intentionalTransition) return `intentional_${intentionalTransition}`;
234
- const ts = Number(row.ts || 0);
235
- const priorUsage = usageRows.filter((r) => sessionId(r) === sid && Number(r.ts || 0) < ts)
236
- .sort((a, b) => Number(b.ts || 0) - Number(a.ts || 0));
237
- const priorTransport = transportRows.filter((r) => sessionId(r) === sid && Number(r.ts || 0) < ts)
238
- .sort((a, b) => Number(b.ts || 0) - Number(a.ts || 0));
239
- const priorCalls = priorUsage.length + priorTransport.length;
240
- if (String(reason || '') === 'no_anchor') {
241
- if (priorCalls === 0) return 'cold_start';
242
- return 'mid_chain_reset';
243
- }
244
- if (priorCalls === 0) return 'first_call_mismatch';
245
- return 'mid_chain_break';
246
- }
247
-
248
- function shortId(id) {
249
- const s = String(id || '');
250
- if (s.length <= 18) return s;
251
- return `${s.slice(0, 10)}…${s.slice(-6)}`;
252
- }
253
-
254
- function groupBy(rows, keyFn) {
255
- const map = new Map();
256
- for (const row of rows) {
257
- const key = keyFn(row);
258
- if (!map.has(key)) map.set(key, []);
259
- map.get(key).push(row);
260
- }
261
- return map;
262
- }
263
-
264
- function padTable(rows) {
265
- if (!rows.length) return [];
266
- const widths = [];
267
- for (const row of rows) {
268
- row.forEach((cell, i) => { widths[i] = Math.max(widths[i] || 0, String(cell ?? '').length); });
269
- }
270
- return rows.map((row) => row.map((cell, i) => String(cell ?? '').padEnd(widths[i])).join(' '));
271
- }
272
-
273
- function inferSessionMeta(rows) {
274
- const sorted = [...rows].sort((a, b) => Number(a.ts || 0) - Number(b.ts || 0));
275
- const preset = sorted.find((r) => r.kind === 'preset_assign');
276
- const usage = [...sorted].reverse().find((r) => r.kind === 'usage_raw' || r.kind === 'usage');
277
- const tool = sorted.find((r) => r.kind === 'tool');
278
- const context = sorted.find((r) => r.kind === 'context');
279
- const last = sorted[sorted.length - 1] || {};
280
- const tsValues = sorted.map((r) => Number(r.ts || 0)).filter((n) => n > 0);
281
- return {
282
- session_id: sessionId(last),
283
- parent_session_id: field(preset, 'parent_session_id') || field(preset, 'parentSessionId') || null,
284
- agent: field(preset, 'agent') || field(tool, 'agent') || field(usage, 'sourceName') || field(last, 'sourceName') || null,
285
- preset: field(preset, 'preset_name') || field(last, 'preset') || null,
286
- provider: field(preset, 'provider') || field(usage, 'provider') || field(context, 'provider') || null,
287
- model: field(preset, 'model') || field(usage, 'model') || field(context, 'model') || null,
288
- min_ts: tsValues.length ? Math.min(...tsValues) : null,
289
- max_ts: tsValues.length ? Math.max(...tsValues) : null,
290
- rows: sorted.length,
291
- };
292
- }
293
-
294
- function chooseCurrentSession(sessionMetas) {
295
- const sorted = [...sessionMetas].sort((a, b) => Number(b.max_ts || 0) - Number(a.max_ts || 0));
296
- // "current" should mean the freshest active route, not necessarily the
297
- // oldest root workflow that still has background child events. Prefer recent
298
- // workflow-lead/main rows, then fall back to newest activity.
299
- const main = sorted.find((m) => {
300
- const agent = String(m.agent || '').toLowerCase();
301
- const preset = String(m.preset || '').toLowerCase();
302
- return agent === 'main' || agent === 'lead' || preset.includes('workflow lead');
303
- });
304
- return main || sorted.find((m) => !m.parent_session_id && m.model) || sorted[0] || null;
305
- }
306
-
307
- function sessionMatches(id, query) {
308
- const q = String(query || '').trim();
309
- if (!q || q === 'current') return false;
310
- return id === q || id.startsWith(q) || shortId(id).startsWith(q);
311
- }
312
-
313
- function selectSessionIds(sessionMetas, query) {
314
- if (String(query || '').trim().toLowerCase() === 'all') {
315
- return sessionMetas.map((m) => m.session_id);
316
- }
317
- let selected;
318
- if (!query || query === 'current') selected = chooseCurrentSession(sessionMetas);
319
- else selected = sessionMetas.find((m) => sessionMatches(m.session_id, query));
320
- if (!selected) return [];
321
- const root = selected.parent_session_id
322
- ? sessionMetas.find((m) => m.session_id === selected.parent_session_id) || selected
323
- : selected;
324
- const ids = new Set([root.session_id]);
325
- for (const meta of sessionMetas) {
326
- if (meta.parent_session_id === root.session_id) ids.add(meta.session_id);
327
- }
328
- if (selected.session_id) ids.add(selected.session_id);
329
- return [...ids];
330
- }
331
-
332
- function filterAgent(rows, agent) {
333
- if (!agent) return rows;
334
- const q = String(agent).toLowerCase();
335
- const rowMatches = (r) => {
336
- const values = [
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
- // Row-level agent fields only exist on some kinds (tool/steer); usage_raw and
345
- // transport rows carry no agent. Filter at SESSION granularity: keep every
346
- // row of any session where at least one row matches, so per-agent views keep
347
- // their usage/cache/turn aggregates intact.
348
- const matchedSessions = new Set();
349
- for (const r of rows) {
350
- if (rowMatches(r)) {
351
- const sid = sessionId(r);
352
- if (sid) matchedSessions.add(sid);
353
- }
354
- }
355
- return rows.filter((r) => (sessionId(r) ? matchedSessions.has(sessionId(r)) : rowMatches(r)));
356
- }
357
-
358
- function buildRouteGroups(rows) {
359
- const bySid = groupBy(rows, sessionId);
360
- const groups = [];
361
- for (const [sid, srows] of bySid.entries()) {
362
- const meta = inferSessionMeta(srows);
363
- const usageRows = srows.filter((r) => r.kind === 'usage_raw');
364
- const toolRows = srows.filter((r) => r.kind === 'tool');
365
- const sseRows = srows.filter((r) => r.kind === 'sse');
366
- const fetchRows = srows.filter((r) => r.kind === 'fetch');
367
- const transportRows = srows.filter((r) => r.kind === 'transport');
368
- const turns = usageRows.length || new Set(transportRows.map((r) => num(r, 'iteration')).filter((n) => n != null)).size;
369
- const promptTokens = sum(usageRows.map((r) => num(r, 'prompt_tokens')));
370
- const outputTokens = sum(usageRows.map((r) => num(r, 'output_tokens')));
371
- const cachedTokens = sum(usageRows.map((r) => num(r, 'cached_tokens')));
372
- const cacheRatio = cacheRatioFromUsage(usageRows);
373
- groups.push({
374
- ...meta,
375
- session_id: sid,
376
- turns,
377
- tool_calls: toolRows.length,
378
- tool_ms: sum(toolRows.map((r) => num(r, 'tool_ms'))),
379
- llm_stream_ms: sum(sseRows.map((r) => num(r, 'stream_total_ms') ?? num(r, 'sse_parse_ms'))),
380
- headers_ms: sum(fetchRows.map((r) => num(r, 'headers_ms'))),
381
- ttft_p50_ms: percentile(sseRows.map((r) => num(r, 'ttft_ms')).filter((n) => n != null), 50),
382
- prompt_tokens: promptTokens,
383
- output_tokens: outputTokens,
384
- cached_tokens: cachedTokens,
385
- cache_ratio: cacheRatio,
386
- ws_full: transportRows.filter((r) => field(r, 'ws_mode') === 'full').length,
387
- ws_delta: transportRows.filter((r) => field(r, 'ws_mode') === 'delta').length,
388
- reused_connection: transportRows.filter((r) => field(r, 'reused_connection') === true).length,
389
- transport_rows: transportRows.length,
390
- });
391
- }
392
- return groups.sort((a, b) => Number(a.min_ts || 0) - Number(b.min_ts || 0));
393
- }
394
-
395
- function buildCacheDiagnostics(rows) {
396
- const transport = rows.filter((r) => r.kind === 'transport');
397
- const usage = rows.filter((r) => r.kind === 'usage_raw');
398
- const breaks = rows.filter((r) => r.kind === 'cache_break');
399
- const misses = rows.filter((r) => r.kind === 'cache_miss');
400
- const keyCounts = new Map();
401
- for (const r of transport) {
402
- const key = field(r, 'cache_key_hash');
403
- if (key) keyCounts.set(key, (keyCounts.get(key) || 0) + 1);
404
- }
405
- const downgrades = transport.filter((r) => {
406
- const requested = field(r, 'requested_service_tier');
407
- const response = field(r, 'response_service_tier');
408
- return requested && response && requested !== response;
409
- }).map((r) => ({
410
- session_id: sessionId(r),
411
- iteration: num(r, 'iteration'),
412
- requested: field(r, 'requested_service_tier'),
413
- response: field(r, 'response_service_tier'),
414
- model: field(r, 'model'),
415
- ts: r.ts,
416
- }));
417
- const cacheBreaks = breaks.map((r) => {
418
- const reason = field(r, 'reason') || field(r, 'chain_delta_reason') || field(r, 'payload')?.reason || null;
419
- const relatedUsage = nearestRowAround(
420
- usage,
421
- r.ts,
422
- 5_000,
423
- (u) => sessionId(u) === sessionId(r) && num(u, 'iteration') === num(r, 'iteration'),
424
- ) || nearestRowBefore(
425
- usage.filter((u) => sessionId(u) === sessionId(r)),
426
- r.ts,
427
- 30_000,
428
- );
429
- const promptTokens = relatedUsage ? cacheDenom(relatedUsage) : 0;
430
- const cachedTokens = relatedUsage ? (num(relatedUsage, 'cached_tokens') || 0) : 0;
431
- const transitionTag = cacheBreakTransitionTag(r);
432
- const transition = cacheBreakIntentionalTransition(r);
433
- return {
434
- session_id: sessionId(r),
435
- iteration: num(r, 'iteration'),
436
- reason,
437
- phase: classifyCacheBreakPhase(r, usage, transport),
438
- intentional_transition: transitionTag,
439
- actionable: !transition,
440
- explanation: cacheBreakExplanation(reason),
441
- ws_mode: field(r, 'ws_mode'),
442
- cache_key_hash: field(r, 'cache_key_hash'),
443
- request_tool_choice: field(r, 'request_tool_choice'),
444
- request_has_previous_response_id: field(r, 'request_has_previous_response_id'),
445
- body_input_items: field(r, 'body_input_items'),
446
- frame_input_items: field(r, 'frame_input_items'),
447
- prompt_tokens: promptTokens,
448
- cached_tokens: cachedTokens,
449
- cache_ratio: promptTokens > 0 ? cachedTokens / promptTokens : null,
450
- output_tokens: relatedUsage ? (num(relatedUsage, 'output_tokens') || 0) : 0,
451
- ts: r.ts,
452
- };
453
- });
454
- const actionableCacheBreaks = cacheBreaks.filter((b) => b.actionable);
455
- return {
456
- usage_cache_ratio: cacheRatioFromUsage(usage),
457
- cached_tokens: sum(usage.map((r) => num(r, 'cached_tokens'))),
458
- prompt_tokens: sum(usage.map((r) => num(r, 'prompt_tokens'))),
459
- transport_count: transport.length,
460
- ws_full: transport.filter((r) => field(r, 'ws_mode') === 'full').length,
461
- ws_delta: transport.filter((r) => field(r, 'ws_mode') === 'delta').length,
462
- reused_connection: transport.filter((r) => field(r, 'reused_connection') === true).length,
463
- previous_response_id: transport.filter((r) => field(r, 'request_has_previous_response_id') === true).length,
464
- cache_key_hashes: [...keyCounts.entries()].map(([key, count]) => ({ key, count })).sort((a, b) => b.count - a.count),
465
- cache_breaks: cacheBreaks,
466
- actionable_cache_breaks: actionableCacheBreaks,
467
- intentional_cache_breaks: cacheBreaks.filter((b) => !b.actionable),
468
- actual_cache_misses: misses.map((r) => ({
469
- session_id: sessionId(r),
470
- iteration: num(r, 'iteration'),
471
- reason: field(r, 'reason') || field(r, 'payload')?.reason || 'unknown',
472
- ws_mode: field(r, 'ws_mode'),
473
- cache_key_hash: field(r, 'cache_key_hash'),
474
- request_has_previous_response_id: field(r, 'request_has_previous_response_id'),
475
- prompt_tokens: num(r, 'prompt_tokens') || num(field(r, 'payload'), 'prompt_tokens') || 0,
476
- cached_tokens: num(r, 'cached_tokens') || num(field(r, 'payload'), 'cached_tokens') || 0,
477
- uncached_tokens: num(r, 'uncached_tokens') || num(field(r, 'payload'), 'uncached_tokens') || 0,
478
- previous_max_cached_tokens: num(r, 'previous_max_cached_tokens') || num(field(r, 'payload'), 'previous_max_cached_tokens') || 0,
479
- cache_ratio: num(r, 'cache_ratio') ?? num(field(r, 'payload'), 'cache_ratio'),
480
- ts: r.ts,
481
- })),
482
- service_tier_downgrades: downgrades,
483
- low_cache_turns: usage.map((r) => {
484
- const denom = cacheDenom(r);
485
- const ratio = denom > 0 ? (num(r, 'cached_tokens') || 0) / denom : null;
486
- return { session_id: sessionId(r), iteration: num(r, 'iteration'), ratio, cached_tokens: num(r, 'cached_tokens') || 0, prompt_tokens: denom, ts: r.ts };
487
- }).filter((x) => x.prompt_tokens >= 1000 && (x.ratio == null || x.ratio < 0.25)),
488
- };
489
- }
490
-
491
- function summarizePathArg(pathValue) {
492
- if (Array.isArray(pathValue)) {
493
- const parts = pathValue.map((p) => (typeof p === 'string' ? p : `${p?.path || '?'}${p?.offset != null ? `:${p.offset}` : ''}`));
494
- return `batch[${parts.length}] ${parts.slice(0, 3).join(', ')}${parts.length > 3 ? ', …' : ''}`;
495
- }
496
- return String(pathValue || '');
497
- }
498
-
499
- function summarizeToolTarget(row) {
500
- const args = toolArgs(row) || {};
501
- const name = String(field(row, 'tool_name') || '');
502
- if (name === 'read') {
503
- if (Array.isArray(args.path)) return summarizePathArg(args.path);
504
- return `${args.path || '?'}:${args.line || args.offset || ''}`;
505
- }
506
- if (name === 'grep') return `${args.path || '?'} :: ${String(args.pattern || '').slice(0, 80)}`;
507
- if (name === 'code_graph') return `${args.mode || '?'} ${args.file || ''} ${args.symbol || ''}`.trim();
508
- if (name === 'find') return `${args.path || '?'} :: ${args.query || ''}`;
509
- if (name === 'glob') return `${args.path || '?'} :: ${args.pattern || ''}`;
510
- if (name === 'list') return String(args.path || '?');
511
- if (name === 'shell') return compactText(args.command || args.cmd || hashValue(args) || '-', 140);
512
- if (name === 'agent') return compactText(`${args.agent || args.type || 'agent'} ${args.tag || args.task_id || ''} ${args.prompt || args.message || ''}`, 140);
513
- if (name === 'apply_patch') return 'patch';
514
- return hashValue(args) || '-';
515
- }
516
-
517
- function toolResultKind(row) {
518
- return String(field(row, 'result_kind') || 'unknown').toLowerCase();
519
- }
520
-
521
- function toolFailureReason(row) {
522
- const candidates = [
523
- field(row, 'error'),
524
- field(row, 'message'),
525
- field(row, 'result_error'),
526
- field(row, 'result_preview'),
527
- field(row, 'result_excerpt'),
528
- field(row, 'error_first_line'),
529
- field(row, 'error_preview'),
530
- field(row, 'stderr'),
531
- field(row, 'stdout'),
532
- ];
533
- const hit = candidates.find((v) => v != null && String(v).trim());
534
- return hit ? compactText(hit, 180) : null;
535
- }
536
-
537
- function toolFailureCategory(row) {
538
- return field(row, 'category') || null;
539
- }
540
-
541
- function findFailureDetail(row, failureRows = []) {
542
- const sid = sessionId(row);
543
- const it = num(row, 'iteration');
544
- const tool = field(row, 'tool_name');
545
- const ts = Number(row.ts || 0);
546
- let best = null;
547
- let bestDelta = Infinity;
548
- for (const f of failureRows) {
549
- if (sessionId(f) !== sid) continue;
550
- if (tool && field(f, 'tool_name') !== tool) continue;
551
- if (it != null && num(f, 'iteration') !== it) continue;
552
- const fts = Number(f.ts || 0);
553
- const delta = Number.isFinite(ts) && Number.isFinite(fts) ? Math.abs(fts - ts) : 0;
554
- if (delta <= 120_000 && delta < bestDelta) {
555
- best = f;
556
- bestDelta = delta;
557
- }
558
- }
559
- return best;
560
- }
561
-
562
- function summarizeKindCounts(rows) {
563
- const counts = new Map();
564
- for (const row of rows) {
565
- const kind = toolResultKind(row);
566
- counts.set(kind, (counts.get(kind) || 0) + 1);
567
- }
568
- return [...counts.entries()]
569
- .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
570
- .map(([kind, count]) => ({ kind, count }));
571
- }
572
-
573
- function fmtKindCounts(counts, limit = 3) {
574
- const shown = (counts || []).slice(0, limit).map((x) => `${x.kind}×${x.count}`);
575
- const hidden = Math.max(0, (counts || []).length - shown.length);
576
- return `${shown.join(', ')}${hidden ? ` +${hidden}` : ''}` || '-';
577
- }
578
-
579
- function buildToolDiagnostics(rows, failureRows = []) {
580
- const tools = rows.filter((r) => r.kind === 'tool');
581
- const byName = [];
582
- for (const [name, trows] of groupBy(tools, (r) => String(field(r, 'tool_name') || '(unknown)')).entries()) {
583
- const kindCounts = summarizeKindCounts(trows);
584
- const errors = trows.filter((r) => toolResultKind(r) === 'error').length;
585
- byName.push({
586
- tool: name,
587
- count: trows.length,
588
- ok: trows.length - errors,
589
- errors,
590
- result_kinds: kindCounts,
591
- total_ms: sum(trows.map((r) => num(r, 'tool_ms'))),
592
- p50_ms: percentile(trows.map((r) => num(r, 'tool_ms')).filter((n) => n != null), 50),
593
- p95_ms: percentile(trows.map((r) => num(r, 'tool_ms')).filter((n) => n != null), 95),
594
- bytes: sum(trows.map((r) => num(r, 'result_bytes_est'))),
595
- lines: sum(trows.map((r) => num(r, 'result_lines_est'))),
596
- });
597
- }
598
- byName.sort((a, b) => b.total_ms - a.total_ms || b.count - a.count);
599
-
600
- const duplicates = [];
601
- for (const [key, trows] of groupBy(tools, (r) => `${field(r, 'tool_name') || ''}:${toolArgsHash(r) || ''}`).entries()) {
602
- if (!key.endsWith(':') && trows.length >= 2) {
603
- const sorted = [...trows].sort((a, b) => Number(a.ts || 0) - Number(b.ts || 0));
604
- for (let i = 1; i < sorted.length; i += 1) {
605
- if (Number(sorted[i].ts || 0) - Number(sorted[i - 1].ts || 0) <= 120_000) {
606
- duplicates.push({
607
- tool: field(sorted[i], 'tool_name'),
608
- count: trows.length,
609
- result_kinds: summarizeKindCounts(trows),
610
- first_ts: sorted[0].ts,
611
- last_ts: sorted[sorted.length - 1].ts,
612
- target: summarizeToolTarget(sorted[i]),
613
- args_hash: toolArgsHash(sorted[i]),
614
- });
615
- break;
616
- }
617
- }
618
- }
619
- }
620
- duplicates.sort((a, b) => b.count - a.count);
621
-
622
- const failedRepeats = [];
623
- for (const dup of duplicates) {
624
- const matches = tools.filter((r) => field(r, 'tool_name') === dup.tool && toolArgsHash(r) === dup.args_hash);
625
- const errors = matches.filter((r) => toolResultKind(r) === 'error');
626
- if (errors.length >= 2) failedRepeats.push({ ...dup, error_count: errors.length });
627
- }
628
-
629
- const failures = tools
630
- .filter((r) => toolResultKind(r) === 'error')
631
- .map((r) => {
632
- const detail = findFailureDetail(r, failureRows);
633
- return {
634
- tool: field(r, 'tool_name'),
635
- session_id: sessionId(r),
636
- agent: field(r, 'agent') || field(r, 'sourceName') || field(detail, 'agent') || null,
637
- iteration: num(r, 'iteration'),
638
- tool_ms: num(r, 'tool_ms') || num(detail, 'tool_ms') || 0,
639
- bytes: num(r, 'result_bytes_est') || num(detail, 'result_bytes_est') || 0,
640
- lines: num(r, 'result_lines_est') || num(detail, 'result_lines_est') || 0,
641
- category: toolFailureCategory(detail) || toolFailureCategory(r),
642
- reason: toolFailureReason(detail) || toolFailureReason(r),
643
- preview: field(detail, 'error_preview') || null,
644
- target: summarizeToolTarget(detail || r),
645
- ts: r.ts,
646
- };
647
- })
648
- .sort((a, b) => Number(b.ts || 0) - Number(a.ts || 0));
649
-
650
- const successes = tools
651
- .filter((r) => toolResultKind(r) !== 'error')
652
- .map((r) => ({
653
- tool: field(r, 'tool_name'),
654
- session_id: sessionId(r),
655
- agent: field(r, 'agent') || field(r, 'sourceName') || null,
656
- iteration: num(r, 'iteration'),
657
- tool_ms: num(r, 'tool_ms') || 0,
658
- bytes: num(r, 'result_bytes_est') || 0,
659
- lines: num(r, 'result_lines_est') || 0,
660
- result_kind: toolResultKind(r),
661
- target: summarizeToolTarget(r),
662
- ts: r.ts,
663
- }))
664
- .sort((a, b) => Number(b.ts || 0) - Number(a.ts || 0));
665
-
666
- const broadResults = tools.filter((r) => {
667
- const bytes = num(r, 'result_bytes_est') || 0;
668
- const lines = num(r, 'result_lines_est') || 0;
669
- return bytes >= 64 * 1024 || lines >= 1000 || String(field(r, 'result_kind') || '').includes('offload');
670
- }).map((r) => ({
671
- tool: field(r, 'tool_name'),
672
- session_id: sessionId(r),
673
- iteration: num(r, 'iteration'),
674
- bytes: num(r, 'result_bytes_est') || 0,
675
- lines: num(r, 'result_lines_est') || 0,
676
- target: summarizeToolTarget(r),
677
- ts: r.ts,
678
- })).sort((a, b) => b.bytes - a.bytes);
679
-
680
- const readRows = tools.filter((r) => field(r, 'tool_name') === 'read');
681
- const readFragmentation = [];
682
- for (const [path, rrows] of groupBy(readRows, (r) => String((toolArgs(r) || {}).path || '')).entries()) {
683
- if (!path || path.includes(',')) continue;
684
- const lineReads = rrows.map((r) => ({ row: r, line: Number((toolArgs(r) || {}).line || (toolArgs(r) || {}).offset || 0), ts: Number(r.ts || 0) }))
685
- .filter((x) => Number.isFinite(x.line) && x.line > 0)
686
- .sort((a, b) => a.line - b.line);
687
- if (lineReads.length >= 3) {
688
- const span = lineReads[lineReads.length - 1].line - lineReads[0].line;
689
- const timeSpan = Math.max(...lineReads.map((x) => x.ts)) - Math.min(...lineReads.map((x) => x.ts));
690
- if (span <= 800 && timeSpan <= 10 * 60_000) {
691
- readFragmentation.push({ path, count: lineReads.length, line_span: span, time_span_ms: timeSpan });
692
- }
693
- }
694
- }
695
- readFragmentation.sort((a, b) => b.count - a.count);
696
-
697
- const singleToolBatches = rows.filter((r) => r.kind === 'batch' && Number(field(r, 'tool_call_count')) === 1)
698
- .sort((a, b) => Number(a.ts || 0) - Number(b.ts || 0));
699
- let missedParallelism = 0;
700
- const singleToolBatchDetails = singleToolBatches.map((batch) => {
701
- const bts = Number(batch.ts || 0);
702
- const sid = sessionId(batch);
703
- const tool = tools
704
- .filter((r) => sessionId(r) === sid && Number(r.ts || 0) >= bts - 100 && Number(r.ts || 0) <= bts + 15_000)
705
- .sort((a, b) => Number(a.ts || 0) - Number(b.ts || 0))[0] || null;
706
- return {
707
- batch,
708
- ts: bts,
709
- session_id: sid,
710
- agent: field(batch, 'agent') || field(tool, 'agent') || null,
711
- iteration: num(tool || batch, 'iteration'),
712
- tool: field(tool, 'tool_name') || '(unknown)',
713
- tool_ms: num(tool, 'tool_ms') || 0,
714
- result_kind: tool ? toolResultKind(tool) : 'unknown',
715
- target: tool ? summarizeToolTarget(tool) : '-',
716
- };
717
- });
718
- const sequentialClusters = [];
719
- let cluster = [];
720
- for (let i = 1; i < singleToolBatches.length; i += 1) {
721
- if (Number(singleToolBatches[i].ts || 0) - Number(singleToolBatches[i - 1].ts || 0) <= 15_000) missedParallelism += 1;
722
- }
723
- for (const detail of singleToolBatchDetails) {
724
- const prev = cluster[cluster.length - 1];
725
- if (prev && detail.session_id === prev.session_id && detail.ts - prev.ts <= 15_000) {
726
- cluster.push(detail);
727
- } else {
728
- if (cluster.length >= 3) sequentialClusters.push(cluster);
729
- cluster = [detail];
730
- }
731
- }
732
- if (cluster.length >= 3) sequentialClusters.push(cluster);
733
- const sequentialToolClusters = sequentialClusters.map((items) => {
734
- const toolCounts = countBy(items, (x) => x.tool).slice(0, 5).map(([tool, count]) => ({ tool, count }));
735
- const errorCount = items.filter((x) => x.result_kind === 'error').length;
736
- return {
737
- session_id: items[0].session_id,
738
- agent: items[0].agent || null,
739
- count: items.length,
740
- span_ms: items[items.length - 1].ts - items[0].ts,
741
- tool_ms: sum(items.map((x) => x.tool_ms)),
742
- errors: errorCount,
743
- tools: toolCounts,
744
- start_it: items[0].iteration,
745
- end_it: items[items.length - 1].iteration,
746
- examples: items.slice(0, 3).map((x) => `${x.tool}:${compactText(x.target, 80)}`),
747
- };
748
- }).sort((a, b) => b.count - a.count || b.tool_ms - a.tool_ms).slice(0, 20);
749
-
750
- const readonlyStalls = [];
751
- for (const [sid, srows] of groupBy(tools, sessionId).entries()) {
752
- for (const [agent, arows] of groupBy(srows, (r) => field(r, 'agent') || field(r, 'sourceName') || '').entries()) {
753
- const sorted = [...arows].sort((a, b) => Number(a.ts || 0) - Number(b.ts || 0));
754
- let run = [];
755
- const flushRun = () => {
756
- const turns = new Set(run.map((r) => num(r, 'iteration')).filter((n) => n != null)).size;
757
- if (turns >= READONLY_STALL_MIN_RUN) {
758
- const pathCounts = countBy(run, (r) => summarizePathArg((toolArgs(r) || {}).path)).filter(([p]) => p).slice(0, 3);
759
- readonlyStalls.push({
760
- session_id: sid,
761
- agent: agent || null,
762
- run_length: run.length,
763
- run_turns: turns,
764
- start_it: num(run[0], 'iteration'),
765
- end_it: num(run[run.length - 1], 'iteration'),
766
- top_paths: pathCounts.map(([path, count]) => ({ path, count })),
767
- });
768
- }
769
- run = [];
770
- };
771
- for (const row of sorted) {
772
- const name = String(field(row, 'tool_name') || '');
773
- if (!READONLY_TOOL_NAMES.has(name)) { flushRun(); continue; }
774
- // A backwards iteration jump means a new prompt/steer restarted the
775
- // loop; that is a fresh run, not a continuation of the same stall.
776
- const it = num(row, 'iteration');
777
- const prevIt = run.length ? num(run[run.length - 1], 'iteration') : null;
778
- if (it != null && prevIt != null && it < prevIt) flushRun();
779
- run.push(row);
780
- }
781
- flushRun();
782
- }
783
- }
784
- readonlyStalls.sort((a, b) => (b.run_turns || 0) - (a.run_turns || 0) || b.run_length - a.run_length);
785
-
786
- const identicalCallRepeats = [];
787
- for (const [key, trows] of groupBy(
788
- tools.filter((r) => toolResultKind(r) !== 'error'),
789
- (r) => `${sessionId(r)}::${field(r, 'tool_name') || ''}::${toolArgsHash(r) || ''}`,
790
- ).entries()) {
791
- if (!toolArgsHash(trows[0])) continue;
792
- const iterations = [...new Set(trows.map((r) => num(r, 'iteration')).filter((n) => n != null))];
793
- if (iterations.length >= IDENTICAL_CALL_MIN_COUNT) {
794
- const sorted = [...trows].sort((a, b) => Number(a.ts || 0) - Number(b.ts || 0));
795
- identicalCallRepeats.push({
796
- session_id: sessionId(sorted[0]),
797
- tool: field(sorted[0], 'tool_name'),
798
- args_hash: toolArgsHash(sorted[0]),
799
- count: iterations.length,
800
- iterations: iterations.slice(0, 6),
801
- });
802
- }
803
- }
804
- identicalCallRepeats.sort((a, b) => b.count - a.count);
805
-
806
- const editFragmentation = [];
807
- for (const [sid, srows] of groupBy(tools.filter((r) => sessionId(r)), sessionId).entries()) {
808
- const patches = srows
809
- .filter((r) => String(field(r, 'tool_name') || '') === 'apply_patch')
810
- .sort((a, b) => Number(a.ts || 0) - Number(b.ts || 0));
811
- if (patches.length < 2) continue;
812
- const byIter = countBy(patches, (r) => num(r, 'iteration'));
813
- const multiPatchTurns = byIter.filter(([, c]) => c > 1).length;
814
- let crossTurnPatch = 0;
815
- for (let i = 1; i < patches.length; i++) {
816
- const earlier = patches[i - 1];
817
- const later = patches[i];
818
- const ei = num(earlier, 'iteration');
819
- const li = num(later, 'iteration');
820
- if (ei == null || li == null || ei === li) continue;
821
- const dt = Number(later.ts || 0) - Number(earlier.ts || 0);
822
- if (li === ei + 1 || dt < 60_000) crossTurnPatch++;
823
- }
824
- const fragScore = multiPatchTurns + crossTurnPatch;
825
- if (fragScore < 2) continue;
826
- editFragmentation.push({
827
- session_id: sid,
828
- agent: field(patches[0], 'agent'),
829
- patch_calls: patches.length,
830
- multi_patch_turns: multiPatchTurns,
831
- cross_turn_patch: crossTurnPatch,
832
- frag_score: fragScore,
833
- start_it: num(patches[0], 'iteration'),
834
- end_it: num(patches[patches.length - 1], 'iteration'),
835
- });
836
- }
837
- editFragmentation.sort((a, b) => b.frag_score - a.frag_score);
838
-
839
- return {
840
- total_tool_calls: tools.length,
841
- total_tool_ms: sum(tools.map((r) => num(r, 'tool_ms'))),
842
- result_kinds: summarizeKindCounts(tools),
843
- by_name: byName,
844
- failures: failures.slice(0, 20),
845
- recent_successes: successes.slice(0, 20),
846
- duplicates: duplicates.slice(0, 20),
847
- failed_repeats: failedRepeats.slice(0, 20),
848
- broad_results: broadResults.slice(0, 20),
849
- read_fragmentation: readFragmentation.slice(0, 20),
850
- missed_parallelism_heuristic: {
851
- consecutive_single_tool_batches: missedParallelism,
852
- single_tool_batches: singleToolBatches.length,
853
- },
854
- sequential_tool_clusters: sequentialToolClusters,
855
- readonly_stalls: readonlyStalls.slice(0, 20),
856
- identical_call_repeats: identicalCallRepeats.slice(0, 20),
857
- edit_fragmentation: editFragmentation.slice(0, 20),
858
- };
859
- }
860
-
861
- function nearestRowBefore(rows, ts, maxBeforeMs = 120_000) {
862
- const t = Number(ts);
863
- if (!Number.isFinite(t)) return null;
864
- let best = null;
865
- let bestDelta = Infinity;
866
- for (const row of rows) {
867
- const rts = Number(row.ts || 0);
868
- if (!Number.isFinite(rts) || rts > t) continue;
869
- const delta = t - rts;
870
- if (delta <= maxBeforeMs && delta < bestDelta) {
871
- best = row;
872
- bestDelta = delta;
873
- }
874
- }
875
- return best;
876
- }
877
-
878
- function nearestRowAround(rows, ts, maxDeltaMs = 1_000, predicate = null) {
879
- const t = Number(ts);
880
- if (!Number.isFinite(t)) return null;
881
- let best = null;
882
- let bestDelta = Infinity;
883
- for (const row of rows) {
884
- if (predicate && !predicate(row)) continue;
885
- const rts = Number(row.ts || 0);
886
- if (!Number.isFinite(rts)) continue;
887
- const delta = Math.abs(rts - t);
888
- if (delta <= maxDeltaMs && delta < bestDelta) {
889
- best = row;
890
- bestDelta = delta;
891
- }
892
- }
893
- return best;
894
- }
895
-
896
- function buildTurnDiagnostics(rows, routeGroups) {
897
- const sessionMeta = new Map(routeGroups.map((g) => [g.session_id, g]));
898
- const bySid = groupBy(rows, sessionId);
899
- const turns = [];
900
- for (const [sid, srows] of bySid.entries()) {
901
- const meta = sessionMeta.get(sid) || inferSessionMeta(srows);
902
- const usageRows = srows.filter((r) => r.kind === 'usage_raw');
903
- const fetchRows = srows.filter((r) => r.kind === 'fetch');
904
- const sseRows = srows.filter((r) => r.kind === 'sse');
905
- const transportRows = srows.filter((r) => r.kind === 'transport');
906
- const cacheBreakRows = srows.filter((r) => r.kind === 'cache_break');
907
- const toolRows = srows.filter((r) => r.kind === 'tool');
908
- const usageSorted = [...usageRows].sort((a, b) => Number(a.ts || 0) - Number(b.ts || 0));
909
- const seenByIteration = new Map();
910
-
911
- const pushTurn = ({ usage = null, transport = null, tools = [], cacheBreaks = [], nextTs = Infinity }) => {
912
- const iteration = num(usage || transport || tools[0] || cacheBreaks[0], 'iteration');
913
- const occurrence = (seenByIteration.get(iteration) || 0) + 1;
914
- seenByIteration.set(iteration, occurrence);
915
- const tRef = Number(usage?.ts || transport?.ts || tools.at(-1)?.ts || 0);
916
- const sse = usage ? nearestRowBefore(sseRows, usage.ts, 10_000) : nearestRowBefore(sseRows, tRef, 10_000);
917
- const streamMs = sse ? (num(sse, 'stream_total_ms') ?? num(sse, 'sse_parse_ms') ?? 0) : 0;
918
- const fetchWindowMs = Math.max(30_000, streamMs + 10_000);
919
- const fetch = transport ? nearestRowBefore(fetchRows, transport.ts, fetchWindowMs) : nearestRowBefore(fetchRows, tRef, fetchWindowMs);
920
- const promptTokens = usage ? cacheDenom(usage) : 0;
921
- const cachedTokens = usage ? (num(usage, 'cached_tokens') || 0) : 0;
922
- const cacheRatio = promptTokens > 0 ? cachedTokens / promptTokens : null;
923
- const headersMs = fetch ? (num(fetch, 'headers_ms') || 0) : 0;
924
- const toolMs = sum(tools.map((r) => num(r, 'tool_ms')));
925
- const outputTokens = usage ? (num(usage, 'output_tokens') || 0) : 0;
926
- const thinkingTokens = usage ? (num(usage, 'thinking_tokens') || 0) : 0;
927
- const serviceRequested = transport ? field(transport, 'requested_service_tier') : null;
928
- const serviceResponse = transport ? field(transport, 'response_service_tier') : field(usage, 'service_tier');
929
- const flags = [];
930
- if (field(transport, 'ws_mode') === 'full') flags.push('full_ws');
931
- if (cacheBreaks.length) flags.push(`cache_break:${cacheBreaks.map((r) => field(r, 'reason') || field(r, 'chain_delta_reason') || field(r, 'payload')?.reason || 'unknown').join('|')}`);
932
- if (serviceRequested && serviceResponse && serviceRequested !== serviceResponse) flags.push(`tier:${serviceRequested}->${serviceResponse}`);
933
- if (streamMs >= 15_000) flags.push('slow_stream');
934
- if (headersMs >= 5_000) flags.push('slow_headers');
935
- if (toolMs >= 5_000) flags.push('slow_tools');
936
- if (promptTokens >= 50_000 && cacheRatio != null && cacheRatio < 0.8) flags.push('low_cache_large_prompt');
937
- turns.push({
938
- session_id: sid,
939
- agent: meta.agent || null,
940
- model: meta.model || null,
941
- provider: meta.provider || null,
942
- iteration,
943
- occurrence,
944
- turn_label: occurrence > 1 ? `${iteration ?? '-'}#${occurrence}` : String(iteration ?? '-'),
945
- ts: tRef || null,
946
- headers_ms: headersMs,
947
- stream_ms: streamMs,
948
- tool_ms: toolMs,
949
- approx_active_ms: headersMs + streamMs + toolMs,
950
- tool_calls: tools.length,
951
- prompt_tokens: promptTokens,
952
- output_tokens: outputTokens,
953
- thinking_tokens: thinkingTokens,
954
- cached_tokens: cachedTokens,
955
- cache_ratio: cacheRatio,
956
- ws_mode: transport ? field(transport, 'ws_mode') : null,
957
- reused_connection: transport ? field(transport, 'reused_connection') : null,
958
- has_previous_response_id: transport ? field(transport, 'request_has_previous_response_id') : null,
959
- service_requested: serviceRequested,
960
- service_response: serviceResponse,
961
- cache_breaks: cacheBreaks.map((r) => field(r, 'reason') || field(r, 'chain_delta_reason') || field(r, 'payload')?.reason || 'unknown'),
962
- top_tools: Object.values(tools.reduce((acc, r) => {
963
- const name = String(field(r, 'tool_name') || '(unknown)');
964
- if (!acc[name]) acc[name] = { tool: name, count: 0, ms: 0 };
965
- acc[name].count += 1;
966
- acc[name].ms += num(r, 'tool_ms') || 0;
967
- return acc;
968
- }, {})).sort((a, b) => b.ms - a.ms || b.count - a.count).slice(0, 3),
969
- flags,
970
- });
971
- };
972
-
973
- for (let idx = 0; idx < usageSorted.length; idx += 1) {
974
- const usage = usageSorted[idx];
975
- const iteration = num(usage, 'iteration');
976
- const ts = Number(usage.ts || 0);
977
- const nextTs = Number(usageSorted[idx + 1]?.ts || Infinity);
978
- const inWindow = (r) => {
979
- const rts = Number(r.ts || 0);
980
- return Number.isFinite(rts) && rts >= ts - 100 && rts < nextTs;
981
- };
982
- const sameIteration = (r) => num(r, 'iteration') === iteration;
983
- const transport = nearestRowAround(transportRows, ts, 1_000, sameIteration);
984
- const tools = toolRows.filter((r) => sameIteration(r) && inWindow(r));
985
- const cacheBreaks = cacheBreakRows.filter((r) => sameIteration(r) && inWindow(r));
986
- pushTurn({ usage, transport, tools, cacheBreaks, nextTs });
987
- }
988
-
989
- const usageMatchedTransports = new Set(usageSorted
990
- .map((usage) => nearestRowAround(transportRows, usage.ts, 1_000, (r) => num(r, 'iteration') === num(usage, 'iteration')))
991
- .filter(Boolean));
992
- for (const transport of transportRows.filter((r) => !usageMatchedTransports.has(r)).sort((a, b) => Number(a.ts || 0) - Number(b.ts || 0))) {
993
- const iteration = num(transport, 'iteration');
994
- const ts = Number(transport.ts || 0);
995
- const nextUsage = usageSorted.find((r) => Number(r.ts || 0) > ts);
996
- const nextTs = Number(nextUsage?.ts || Infinity);
997
- const tools = toolRows.filter((r) => num(r, 'iteration') === iteration && Number(r.ts || 0) >= ts - 100 && Number(r.ts || 0) < nextTs);
998
- const cacheBreaks = cacheBreakRows.filter((r) => num(r, 'iteration') === iteration && Number(r.ts || 0) >= ts - 100 && Number(r.ts || 0) < nextTs);
999
- pushTurn({ transport, tools, cacheBreaks, nextTs });
1000
- }
1001
- }
1002
- const slowestActive = [...turns].sort((a, b) => b.approx_active_ms - a.approx_active_ms).slice(0, 20);
1003
- const slowestStream = [...turns].sort((a, b) => b.stream_ms - a.stream_ms).slice(0, 20);
1004
- const slowestTools = [...turns].sort((a, b) => b.tool_ms - a.tool_ms).slice(0, 20);
1005
- const cacheBreakTurns = turns.filter((t) => t.cache_breaks.length > 0);
1006
- return { turns, slowest_active: slowestActive, slowest_stream: slowestStream, slowest_tools: slowestTools, cache_break_turns: cacheBreakTurns };
1007
- }
1008
-
1009
- function buildTokenDiagnostics(turns) {
1010
- const bySession = groupBy([...turns].sort((a, b) => Number(a.ts || 0) - Number(b.ts || 0)), (t) => t.session_id);
1011
- const sessions = [];
1012
- const growthTurns = [];
1013
- const outputHeavy = [];
1014
- const cacheMissCost = [];
1015
-
1016
- for (const [sid, trows] of bySession.entries()) {
1017
- const useful = trows.filter((t) => Number(t.prompt_tokens || 0) > 0);
1018
- if (!useful.length) continue;
1019
- const first = useful[0];
1020
- const last = useful[useful.length - 1];
1021
- const maxPromptTurn = useful.reduce((best, t) => (Number(t.prompt_tokens || 0) > Number(best.prompt_tokens || 0) ? t : best), useful[0]);
1022
- const totalOutput = sum(useful.map((t) => t.output_tokens));
1023
- const totalThinking = sum(useful.map((t) => t.thinking_tokens));
1024
- const uncachedTokens = sum(useful.map((t) => Math.max(0, (t.prompt_tokens || 0) - (t.cached_tokens || 0))));
1025
- sessions.push({
1026
- session_id: sid,
1027
- agent: last.agent || first.agent || null,
1028
- turns: useful.length,
1029
- first_prompt: first.prompt_tokens || 0,
1030
- last_prompt: last.prompt_tokens || 0,
1031
- max_prompt: maxPromptTurn.prompt_tokens || 0,
1032
- max_prompt_it: maxPromptTurn.turn_label || maxPromptTurn.iteration,
1033
- prompt_growth: Math.max(0, (last.prompt_tokens || 0) - (first.prompt_tokens || 0)),
1034
- total_output: totalOutput,
1035
- total_thinking: totalThinking,
1036
- uncached_tokens: uncachedTokens,
1037
- cache_ratio: cacheRatioFromUsage(useful.map((t) => ({
1038
- prompt_tokens: t.prompt_tokens,
1039
- cached_tokens: t.cached_tokens,
1040
- }))),
1041
- });
1042
-
1043
- for (let i = 0; i < useful.length; i += 1) {
1044
- const t = useful[i];
1045
- const prev = useful[i - 1] || null;
1046
- const promptDelta = prev ? (t.prompt_tokens || 0) - (prev.prompt_tokens || 0) : 0;
1047
- const out = t.output_tokens || 0;
1048
- const uncached = Math.max(0, (t.prompt_tokens || 0) - (t.cached_tokens || 0));
1049
- const growthPerOutput = out > 0 ? promptDelta / out : null;
1050
- const tokenRow = {
1051
- session_id: sid,
1052
- agent: t.agent || null,
1053
- iteration: t.iteration,
1054
- turn_label: t.turn_label || (t.iteration ?? '-'),
1055
- prompt_tokens: t.prompt_tokens || 0,
1056
- prompt_delta: promptDelta,
1057
- output_tokens: out,
1058
- thinking_tokens: t.thinking_tokens || 0,
1059
- cached_tokens: t.cached_tokens || 0,
1060
- cache_ratio: t.cache_ratio,
1061
- uncached_tokens: uncached,
1062
- growth_per_output: growthPerOutput,
1063
- ts: t.ts,
1064
- };
1065
- if (promptDelta >= 5_000 || (growthPerOutput != null && growthPerOutput >= 10)) growthTurns.push(tokenRow);
1066
- if (out >= 1_000 || (t.thinking_tokens || 0) >= 1_000) outputHeavy.push(tokenRow);
1067
- if (uncached >= 10_000 || ((t.cache_ratio ?? 1) < 0.8 && (t.prompt_tokens || 0) >= 20_000)) cacheMissCost.push(tokenRow);
1068
- }
1069
- }
1070
-
1071
- sessions.sort((a, b) => b.prompt_growth - a.prompt_growth || b.total_output - a.total_output);
1072
- growthTurns.sort((a, b) => b.prompt_delta - a.prompt_delta);
1073
- outputHeavy.sort((a, b) => (b.output_tokens + b.thinking_tokens) - (a.output_tokens + a.thinking_tokens));
1074
- cacheMissCost.sort((a, b) => b.uncached_tokens - a.uncached_tokens);
1075
- return {
1076
- sessions,
1077
- growth_turns: growthTurns.slice(0, 20),
1078
- output_heavy_turns: outputHeavy.slice(0, 20),
1079
- cache_miss_cost_turns: cacheMissCost.slice(0, 20),
1080
- };
1081
- }
1082
-
1083
- function buildCompactDiagnostics(rows, selectedIds) {
1084
- const selectedBase = new Set(selectedIds.map(baseSessionId));
1085
- const compactRows = rows.filter((r) => {
1086
- const sid = sessionId(r);
1087
- return isCompactSessionId(sid) && selectedBase.has(baseSessionId(sid));
1088
- });
1089
- const metaRows = rows.filter((r) => {
1090
- if (r.kind !== 'compact_meta') return false;
1091
- return selectedBase.has(baseSessionId(sessionId(r)));
1092
- }).sort((a, b) => Number(b.ts || 0) - Number(a.ts || 0));
1093
- const cache = buildCacheDiagnostics(compactRows);
1094
- const usageRows = compactRows.filter((r) => r.kind === 'usage_raw');
1095
- const transportRows = compactRows.filter((r) => r.kind === 'transport');
1096
- const sessions = [];
1097
- for (const [sid, srows] of groupBy(compactRows, sessionId).entries()) {
1098
- const usage = srows.filter((r) => r.kind === 'usage_raw');
1099
- const transports = srows.filter((r) => r.kind === 'transport');
1100
- const breaks = srows.filter((r) => r.kind === 'cache_break');
1101
- const sseRows = srows.filter((r) => r.kind === 'sse');
1102
- sessions.push({
1103
- session_id: sid,
1104
- base_session_id: baseSessionId(sid),
1105
- turns: usage.length,
1106
- prompt_tokens: sum(usage.map((r) => cacheDenom(r))),
1107
- cached_tokens: sum(usage.map((r) => num(r, 'cached_tokens'))),
1108
- output_tokens: sum(usage.map((r) => num(r, 'output_tokens'))),
1109
- thinking_tokens: sum(usage.map((r) => num(r, 'thinking_tokens'))),
1110
- stream_ms: sum(sseRows.map((r) => num(r, 'stream_total_ms') ?? num(r, 'sse_parse_ms'))),
1111
- cache_ratio: cacheRatioFromUsage(usage),
1112
- breaks: breaks.length,
1113
- no_anchor: breaks.filter((r) => String(field(r, 'reason') || field(r, 'chain_delta_reason') || '').includes('no_anchor')).length,
1114
- full_ws: transports.filter((r) => field(r, 'ws_mode') === 'full').length,
1115
- delta_ws: transports.filter((r) => field(r, 'ws_mode') === 'delta').length,
1116
- });
1117
- }
1118
- sessions.sort((a, b) => b.stream_ms - a.stream_ms || b.prompt_tokens - a.prompt_tokens);
1119
- return {
1120
- rows: compactRows.length,
1121
- sessions,
1122
- meta_rows: metaRows.length,
1123
- recent_meta: metaRows.slice(0, 10).map((r) => {
1124
- const details = field(r, 'details') || {};
1125
- const semantic = details?.semantic || null;
1126
- const recall = details?.recallFastTrack || null;
1127
- const pipe = recall?.pipeline || null;
1128
- return {
1129
- session_id: sessionId(r),
1130
- ts: r.ts || null,
1131
- iteration: num(r, 'iteration'),
1132
- stage: field(r, 'stage'),
1133
- trigger: field(r, 'trigger'),
1134
- compact_type: field(r, 'compact_type'),
1135
- changed: field(r, 'compact_changed') === true,
1136
- before_tokens: num(r, 'message_tokens_est'),
1137
- pressure_tokens: num(r, 'pressure_tokens'),
1138
- trigger_tokens: num(r, 'trigger_tokens'),
1139
- boundary_tokens: num(r, 'boundary_tokens') ?? num(r, 'budget_tokens'),
1140
- target_budget_tokens: num(r, 'target_budget_tokens'),
1141
- after_tokens: recall?.finalTokens ?? semantic?.finalTokens ?? null,
1142
- before_messages: num(r, 'before_count'),
1143
- after_messages: num(r, 'after_count'),
1144
- duration_ms: num(r, 'duration_ms'),
1145
- error: field(r, 'error'),
1146
- recall_pipeline: pipe ? {
1147
- ingest_ms: pipe.ingestMs ?? null,
1148
- initial_dump_ms: pipe.initialDumpMs ?? null,
1149
- initial_raw_pending: pipe.initialRawPending ?? null,
1150
- cycle1_ms: pipe.cycle1Ms ?? null,
1151
- cycle1_passes: pipe.cycle1Passes ?? null,
1152
- cycle1_raw_remaining: pipe.cycle1RawRemaining ?? null,
1153
- final_recall_kb: pipe.finalRecallBytes != null ? Math.round(pipe.finalRecallBytes / 1024) : null,
1154
- } : null,
1155
- fit: recall || semantic ? {
1156
- head_messages: (recall || semantic).headMessages ?? null,
1157
- tail_messages: (recall || semantic).tailMessages ?? null,
1158
- mandatory_cost: (recall || semantic).mandatoryCost ?? null,
1159
- remaining_tokens: (recall || semantic).remainingTokens ?? null,
1160
- budget_raised: (recall || semantic).budgetRaised === true,
1161
- final_tokens: (recall || semantic).finalTokens ?? null,
1162
- recall_chars: recall?.recallChars ?? null,
1163
- prior_chars: recall?.priorChars ?? null,
1164
- tail_truncated: recall?.tailTruncated ?? null,
1165
- summary_repaired: semantic?.summaryRepaired ?? null,
1166
- } : null,
1167
- };
1168
- }),
1169
- cache_breaks: cache.cache_breaks,
1170
- usage_rows: usageRows.length,
1171
- transport_rows: transportRows.length,
1172
- };
1173
- }
1174
-
1175
- function selectCacheBreakIssues(rows, limit = 10) {
1176
- const ordered = [...rows].sort((a, b) => (
1177
- Number(a.ts || 0) - Number(b.ts || 0)
1178
- || String(a.session_id || '').localeCompare(String(b.session_id || ''))
1179
- || Number(a.iteration || 0) - Number(b.iteration || 0)
1180
- ));
1181
- const root = ordered.filter((row) => !isCompactSessionId(row.session_id));
1182
- const compact = ordered.filter((row) => isCompactSessionId(row.session_id));
1183
- if (!root.length || !compact.length || limit < 2) return ordered.slice(0, limit);
1184
- return [...root.slice(0, limit - 1), ...compact.slice(0, 1)];
1185
- }
1186
-
1187
- function buildIssues(routeGroups, cache, tools) {
1188
- const issues = [];
1189
- for (const stall of (tools.readonly_stalls || []).slice(0, 10)) {
1190
- const readonlyRole = READONLY_ROLE_AGENTS.has(String(stall.agent || '').toLowerCase());
1191
- const turns = stall.run_turns ?? stall.run_length;
1192
- issues.push({
1193
- severity: readonlyRole ? 'low' : (turns >= 15 ? 'high' : 'medium'),
1194
- type: 'readonly_stall',
1195
- message: `${stall.agent || shortId(stall.session_id)} readonly stall ${turns} turns/${stall.run_length} calls (it=${stall.start_it ?? '-'}..${stall.end_it ?? '-'})${readonlyRole ? ' [readonly role]' : ''} top paths: ${stall.top_paths.map((p) => `${p.path}×${p.count}`).join(', ') || '-'}`,
1196
- session_id: stall.session_id,
1197
- });
1198
- }
1199
- for (const rep of (tools.identical_call_repeats || []).slice(0, 10)) {
1200
- issues.push({
1201
- severity: 'medium',
1202
- type: 'identical_call_repeat',
1203
- message: `identical ${rep.tool} call x${rep.count} (hash=${rep.args_hash}) at it=${rep.iterations.join(',')}`,
1204
- session_id: rep.session_id,
1205
- });
1206
- }
1207
- for (const g of routeGroups) {
1208
- // One-shot pseudo-sessions (native web search stamps `<caller>:native-search:<ts>`
1209
- // per send) have no loop, so full-WS/low-cache is their normal shape, not an issue.
1210
- if (/:native-search:/.test(String(g.session_id || ''))) continue;
1211
- if (g.ws_full > 0 && g.ws_delta === 0 && g.transport_rows > 0) {
1212
- 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 });
1213
- } else if (g.ws_full > 0) {
1214
- 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 });
1215
- }
1216
- // Sessions with <=2 turns (explorer-style ephemeral one-shots) have no
1217
- // prior turn to cache against; low ratio is structural, not actionable.
1218
- if (g.cache_ratio != null && g.prompt_tokens > 5000 && g.cache_ratio < 0.25 && (g.turns ?? 0) > 2) {
1219
- 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 });
1220
- }
1221
- }
1222
- for (const b of selectCacheBreakIssues(cache.actionable_cache_breaks, 10)) {
1223
- 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 });
1224
- }
1225
- for (const m of (cache.actual_cache_misses || []).slice(0, 10)) {
1226
- issues.push({ severity: 'high', type: 'cache_miss', message: `cache_miss ${m.reason || 'unknown'} at it=${m.iteration ?? '-'} uncached=${fmtTok(m.uncached_tokens)}`, session_id: m.session_id });
1227
- }
1228
- if (cache.service_tier_downgrades.length) {
1229
- const first = cache.service_tier_downgrades[0];
1230
- // priority->default is the backend's normal answer when the account/plan
1231
- // has no priority lane — expected, not actionable per session. Keep it
1232
- // visible as info-level; only other downgrade shapes stay medium.
1233
- const expected = cache.service_tier_downgrades.every((d) => d.requested === 'priority' && d.response === 'default');
1234
- issues.push({ severity: expected ? 'low' : 'medium', type: 'service_tier', message: `${cache.service_tier_downgrades.length} service tier downgrade(s), e.g. ${first.requested}->${first.response}${expected ? ' (expected: no priority lane on this account)' : ''}`, session_id: first.session_id });
1235
- }
1236
- if (tools.failures.length) {
1237
- const byTool = new Map();
1238
- for (const f of tools.failures) byTool.set(f.tool || '(unknown)', (byTool.get(f.tool || '(unknown)') || 0) + 1);
1239
- const summary = [...byTool.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([tool, count]) => `${tool}×${count}`).join(', ');
1240
- issues.push({ severity: 'medium', type: 'tool_error', message: `${tools.failures.length} failed tool call(s): ${summary}` });
1241
- }
1242
- for (const dup of tools.duplicates.slice(0, 5)) {
1243
- issues.push({ severity: 'low', type: 'duplicate_tool', message: `duplicate ${dup.tool} x${dup.count}: ${dup.target}`, args_hash: dup.args_hash });
1244
- }
1245
- for (const fail of tools.failed_repeats.slice(0, 5)) {
1246
- issues.push({ severity: 'medium', type: 'failed_tool_repeat', message: `repeated failing ${fail.tool} x${fail.error_count}: ${fail.target}`, args_hash: fail.args_hash });
1247
- }
1248
- for (const broad of tools.broad_results.slice(0, 5)) {
1249
- 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 });
1250
- }
1251
- for (const frag of tools.read_fragmentation.slice(0, 5)) {
1252
- issues.push({ severity: 'low', type: 'read_fragmentation', message: `read fragmentation x${frag.count} within ${frag.line_span} lines: ${frag.path}` });
1253
- }
1254
- if (tools.missed_parallelism_heuristic.consecutive_single_tool_batches >= 3) {
1255
- issues.push({ severity: 'low', type: 'missed_parallelism', message: `${tools.missed_parallelism_heuristic.consecutive_single_tool_batches} close consecutive single-tool batches` });
1256
- }
1257
- if (tools.sequential_tool_clusters?.length) {
1258
- const c = tools.sequential_tool_clusters[0];
1259
- const toolSummary = c.tools.map((x) => `${x.tool}×${x.count}`).join(', ');
1260
- issues.push({ severity: 'low', type: 'tool_churn_cluster', message: `sequential single-tool cluster x${c.count} over ${fmtMs(c.span_ms)}: ${toolSummary}` });
1261
- }
1262
- for (const e of (tools.edit_fragmentation || []).slice(0, 5)) {
1263
- if (e.frag_score < 2) continue;
1264
- issues.push({ severity: 'low', type: 'edit_fragmentation', message: `edit fragmentation x${e.frag_score} (multi-patch turns ${e.multi_patch_turns}, cross-turn ${e.cross_turn_patch}): ${e.agent || '-'}`, session_id: e.session_id });
1265
- }
1266
- const rank = { high: 0, medium: 1, low: 2 };
1267
- return issues.sort((a, b) => (rank[a.severity] ?? 9) - (rank[b.severity] ?? 9));
1268
- }
1269
-
1270
- function countBy(items, keyFn) {
1271
- const map = new Map();
1272
- for (const item of items || []) {
1273
- const key = keyFn(item);
1274
- map.set(key, (map.get(key) || 0) + 1);
1275
- }
1276
- return [...map.entries()].sort((a, b) => b[1] - a[1]);
1277
- }
1278
-
1279
- function buildWhySlowRankings(report) {
1280
- const ranks = [];
1281
- const topStream = report.stages.slowest_stream[0];
1282
- if (topStream && topStream.stream_ms > 0) {
1283
- 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)}` });
1284
- }
1285
- const topTool = report.stages.slowest_tools[0];
1286
- if (topTool && topTool.tool_ms > 0) {
1287
- const label = topTool.top_tools.map((x) => `${x.tool}×${x.count}/${fmtMs(x.ms)}`).join(', ');
1288
- 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}` });
1289
- }
1290
- if (report.tools.failures.length) {
1291
- const summary = countBy(report.tools.failures, (f) => f.category || f.tool || 'unknown').slice(0, 3).map(([k, v]) => `${k}×${v}`).join(', ');
1292
- ranks.push({ score: 50 + report.tools.failures.length, type: 'tool_failures', message: `${report.tools.failures.length} failed tool call(s): ${summary}` });
1293
- }
1294
- if (report.cache.actionable_cache_breaks.length) {
1295
- const summary = countBy(report.cache.actionable_cache_breaks, (b) => `${b.reason || 'unknown'}/${b.phase || 'unknown'}`).slice(0, 3).map(([k, v]) => `${k}×${v}`).join(', ');
1296
- ranks.push({ score: 40 + report.cache.actionable_cache_breaks.length, type: 'cache_breaks', message: `${report.cache.actionable_cache_breaks.length} actionable cache break(s): ${summary}` });
1297
- }
1298
- if (report.cache.actual_cache_misses?.length) {
1299
- const top = [...report.cache.actual_cache_misses].sort((a, b) => (b.uncached_tokens || 0) - (a.uncached_tokens || 0))[0];
1300
- ranks.push({ score: 60 + (top?.uncached_tokens || 0) / 1000, type: 'cache_miss', message: `${report.cache.actual_cache_misses.length} actual cache miss(es), top uncached=${fmtTok(top?.uncached_tokens || 0)} it=${top?.iteration ?? '-'}` });
1301
- }
1302
- const topGrowth = report.tokens.growth_turns[0];
1303
- if (topGrowth) {
1304
- 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)}` });
1305
- }
1306
- const topUncached = report.tokens.cache_miss_cost_turns[0];
1307
- if (topUncached) {
1308
- 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)}` });
1309
- }
1310
- if (report.tools.duplicates.length) {
1311
- const d = report.tools.duplicates[0];
1312
- ranks.push({ score: 10 + d.count, type: 'tool_churn', message: `${d.tool} repeated x${d.count} kinds=${fmtKindCounts(d.result_kinds)}: ${d.target}` });
1313
- }
1314
- return ranks.sort((a, b) => b.score - a.score).slice(0, 10);
1315
- }
1316
-
1317
- function buildExecutiveSummary(report) {
1318
- const lines = [];
1319
- const headers = report.summary.headers_ms || 0;
1320
- const stream = report.summary.llm_stream_ms || 0;
1321
- const tools = report.summary.total_tool_ms || 0;
1322
- const total = headers + stream + tools;
1323
- const dominant = [['stream', stream], ['tools', tools], ['headers', headers]].sort((a, b) => b[1] - a[1])[0];
1324
- lines.push(`\uC8FC \uBCD1\uBAA9=${dominant[0]} ${fmtMs(dominant[1])}/${fmtMs(total)}; cache=${fmtPct((report.summary.cache_ratio ?? 0) * 100)}; turns=${report.summary.turns}; tools=${report.summary.tool_calls}`);
1325
- if (report.cache.cache_breaks.length) {
1326
- const summary = countBy(report.cache.cache_breaks, (b) => `${b.reason || 'unknown'}/${b.phase || 'unknown'}`).slice(0, 3).map(([k, v]) => `${k}×${v}`).join(', ');
1327
- lines.push(`\uCE90\uC2DC \uAE68\uC9D0=${report.cache.cache_breaks.length} (${summary})`);
1328
- }
1329
- if (report.cache.actual_cache_misses?.length) {
1330
- const top = [...report.cache.actual_cache_misses].sort((a, b) => (b.uncached_tokens || 0) - (a.uncached_tokens || 0))[0];
1331
- lines.push(`\uC2E4\uC81C \uCE90\uC2DC \uBBF8\uC2A4=${report.cache.actual_cache_misses.length} (top uncached=${fmtTok(top?.uncached_tokens || 0)})`);
1332
- }
1333
- if (report.compact?.cache_breaks?.length) {
1334
- const summary = countBy(report.compact.cache_breaks, (b) => `${b.reason || 'unknown'}/${b.phase || 'unknown'}`).slice(0, 3).map(([k, v]) => `${k}×${v}`).join(', ');
1335
- lines.push(`\uCEF4\uD329\uD2B8 \uD638\uCD9C=${report.compact.sessions.length} session, cache reset=${report.compact.cache_breaks.length} (${summary})`);
1336
- }
1337
- if (report.tools.failures.length) {
1338
- const summary = countBy(report.tools.failures, (f) => f.category || f.tool || 'unknown').slice(0, 3).map(([k, v]) => `${k}×${v}`).join(', ');
1339
- lines.push(`\uD234 \uC2E4\uD328=${report.tools.failures.length} (${summary})`);
1340
- }
1341
- const topGrowth = report.tokens.growth_turns[0];
1342
- if (topGrowth) lines.push(`\uD1A0\uD070 \uC99D\uD3ED=${topGrowth.agent || '-'} it=${topGrowth.turn_label} promptΔ=${fmtTok(topGrowth.prompt_delta)} out=${fmtTok(topGrowth.output_tokens + topGrowth.thinking_tokens)}`);
1343
- const topChurn = report.tools.duplicates[0];
1344
- if (topChurn) lines.push(`\uD234 \uD5DB\uB3CE=${topChurn.tool}×${topChurn.count} ${compactText(topChurn.target, 90)}`);
1345
- return lines;
1346
- }
1347
-
1348
- function buildReport(rows, selectedIds, failureRows = []) {
1349
- const selected = rows.filter((r) => selectedIds.includes(sessionId(r)));
1350
- const selectedFailures = failureRows.filter((r) => selectedIds.includes(sessionId(r)));
1351
- const routeGroups = buildRouteGroups(selected);
1352
- const selectedCache = buildCacheDiagnostics(selected);
1353
- const tools = buildToolDiagnostics(selected, selectedFailures);
1354
- const stages = buildTurnDiagnostics(selected, routeGroups);
1355
- const tokens = buildTokenDiagnostics(stages.turns);
1356
- const compact = buildCompactDiagnostics(rows, selectedIds);
1357
- // Compact workers are normally sibling sessions rather than direct children,
1358
- // so their genuine mismatches are absent from `selected`. Promote only those
1359
- // actionable rows into the root report; intentional compact transitions stay
1360
- // isolated in compact diagnostics and selected compact children never duplicate.
1361
- const selectedSessionIds = new Set(selectedIds);
1362
- const compactActionableBreaks = compact.cache_breaks.filter(
1363
- (row) => row.actionable && !selectedSessionIds.has(row.session_id),
1364
- );
1365
- const cache = {
1366
- ...selectedCache,
1367
- cache_breaks: [...selectedCache.cache_breaks, ...compactActionableBreaks],
1368
- actionable_cache_breaks: [...selectedCache.actionable_cache_breaks, ...compactActionableBreaks],
1369
- };
1370
- const issues = buildIssues(routeGroups, cache, tools);
1371
- const tsValues = selected.map((r) => Number(r.ts || 0)).filter((n) => n > 0);
1372
- const report = {
1373
- trace: opts.trace || defaultTracePath(),
1374
- selected_sessions: selectedIds,
1375
- time_range: {
1376
- start_ts: tsValues.length ? Math.min(...tsValues) : null,
1377
- end_ts: tsValues.length ? Math.max(...tsValues) : null,
1378
- start: tsValues.length ? fmtTime(Math.min(...tsValues)) : null,
1379
- end: tsValues.length ? fmtTime(Math.max(...tsValues)) : null,
1380
- span_ms: tsValues.length ? Math.max(...tsValues) - Math.min(...tsValues) : 0,
1381
- },
1382
- summary: {
1383
- row_count: selected.length,
1384
- sessions: selectedIds.length,
1385
- turns: sum(routeGroups.map((g) => g.turns)),
1386
- tool_calls: tools.total_tool_calls,
1387
- total_tool_ms: tools.total_tool_ms,
1388
- llm_stream_ms: sum(routeGroups.map((g) => g.llm_stream_ms)),
1389
- headers_ms: sum(routeGroups.map((g) => g.headers_ms)),
1390
- cache_ratio: cache.usage_cache_ratio,
1391
- issues: issues.length,
1392
- },
1393
- routeGroups,
1394
- cache,
1395
- tools,
1396
- stages,
1397
- tokens,
1398
- compact,
1399
- issues,
1400
- selected_tool_failures: selectedFailures.length,
1401
- };
1402
- report.rankings = buildWhySlowRankings(report);
1403
- report.executive_summary = buildExecutiveSummary(report);
1404
- return report;
1405
- }
1406
-
1407
- function renderText(report) {
1408
- const lines = [];
1409
- const focused = opts.failuresOnly || opts.compactOnly || opts.tokensOnly || opts.slowOnly;
1410
- lines.push(`Session bench (${report.selected_sessions.map(shortId).join(', ')})`);
1411
- lines.push(`range: ${report.time_range.start || '-'} → ${report.time_range.end || '-'} (${fmtSec(report.time_range.span_ms)})`);
1412
- 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)}`);
1413
- lines.push('');
1414
-
1415
- if (!opts.issuesOnly && !opts.cacheOnly && !opts.toolsOnly && !opts.compactOnly && !opts.tokensOnly && !opts.failuresOnly) {
1416
- lines.push('Executive summary');
1417
- for (const line of report.executive_summary || []) lines.push(`- ${line}`);
1418
- if (report.rankings?.length) {
1419
- lines.push('why slow / risk ranking:');
1420
- for (const r of report.rankings.slice(0, Math.min(opts.limit, 8))) lines.push(`- [${r.type}] ${r.message}`);
1421
- }
1422
- lines.push('');
1423
- }
1424
-
1425
- const pushCompactDiagnostics = () => {
1426
- if (!(report.compact?.sessions?.length || report.compact?.recent_meta?.length || report.compact?.cache_breaks?.length)) {
1427
- lines.push('compact diagnostics: none');
1428
- return;
1429
- }
1430
- lines.push('compact diagnostics:');
1431
- if (report.compact.sessions.length) {
1432
- const ctable = [['session', 'turns', 'stream', 'prompt', 'out', 'cache', 'breaks', 'WS']];
1433
- for (const c of report.compact.sessions.slice(0, 5)) {
1434
- ctable.push([
1435
- shortId(c.session_id),
1436
- c.turns,
1437
- fmtMs(c.stream_ms),
1438
- fmtTok(c.prompt_tokens),
1439
- fmtTok(c.output_tokens + c.thinking_tokens),
1440
- fmtPct((c.cache_ratio ?? 0) * 100),
1441
- `${c.breaks}${c.no_anchor ? `/no_anchor×${c.no_anchor}` : ''}`,
1442
- `${c.delta_ws}Δ/${c.full_ws}F`,
1443
- ]);
1444
- }
1445
- lines.push(...padTable(ctable));
1446
- }
1447
- if (report.compact.recent_meta?.length) {
1448
- lines.push('compact meta:');
1449
- for (const m of report.compact.recent_meta.slice(0, Math.min(opts.limit, 10))) {
1450
- const pipe = m.recall_pipeline;
1451
- const fit = m.fit;
1452
- const parts = [
1453
- `- ${shortId(m.session_id)} it=${m.iteration ?? '-'} ${m.compact_type || '-'} ${fmtMs(m.duration_ms)} ${fmtTok(m.before_tokens)}→${fmtTok(m.after_tokens)}`,
1454
- `pressure=${fmtTok(m.pressure_tokens)}/${fmtTok(m.trigger_tokens)}/${fmtTok(m.boundary_tokens)}`,
1455
- `target=${fmtTok(m.target_budget_tokens)} changed=${m.changed}`,
1456
- ];
1457
- if (m.error) parts.push(`error=${compactText(m.error, 120)}`);
1458
- lines.push(parts.join(' '));
1459
- if (pipe) {
1460
- 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`);
1461
- }
1462
- if (fit) {
1463
- const recallPart = fit.recall_chars != null
1464
- ? ` recallChars=${fit.recall_chars} priorChars=${fit.prior_chars ?? 0} tailTrunc=${fit.tail_truncated}`
1465
- : '';
1466
- const semanticPart = fit.summary_repaired != null ? ` repaired=${fit.summary_repaired}` : '';
1467
- 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}`);
1468
- }
1469
- }
1470
- }
1471
- const intentionalCompactBreaks = report.compact.cache_breaks.filter((b) => !b.actionable);
1472
- const actionableCompactBreaks = report.compact.cache_breaks.filter((b) => b.actionable);
1473
- if (intentionalCompactBreaks.length) {
1474
- lines.push('compact cache resets (intentional):');
1475
- for (const b of intentionalCompactBreaks.slice(0, 5)) {
1476
- 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)}`);
1477
- }
1478
- }
1479
- if (actionableCompactBreaks.length) {
1480
- lines.push('compact cache breaks (actionable):');
1481
- for (const b of actionableCompactBreaks.slice(0, 5)) {
1482
- 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)}`);
1483
- }
1484
- }
1485
- };
1486
-
1487
- if (opts.compactOnly) {
1488
- pushCompactDiagnostics();
1489
- return lines.join('\n');
1490
- }
1491
-
1492
- if (!focused && !opts.toolsOnly && !opts.issuesOnly) {
1493
- lines.push('Route timeline');
1494
- const table = [['agent', 'model', 'turns', 'wall', 'LLM', 'tools', 'tool_ms', 'cache', 'WS', 'reused', 'session']];
1495
- for (const g of report.routeGroups) {
1496
- table.push([
1497
- g.agent || '-',
1498
- `${g.provider || '?'}:${g.model || '?'}`,
1499
- g.turns,
1500
- fmtSec((g.max_ts || 0) - (g.min_ts || 0)),
1501
- fmtMs(g.llm_stream_ms),
1502
- g.tool_calls,
1503
- fmtMs(g.tool_ms),
1504
- fmtPct((g.cache_ratio ?? 0) * 100),
1505
- `${g.ws_delta}Δ/${g.ws_full}F`,
1506
- `${g.reused_connection}/${g.transport_rows}`,
1507
- shortId(g.session_id),
1508
- ]);
1509
- }
1510
- lines.push(...padTable(table));
1511
- lines.push('');
1512
- }
1513
-
1514
- if (!focused && !opts.toolsOnly && !opts.issuesOnly) {
1515
- lines.push('Cache / transport');
1516
- lines.push(`cache: ${fmtTok(report.cache.cached_tokens)} / ${fmtTok(report.cache.prompt_tokens)} (${fmtPct((report.cache.usage_cache_ratio ?? 0) * 100)})`);
1517
- 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}`);
1518
- 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(', ')}`);
1519
- if (report.cache.cache_breaks.length) {
1520
- lines.push(`cache breaks (raw=${report.cache.cache_breaks.length}, actionable=${report.cache.actionable_cache_breaks.length}):`);
1521
- for (const b of report.cache.cache_breaks.slice(0, 10)) {
1522
- lines.push(`- ${shortId(b.session_id)} it=${b.iteration ?? '-'} ${b.actionable ? 'actionable' : `intentional:${b.intentional_transition}`} phase=${b.phase || '-'} reason=${b.reason || '-'} (${b.explanation || '-'}) ws=${b.ws_mode || '-'} tool_choice=${b.request_tool_choice ?? '-'} 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 ?? '-'}`);
1523
- }
1524
- }
1525
- if (report.cache.actual_cache_misses?.length) {
1526
- lines.push('actual cache misses:');
1527
- for (const m of report.cache.actual_cache_misses.slice(0, 10)) {
1528
- lines.push(`- ${shortId(m.session_id)} it=${m.iteration ?? '-'} reason=${m.reason || '-'} ws=${m.ws_mode || '-'} prev=${m.request_has_previous_response_id} cache=${fmtPct((m.cache_ratio ?? 0) * 100)} prompt=${fmtTok(m.prompt_tokens)} uncached=${fmtTok(m.uncached_tokens)} prevMax=${fmtTok(m.previous_max_cached_tokens)}`);
1529
- }
1530
- }
1531
- if (report.cache.service_tier_downgrades.length) {
1532
- lines.push('service tier downgrades:');
1533
- 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}`);
1534
- }
1535
- if (report.compact?.sessions?.length || report.compact?.recent_meta?.length) {
1536
- pushCompactDiagnostics();
1537
- }
1538
- lines.push('');
1539
- }
1540
-
1541
- if ((opts.slowOnly || !focused) && !opts.cacheOnly && !opts.toolsOnly && !opts.issuesOnly && !opts.failuresOnly && !opts.tokensOnly) {
1542
- lines.push('Slow turns / stage breakdown');
1543
- const table = [['agent', 'it', 'active', 'headers', 'stream', 'tools', 'cache', 'ws', 'prompt', 'out', 'flags']];
1544
- for (const t of report.stages.slowest_active.slice(0, Math.min(opts.limit, 12))) {
1545
- table.push([
1546
- t.agent || '-',
1547
- t.turn_label || (t.iteration ?? '-'),
1548
- fmtMs(t.approx_active_ms),
1549
- fmtMs(t.headers_ms),
1550
- fmtMs(t.stream_ms),
1551
- `${fmtMs(t.tool_ms)}/${t.tool_calls}`,
1552
- fmtPct((t.cache_ratio ?? 0) * 100),
1553
- `${t.ws_mode || '-'}${t.reused_connection === true ? '+reuse' : ''}`,
1554
- fmtTok(t.prompt_tokens),
1555
- fmtTok(t.output_tokens),
1556
- t.flags.join(',') || '-',
1557
- ]);
1558
- }
1559
- lines.push(...padTable(table));
1560
- if (report.stages.slowest_tools.some((t) => t.tool_ms > 0)) {
1561
- lines.push('slow tool turns:');
1562
- for (const t of report.stages.slowest_tools.slice(0, 5)) {
1563
- const toolLabel = t.top_tools.map((x) => `${x.tool}×${x.count}/${fmtMs(x.ms)}`).join(', ') || '-';
1564
- lines.push(`- ${t.agent || '-'} it=${t.turn_label || (t.iteration ?? '-')} tools=${fmtMs(t.tool_ms)} calls=${t.tool_calls}: ${toolLabel}`);
1565
- }
1566
- }
1567
- lines.push('');
1568
- }
1569
-
1570
- if ((opts.tokensOnly || !focused) && !opts.cacheOnly && !opts.toolsOnly && !opts.issuesOnly && !opts.failuresOnly && !opts.slowOnly) {
1571
- lines.push('Token amplification');
1572
- const sessionTable = [['agent', 'turns', 'prompt first→last', 'max', 'Δprompt', 'out', 'uncached', 'cache', 'session']];
1573
- for (const s of report.tokens.sessions.slice(0, Math.min(opts.limit, 8))) {
1574
- sessionTable.push([
1575
- s.agent || '-',
1576
- s.turns,
1577
- `${fmtTok(s.first_prompt)}→${fmtTok(s.last_prompt)}`,
1578
- `${fmtTok(s.max_prompt)}@${s.max_prompt_it}`,
1579
- fmtTok(s.prompt_growth),
1580
- fmtTok(s.total_output + s.total_thinking),
1581
- fmtTok(s.uncached_tokens),
1582
- fmtPct((s.cache_ratio ?? 0) * 100),
1583
- shortId(s.session_id),
1584
- ]);
1585
- }
1586
- lines.push(...padTable(sessionTable));
1587
- if (report.tokens.growth_turns.length) {
1588
- lines.push('prompt growth spikes:');
1589
- for (const t of report.tokens.growth_turns.slice(0, 8)) {
1590
- const ratio = t.growth_per_output == null ? '-' : `${t.growth_per_output.toFixed(t.growth_per_output >= 10 ? 0 : 1)}x/out`;
1591
- 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)}`);
1592
- }
1593
- }
1594
- if (report.tokens.output_heavy_turns.length) {
1595
- lines.push('large output turns:');
1596
- for (const t of report.tokens.output_heavy_turns.slice(0, 5)) {
1597
- 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)}`);
1598
- }
1599
- }
1600
- if (report.tokens.cache_miss_cost_turns.length) {
1601
- lines.push('uncached prompt cost:');
1602
- for (const t of report.tokens.cache_miss_cost_turns.slice(0, 5)) {
1603
- 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)}`);
1604
- }
1605
- }
1606
- lines.push('');
1607
- }
1608
-
1609
- if ((opts.toolsOnly || opts.failuresOnly || !focused) && !opts.cacheOnly && !opts.issuesOnly && !opts.tokensOnly && !opts.slowOnly) {
1610
- lines.push('Tool diagnostics');
1611
- lines.push(`tool result kinds: ${fmtKindCounts(report.tools.result_kinds, 6)}`);
1612
- const table = [['tool', 'count', 'ok/err', 'total', 'p50', 'p95', 'kinds', 'result']];
1613
- for (const t of report.tools.by_name.slice(0, opts.limit)) {
1614
- table.push([
1615
- t.tool,
1616
- t.count,
1617
- `${t.ok}/${t.errors}`,
1618
- fmtMs(t.total_ms),
1619
- fmtMs(t.p50_ms),
1620
- fmtMs(t.p95_ms),
1621
- fmtKindCounts(t.result_kinds),
1622
- `${Math.round(t.bytes / 1024)}KB/${t.lines}l`,
1623
- ]);
1624
- }
1625
- lines.push(...padTable(table));
1626
- if (report.tools.failures.length) {
1627
- lines.push('tool failures:');
1628
- for (const f of report.tools.failures.slice(0, 10)) {
1629
- lines.push(`- ${f.agent || '-'} it=${f.iteration ?? '-'} ${f.tool || '-'} ${fmtMs(f.tool_ms)} category=${f.category || '-'} reason=${f.reason || 'trace\uC5D0 \uC0C1\uC138 stderr/\uC608\uC678 \uBBF8\uC800\uC7A5'} result=${Math.round(f.bytes / 1024)}KB/${f.lines}l: ${f.target}`);
1630
- if (opts.failuresOnly && f.preview) {
1631
- const preview = compactText(f.preview, 700);
1632
- lines.push(` preview: ${preview}`);
1633
- }
1634
- }
1635
- }
1636
- if (report.tools.recent_successes.length) {
1637
- lines.push('recent successful tools:');
1638
- for (const s of report.tools.recent_successes.slice(0, 5)) {
1639
- lines.push(`- ${s.agent || '-'} it=${s.iteration ?? '-'} ${s.tool || '-'} ${fmtMs(s.tool_ms)} kind=${s.result_kind}: ${s.target}`);
1640
- }
1641
- }
1642
- if (report.tools.duplicates.length) {
1643
- lines.push('tool churn / duplicates:');
1644
- for (const d of report.tools.duplicates.slice(0, 10)) lines.push(`- ${d.tool} x${d.count} kinds=${fmtKindCounts(d.result_kinds)}: ${d.target}`);
1645
- }
1646
- if (report.tools.broad_results.length) {
1647
- lines.push('broad/offloaded results:');
1648
- 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}`);
1649
- }
1650
- if (report.tools.read_fragmentation.length) {
1651
- lines.push('read fragmentation:');
1652
- for (const f of report.tools.read_fragmentation.slice(0, 10)) lines.push(`- x${f.count} span=${f.line_span} lines: ${f.path}`);
1653
- }
1654
- if (report.tools.sequential_tool_clusters?.length) {
1655
- lines.push('sequential single-tool clusters:');
1656
- for (const c of report.tools.sequential_tool_clusters.slice(0, 8)) {
1657
- const toolSummary = c.tools.map((x) => `${x.tool}×${x.count}`).join(', ');
1658
- 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}`);
1659
- if (c.examples?.length) lines.push(` e.g. ${c.examples.join(' | ')}`);
1660
- }
1661
- }
1662
- if (report.tools.edit_fragmentation?.length) {
1663
- lines.push('edit fragmentation:');
1664
- for (const e of report.tools.edit_fragmentation.slice(0, 8)) {
1665
- lines.push(`- ${e.agent || '-'} it=${e.start_it}→${e.end_it} score=${e.frag_score} multi=${e.multi_patch_turns} cross=${e.cross_turn_patch}`);
1666
- }
1667
- }
1668
- lines.push(`missed parallelism heuristic: ${report.tools.missed_parallelism_heuristic.consecutive_single_tool_batches} close single-tool batches`);
1669
- lines.push('');
1670
- }
1671
-
1672
- if (!opts.cacheOnly && !opts.toolsOnly) {
1673
- lines.push('Issues');
1674
- if (!report.issues.length) lines.push('- none detected by current heuristics');
1675
- for (const issue of report.issues.slice(0, opts.limit)) lines.push(`- [${issue.severity}] ${issue.type}: ${issue.message}`);
1676
- }
1677
- return lines.join('\n');
1678
- }
1679
-
1680
- const tracePath = opts.trace ? resolve(opts.trace) : defaultTracePath();
1681
- const failurePath = defaultToolFailurePath(tracePath);
1682
- let rows = readRows(tracePath);
1683
- if (opts.since != null) rows = rows.filter((r) => Number(r.ts || 0) >= opts.since);
1684
- rows = filterAgent(rows, opts.agent);
1685
-
1686
- let failureRows = readRows(failurePath);
1687
- if (opts.since != null) failureRows = failureRows.filter((r) => Number(r.ts || 0) >= opts.since);
1688
- failureRows = filterAgent(failureRows, opts.agent);
1689
-
1690
- const bySession = groupBy(rows.filter((r) => sessionId(r)), sessionId);
1691
- const metas = [...bySession.entries()].map(([sid, srows]) => ({ ...inferSessionMeta(srows), session_id: sid }))
1692
- .filter((m) => m.max_ts != null)
1693
- .sort((a, b) => Number(b.max_ts || 0) - Number(a.max_ts || 0));
1694
-
1695
- const selectedIds = selectSessionIds(metas, opts.session);
1696
- if (!selectedIds.length) {
1697
- 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) })) };
1698
- if (opts.json) console.log(JSON.stringify(fallback, null, 2));
1699
- else {
1700
- console.error(fallback.error);
1701
- for (const s of fallback.sessions_seen) console.error(`- ${shortId(s.session_id)} ${s.agent || '-'} ${s.model || '-'} ${s.last}`);
1702
- }
1703
- process.exitCode = 1;
1704
- } else {
1705
- const report = buildReport(rows, selectedIds, failureRows);
1706
- if (opts.json) console.log(JSON.stringify(report, null, 2));
1707
- else console.log(renderText(report));
1708
- }