mixdog 0.9.62 → 0.9.64

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 (403) 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/update-checker.mjs +1 -1
  187. package/src/runtime/shared/user-cwd.mjs +3 -3
  188. package/src/runtime/shared/user-data-guard.mjs +4 -4
  189. package/src/runtime/shared/workspace-router.mjs +5 -5
  190. package/src/session-runtime/config-helpers.mjs +2 -3
  191. package/src/session-runtime/config-lifecycle.mjs +1 -1
  192. package/src/session-runtime/hitch-profile.mjs +1 -1
  193. package/src/session-runtime/lifecycle-api.mjs +6 -4
  194. package/src/session-runtime/model-recency.mjs +5 -5
  195. package/src/session-runtime/plugin-mcp.mjs +1 -1
  196. package/src/session-runtime/provider-request-snapshot.mjs +338 -0
  197. package/src/session-runtime/session-text.mjs +4 -4
  198. package/src/session-runtime/tool-catalog-schema.mjs +197 -0
  199. package/src/session-runtime/tool-catalog.mjs +6 -483
  200. package/src/session-runtime/tool-defs.mjs +1 -1
  201. package/src/standalone/agent-task-status.mjs +1 -1
  202. package/src/standalone/agent-tool/helpers.mjs +2 -2
  203. package/src/standalone/channel-admin.mjs +4 -4
  204. package/src/standalone/explore-tool.mjs +3 -3
  205. package/src/standalone/hook-bus/config.mjs +1 -1
  206. package/src/standalone/hook-bus/constants.mjs +1 -1
  207. package/src/standalone/hook-bus/rules.mjs +1 -1
  208. package/src/standalone/projects.mjs +1 -1
  209. package/src/standalone/provider-admin.mjs +2 -2
  210. package/src/tui/app/app-format.mjs +2 -2
  211. package/src/tui/app/clipboard.mjs +3 -3
  212. package/src/tui/app/input-parsers.mjs +1 -1
  213. package/src/tui/app/text-layout.mjs +1 -1
  214. package/src/tui/app/transcript-row-estimate.mjs +360 -0
  215. package/src/tui/app/transcript-window.mjs +5 -347
  216. package/src/tui/app/use-mouse-input.mjs +1 -1
  217. package/src/tui/app/use-prompt-handlers.mjs +1 -1
  218. package/src/tui/components/tool-execution/surface-detail.mjs +9 -9
  219. package/src/tui/components/tool-execution/text-format.mjs +2 -2
  220. package/src/tui/components/tool-output-format.mjs +7 -7
  221. package/src/tui/display-width.mjs +4 -4
  222. package/src/tui/dist/index.mjs +63 -97
  223. package/src/tui/engine/agent-envelope.mjs +5 -5
  224. package/src/tui/engine/notification-plan.mjs +1 -1
  225. package/src/tui/engine/queue-helpers.mjs +3 -3
  226. package/src/tui/engine/render-timing.mjs +4 -5
  227. package/src/tui/engine/session-api-ext.mjs +3 -3
  228. package/src/tui/engine/tool-result-status.mjs +5 -5
  229. package/src/tui/engine/tool-result-text.mjs +1 -1
  230. package/src/tui/engine/transcript-spill.mjs +585 -0
  231. package/src/tui/engine/tui-steering-persist.mjs +1 -1
  232. package/src/tui/engine.mjs +2 -474
  233. package/src/tui/lib/voice-setup.mjs +1 -1
  234. package/src/tui/markdown/format-token.mjs +1 -1
  235. package/src/tui/markdown/render-ansi.mjs +1 -1
  236. package/src/tui/markdown/table-layout.mjs +4 -4
  237. package/src/tui/paste-attachments.mjs +1 -1
  238. package/src/tui/prompt-history-store.mjs +3 -3
  239. package/src/tui/statusline-ansi-bridge.mjs +2 -2
  240. package/src/tui/theme.mjs +1 -1
  241. package/src/tui/transcript-tool-failures.mjs +2 -2
  242. package/src/ui/ansi.mjs +0 -9
  243. package/src/ui/statusline-format.mjs +2 -4
  244. package/scripts/_bench-cwc.json +0 -20
  245. package/scripts/_jitter-fuzz.jsx +0 -42
  246. package/scripts/_jitter-fuzz2.jsx +0 -30
  247. package/scripts/_jitter-probe.jsx +0 -35
  248. package/scripts/_jp2.jsx +0 -16
  249. package/scripts/_smoke_wd.mjs +0 -7
  250. package/scripts/abort-recovery-test.mjs +0 -175
  251. package/scripts/agent-dispatch-abort-compose-test.mjs +0 -31
  252. package/scripts/agent-live-arg-guard-smoke.mjs +0 -20
  253. package/scripts/agent-live-path-suffix-smoke.mjs +0 -34
  254. package/scripts/agent-live-toolcall-args-smoke.mjs +0 -45
  255. package/scripts/agent-loop-policy-test.mjs +0 -37
  256. package/scripts/agent-model-liveness-test.mjs +0 -763
  257. package/scripts/agent-parallel-smoke.mjs +0 -435
  258. package/scripts/agent-route-batch-test.mjs +0 -40
  259. package/scripts/agent-tag-reuse-smoke.mjs +0 -441
  260. package/scripts/agent-terminal-reap-test.mjs +0 -252
  261. package/scripts/agent-trace-io-test.mjs +0 -133
  262. package/scripts/ansi-color-capability-test.mjs +0 -90
  263. package/scripts/anthropic-admission-retry-integration-test.mjs +0 -119
  264. package/scripts/anthropic-maxtokens-test.mjs +0 -119
  265. package/scripts/anthropic-oauth-refresh-race-test.mjs +0 -397
  266. package/scripts/anthropic-transport-policy-test.mjs +0 -726
  267. package/scripts/apply-patch-edit-smoke.mjs +0 -71
  268. package/scripts/arg-guard-test.mjs +0 -93
  269. package/scripts/async-notify-settlement-test.mjs +0 -99
  270. package/scripts/atomic-lock-tryonce-test.mjs +0 -125
  271. package/scripts/background-task-meta-smoke.mjs +0 -38
  272. package/scripts/bench-run.mjs +0 -508
  273. package/scripts/boot-smoke.mjs +0 -137
  274. package/scripts/build-runtime-windows.ps1 +0 -242
  275. package/scripts/channel-daemon-smoke.mjs +0 -1103
  276. package/scripts/code-graph-aggregate-cwd-test.mjs +0 -158
  277. package/scripts/code-graph-disk-hit-test.mjs +0 -290
  278. package/scripts/code-graph-dispatch-test.mjs +0 -96
  279. package/scripts/code-graph-root-federation-test.mjs +0 -273
  280. package/scripts/compact-active-turn-test.mjs +0 -68
  281. package/scripts/compact-file-reattach-test.mjs +0 -88
  282. package/scripts/compact-pressure-test.mjs +0 -788
  283. package/scripts/compact-prior-context-flatten-test.mjs +0 -252
  284. package/scripts/compact-recall-digest-test.mjs +0 -57
  285. package/scripts/compact-smoke.mjs +0 -1017
  286. package/scripts/compact-trigger-migration-smoke.mjs +0 -261
  287. package/scripts/compacted-placeholder-scrub-test.mjs +0 -63
  288. package/scripts/context-mcp-metering-test.mjs +0 -1406
  289. package/scripts/debounced-skills-async-save-test.mjs +0 -57
  290. package/scripts/deferred-tool-loading-test.mjs +0 -250
  291. package/scripts/desktop-session-bridge-test.mjs +0 -1013
  292. package/scripts/dispatch-persist-recovery-test.mjs +0 -141
  293. package/scripts/embedding-worker-exit-test.mjs +0 -76
  294. package/scripts/execution-completion-dedup-test.mjs +0 -205
  295. package/scripts/execution-pending-resume-kick-test.mjs +0 -151
  296. package/scripts/execution-resume-esc-integration-test.mjs +0 -176
  297. package/scripts/explore-bench-tmp.mjs +0 -36
  298. package/scripts/explore-bench.mjs +0 -248
  299. package/scripts/explore-prompt-policy-test.mjs +0 -256
  300. package/scripts/explore-timeout-cancel-test.mjs +0 -345
  301. package/scripts/find-fuzzy-hidden-test.mjs +0 -267
  302. package/scripts/forwarder-rebind-tail-test.mjs +0 -67
  303. package/scripts/freevar-smoke.mjs +0 -98
  304. package/scripts/gemini-provider-test.mjs +0 -1448
  305. package/scripts/generate-oc-icons.mjs +0 -11
  306. package/scripts/grok-oauth-refresh-race-test.mjs +0 -273
  307. package/scripts/headless-pristine-execution-test.mjs +0 -614
  308. package/scripts/hook-bus-test.mjs +0 -552
  309. package/scripts/ingest-pure-conversation-smoke.mjs +0 -175
  310. package/scripts/internal-comms-bench-test.mjs +0 -226
  311. package/scripts/internal-comms-bench.mjs +0 -853
  312. package/scripts/internal-comms-smoke.mjs +0 -277
  313. package/scripts/internal-tools-normalization-test.mjs +0 -62
  314. package/scripts/interrupted-turn-history-test.mjs +0 -399
  315. package/scripts/lead-workflow-smoke.mjs +0 -592
  316. package/scripts/lifecycle-api-test.mjs +0 -137
  317. package/scripts/live-share-test.mjs +0 -148
  318. package/scripts/live-worker-smoke.mjs +0 -333
  319. package/scripts/log-writer-guard-smoke.mjs +0 -131
  320. package/scripts/maintenance-default-routes-test.mjs +0 -164
  321. package/scripts/max-output-recovery-persist-test.mjs +0 -81
  322. package/scripts/max-output-recovery-test.mjs +0 -368
  323. package/scripts/mcp-client-normalization-test.mjs +0 -45
  324. package/scripts/mcp-grace-deferred-test.mjs +0 -225
  325. package/scripts/memory-core-input-test.mjs +0 -167
  326. package/scripts/memory-cycle-routing-test.mjs +0 -111
  327. package/scripts/memory-meta-concurrency-test.mjs +0 -20
  328. package/scripts/memory-pg-recovery-test.mjs +0 -59
  329. package/scripts/memory-rule-contract-test.mjs +0 -93
  330. package/scripts/model-list-sanitize-test.mjs +0 -37
  331. package/scripts/mouse-tracking-restore-smoke.mjs +0 -107
  332. package/scripts/native-edit-wire-test.mjs +0 -152
  333. package/scripts/notify-completion-mirror-test.mjs +0 -73
  334. package/scripts/openai-oauth-refresh-race-test.mjs +0 -120
  335. package/scripts/openai-oauth-ws-1006-retry-test.mjs +0 -954
  336. package/scripts/openai-ws-early-settle-test.mjs +0 -216
  337. package/scripts/output-style-bench.mjs +0 -292
  338. package/scripts/output-style-smoke.mjs +0 -145
  339. package/scripts/parent-abort-link-test.mjs +0 -90
  340. package/scripts/patch-binary-cache-test.mjs +0 -181
  341. package/scripts/path-suffix-test.mjs +0 -57
  342. package/scripts/pending-completion-drop-test.mjs +0 -239
  343. package/scripts/pending-messages-lock-nonblocking-test.mjs +0 -62
  344. package/scripts/pretool-ask-runtime-test.mjs +0 -54
  345. package/scripts/process-lifecycle-test.mjs +0 -461
  346. package/scripts/prompt-immediate-render-test.mjs +0 -89
  347. package/scripts/prompt-input-parity-test.mjs +0 -145
  348. package/scripts/provider-admission-scheduler-test.mjs +0 -681
  349. package/scripts/provider-contract-test.mjs +0 -632
  350. package/scripts/provider-stream-stall-test.mjs +0 -276
  351. package/scripts/provider-toolcall-test.mjs +0 -3063
  352. package/scripts/reactive-compact-persist-smoke.mjs +0 -203
  353. package/scripts/recall-bench.mjs +0 -359
  354. package/scripts/resource-admission-test.mjs +0 -792
  355. package/scripts/result-classification-test.mjs +0 -75
  356. package/scripts/rg-runner-test.mjs +0 -240
  357. package/scripts/routing-corpus-test.mjs +0 -349
  358. package/scripts/sanitize-tool-pairs-test.mjs +0 -260
  359. package/scripts/session-bench-cache-break-test.mjs +0 -102
  360. package/scripts/session-bench.mjs +0 -1708
  361. package/scripts/session-context-bench.mjs +0 -344
  362. package/scripts/session-heartbeat-lifecycle-test.mjs +0 -68
  363. package/scripts/session-ingest-compaction-smoke.mjs +0 -279
  364. package/scripts/session-ingest-smoke.mjs +0 -177
  365. package/scripts/session-orphan-sweep-test.mjs +0 -109
  366. package/scripts/set-effort-config-test.mjs +0 -41
  367. package/scripts/shell-failure-diagnostics-test.mjs +0 -280
  368. package/scripts/shell-hardening-test.mjs +0 -209
  369. package/scripts/shell-jobs-windows-hide-test.mjs +0 -164
  370. package/scripts/ship-mode-test.mjs +0 -98
  371. package/scripts/smoke-loop-failure-summary-test.mjs +0 -38
  372. package/scripts/smoke-loop-failure-summary.mjs +0 -16
  373. package/scripts/smoke-loop-report.mjs +0 -221
  374. package/scripts/smoke-loop.mjs +0 -202
  375. package/scripts/smoke-runtime-negative.ps1 +0 -106
  376. package/scripts/statusline-quota-hysteresis-test.mjs +0 -62
  377. package/scripts/steering-drain-buckets-test.mjs +0 -467
  378. package/scripts/steering-fold-provenance-test.mjs +0 -71
  379. package/scripts/streaming-tail-window-test.mjs +0 -175
  380. package/scripts/submit-commandbusy-race-test.mjs +0 -257
  381. package/scripts/task-bench.mjs +0 -205
  382. package/scripts/terminal-bench-isolation-guards-test.mjs +0 -102
  383. package/scripts/termio-input-smoke.mjs +0 -199
  384. package/scripts/tool-result-hook-test.mjs +0 -48
  385. package/scripts/tool-smoke.mjs +0 -2453
  386. package/scripts/tool-tui-presentation-test.mjs +0 -286
  387. package/scripts/toolcall-args-test.mjs +0 -239
  388. package/scripts/tui-ambiguous-width-test.mjs +0 -113
  389. package/scripts/tui-background-failure-smoke.mjs +0 -73
  390. package/scripts/tui-perf-run.ps1 +0 -26
  391. package/scripts/tui-render-smoke.mjs +0 -90
  392. package/scripts/tui-runtime-load-bench-entry.jsx +0 -608
  393. package/scripts/tui-runtime-load-bench.mjs +0 -50
  394. package/scripts/tui-store-frame-batch-test.mjs +0 -99
  395. package/scripts/tui-transcript-jitter-harness-entry.jsx +0 -383
  396. package/scripts/tui-transcript-perf-test.mjs +0 -934
  397. package/scripts/usage-metrics-epoch-smoke.mjs +0 -114
  398. package/scripts/verify-release-assets-test.mjs +0 -281
  399. package/scripts/web-fetch-routing-test.mjs +0 -158
  400. package/scripts/webhook-smoke.mjs +0 -201
  401. package/scripts/windows-hide-spawn-options-test.mjs +0 -19
  402. package/scripts/worker-notify-rejection-test.mjs +0 -51
  403. 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
- }