@selesai/code 0.9.15 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +6 -9
  3. package/dist/defaults/models.json +2 -2
  4. package/dist/defaults/settings.json +3 -7
  5. package/dist/extensions/package.json +2 -4
  6. package/dist/extensions/pi-zentui/.editorconfig +12 -0
  7. package/dist/extensions/pi-zentui/.github/CODEOWNERS +1 -0
  8. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/bug_report.yml +110 -0
  9. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/config.yml +8 -0
  10. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/config_question.yml +56 -0
  11. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/feature_request.yml +63 -0
  12. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/rendering_issue.yml +106 -0
  13. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/runtime_detection.yml +103 -0
  14. package/dist/extensions/pi-zentui/.github/dependabot.yml +47 -0
  15. package/dist/extensions/pi-zentui/.github/pull_request_template.md +34 -0
  16. package/dist/extensions/pi-zentui/.github/release.yml +18 -0
  17. package/dist/extensions/pi-zentui/.github/workflows/ci.yml +79 -0
  18. package/dist/extensions/pi-zentui/.github/workflows/publish.yml +63 -0
  19. package/dist/extensions/pi-zentui/AGENTS.md +107 -0
  20. package/dist/extensions/pi-zentui/CONTRIBUTING.md +68 -0
  21. package/dist/extensions/pi-zentui/LICENSE +21 -0
  22. package/dist/extensions/pi-zentui/README.md +188 -0
  23. package/dist/extensions/pi-zentui/SECURITY.md +18 -0
  24. package/dist/extensions/pi-zentui/assets/main-cover.png +0 -0
  25. package/dist/extensions/pi-zentui/assets/screenshots/editor-accent-rail.png +0 -0
  26. package/dist/extensions/pi-zentui/assets/screenshots/editor-minimalist.png +0 -0
  27. package/dist/extensions/pi-zentui/assets/screenshots/editor-opencode-copy-friendly.png +0 -0
  28. package/dist/extensions/pi-zentui/assets/screenshots/editor-opencode.png +0 -0
  29. package/dist/extensions/pi-zentui/assets/screenshots/user-message-compact.png +0 -0
  30. package/dist/extensions/pi-zentui/assets/screenshots/user-message-framed-copy-friendly.png +0 -0
  31. package/dist/extensions/pi-zentui/assets/screenshots/user-message-framed.png +0 -0
  32. package/dist/extensions/pi-zentui/assets/screenshots/user-message-labeled.png +0 -0
  33. package/dist/extensions/pi-zentui/assets/zentui.png +0 -0
  34. package/dist/extensions/pi-zentui/biome.json +31 -0
  35. package/dist/extensions/pi-zentui/docs/configuration.md +509 -0
  36. package/dist/extensions/pi-zentui/docs/footer-format.md +162 -0
  37. package/dist/extensions/pi-zentui/extensions/zentui/accent-rail-editor.ts +99 -0
  38. package/dist/extensions/pi-zentui/extensions/zentui/accent-rail-layout-patch.ts +457 -0
  39. package/dist/extensions/pi-zentui/extensions/zentui/completion-menu.ts +157 -0
  40. package/dist/extensions/pi-zentui/extensions/zentui/config.ts +2100 -0
  41. package/dist/extensions/pi-zentui/extensions/zentui/editor-metadata-format.ts +321 -0
  42. package/dist/extensions/pi-zentui/extensions/zentui/editor-transfer.ts +73 -0
  43. package/dist/extensions/pi-zentui/extensions/zentui/extension-status.ts +89 -0
  44. package/dist/extensions/pi-zentui/extensions/zentui/footer-format.ts +364 -0
  45. package/dist/extensions/pi-zentui/extensions/zentui/footer-layout.ts +122 -0
  46. package/dist/extensions/pi-zentui/extensions/zentui/footer.ts +812 -0
  47. package/dist/extensions/pi-zentui/extensions/zentui/format.ts +568 -0
  48. package/dist/extensions/pi-zentui/extensions/zentui/git.ts +384 -0
  49. package/dist/extensions/pi-zentui/extensions/zentui/icons.ts +252 -0
  50. package/dist/extensions/pi-zentui/extensions/zentui/index.ts +1406 -0
  51. package/dist/extensions/pi-zentui/extensions/zentui/interaction-summary.ts +1049 -0
  52. package/dist/extensions/pi-zentui/extensions/zentui/live-context.ts +75 -0
  53. package/dist/extensions/pi-zentui/extensions/zentui/minimalist-editor.ts +463 -0
  54. package/dist/extensions/pi-zentui/extensions/zentui/package-version.ts +651 -0
  55. package/dist/extensions/pi-zentui/extensions/zentui/project-refresh.ts +120 -0
  56. package/dist/extensions/pi-zentui/extensions/zentui/project-state.ts +59 -0
  57. package/dist/extensions/pi-zentui/extensions/zentui/prototype-patch-registry.ts +166 -0
  58. package/dist/extensions/pi-zentui/extensions/zentui/repository-root.ts +98 -0
  59. package/dist/extensions/pi-zentui/extensions/zentui/runtime.ts +841 -0
  60. package/dist/extensions/pi-zentui/extensions/zentui/selector-border.ts +93 -0
  61. package/dist/extensions/pi-zentui/extensions/zentui/session-lifecycle.ts +60 -0
  62. package/dist/extensions/pi-zentui/extensions/zentui/settings-command.ts +1796 -0
  63. package/dist/extensions/pi-zentui/extensions/zentui/settings-previews.ts +128 -0
  64. package/dist/extensions/pi-zentui/extensions/zentui/state.ts +94 -0
  65. package/dist/extensions/pi-zentui/extensions/zentui/style.ts +424 -0
  66. package/dist/extensions/pi-zentui/extensions/zentui/telemetry.ts +82 -0
  67. package/dist/extensions/pi-zentui/extensions/zentui/ui.ts +1290 -0
  68. package/dist/extensions/pi-zentui/extensions/zentui/user-message-osc.ts +248 -0
  69. package/dist/extensions/pi-zentui/extensions/zentui/user-message-styles.ts +215 -0
  70. package/dist/extensions/pi-zentui/extensions/zentui/user-message.ts +206 -0
  71. package/dist/extensions/pi-zentui/extensions/zentui/working-line-messages.ts +24 -0
  72. package/dist/extensions/pi-zentui/extensions/zentui/working-line-spinners.ts +54 -0
  73. package/dist/extensions/pi-zentui/extensions/zentui/working-line.ts +1433 -0
  74. package/dist/extensions/pi-zentui/package-lock.json +5311 -0
  75. package/dist/extensions/pi-zentui/package.json +60 -0
  76. package/dist/extensions/pi-zentui/test/accent-rail-editor.test.ts +232 -0
  77. package/dist/extensions/pi-zentui/test/accent-rail-layout-patch.test.ts +669 -0
  78. package/dist/extensions/pi-zentui/test/completion-menu.test.ts +265 -0
  79. package/dist/extensions/pi-zentui/test/config.test.ts +3119 -0
  80. package/dist/extensions/pi-zentui/test/editor-metadata-format.test.ts +405 -0
  81. package/dist/extensions/pi-zentui/test/editor-transfer.test.ts +260 -0
  82. package/dist/extensions/pi-zentui/test/editor-viewport-indicators.test.ts +1286 -0
  83. package/dist/extensions/pi-zentui/test/extension-compliance.test.ts +7196 -0
  84. package/dist/extensions/pi-zentui/test/extension-status.test.ts +182 -0
  85. package/dist/extensions/pi-zentui/test/fixtures/starship-nerd-font-symbols.toml +172 -0
  86. package/dist/extensions/pi-zentui/test/footer-format.test.ts +431 -0
  87. package/dist/extensions/pi-zentui/test/footer-layout.test.ts +113 -0
  88. package/dist/extensions/pi-zentui/test/footer-repository-path.test.ts +123 -0
  89. package/dist/extensions/pi-zentui/test/format.test.ts +865 -0
  90. package/dist/extensions/pi-zentui/test/git.test.ts +212 -0
  91. package/dist/extensions/pi-zentui/test/icons.test.ts +69 -0
  92. package/dist/extensions/pi-zentui/test/interaction-summary.test.ts +1402 -0
  93. package/dist/extensions/pi-zentui/test/live-context-integration.test.ts +378 -0
  94. package/dist/extensions/pi-zentui/test/live-context.test.ts +139 -0
  95. package/dist/extensions/pi-zentui/test/minimalist-editor.test.ts +811 -0
  96. package/dist/extensions/pi-zentui/test/package-version.test.ts +321 -0
  97. package/dist/extensions/pi-zentui/test/project-refresh.test.ts +250 -0
  98. package/dist/extensions/pi-zentui/test/project-state.test.ts +159 -0
  99. package/dist/extensions/pi-zentui/test/repository-root.test.ts +108 -0
  100. package/dist/extensions/pi-zentui/test/responsive-dependencies.test.ts +825 -0
  101. package/dist/extensions/pi-zentui/test/runtime.test.ts +183 -0
  102. package/dist/extensions/pi-zentui/test/session-lifecycle.test.ts +40 -0
  103. package/dist/extensions/pi-zentui/test/settings-command.test.ts +1381 -0
  104. package/dist/extensions/pi-zentui/test/settings-previews.test.ts +347 -0
  105. package/dist/extensions/pi-zentui/test/state.test.ts +84 -0
  106. package/dist/extensions/pi-zentui/test/telemetry-lifecycle-integration.test.ts +276 -0
  107. package/dist/extensions/pi-zentui/test/telemetry.test.ts +105 -0
  108. package/dist/extensions/pi-zentui/test/turn-summary-lifecycle.integration.test.ts +327 -0
  109. package/dist/extensions/pi-zentui/test/user-message-osc.test.ts +164 -0
  110. package/dist/extensions/pi-zentui/test/user-message-styles.test.ts +292 -0
  111. package/dist/extensions/pi-zentui/test/working-line-lifecycle.integration.test.ts +730 -0
  112. package/dist/extensions/pi-zentui/test/working-line-messages.test.ts +34 -0
  113. package/dist/extensions/pi-zentui/test/working-line.test.ts +1990 -0
  114. package/dist/extensions/pi-zentui/tsconfig.json +16 -0
  115. package/dist/extensions/tps.test.ts +38 -0
  116. package/dist/extensions/{pi-powerline-footer/tps.ts → tps.ts} +4 -0
  117. package/dist/skills/design-references/SKILL.md +31 -0
  118. package/dist/skills/design-references/references/clickhouse/DESIGN.md +544 -0
  119. package/dist/themes/adventure.json +84 -0
  120. package/dist/themes/adwaita-dark.json +84 -0
  121. package/dist/themes/arcoiris.json +84 -0
  122. package/dist/themes/arthur.json +84 -0
  123. package/dist/themes/atom.json +84 -0
  124. package/dist/themes/aura.json +84 -0
  125. package/dist/themes/black-metal-bathory.json +84 -0
  126. package/dist/themes/black-metal-burzum.json +84 -0
  127. package/dist/themes/black-metal-khold.json +84 -0
  128. package/dist/themes/box.json +84 -0
  129. package/dist/themes/brogrammer.json +84 -0
  130. package/dist/themes/carbonfox.json +84 -0
  131. package/dist/themes/catppuccin-mocha.json +84 -0
  132. package/dist/themes/citruszest.json +84 -0
  133. package/dist/themes/cursor-dark.json +84 -0
  134. package/dist/themes/cutie-pro.json +84 -0
  135. package/dist/themes/dark-modern.json +84 -0
  136. package/dist/themes/dark-pastel.json +84 -0
  137. package/dist/themes/dimmed-monokai.json +84 -0
  138. package/dist/themes/doom-peacock.json +84 -0
  139. package/dist/themes/dracula-plus.json +84 -0
  140. package/dist/themes/dracula.json +80 -0
  141. package/dist/themes/earthsong.json +84 -0
  142. package/dist/themes/everforest-dark-hard.json +84 -0
  143. package/dist/themes/everforest-dark.json +80 -0
  144. package/dist/themes/fahrenheit.json +84 -0
  145. package/dist/themes/flatland.json +84 -0
  146. package/dist/themes/flexoki-dark.json +84 -0
  147. package/dist/themes/front-end-delight.json +84 -0
  148. package/dist/themes/fun-forrest.json +84 -0
  149. package/dist/themes/galizur.json +84 -0
  150. package/dist/themes/github-dark-colorblind.json +84 -0
  151. package/dist/themes/github-dark-high-contrast.json +84 -0
  152. package/dist/themes/glacier.json +84 -0
  153. package/dist/themes/gruber-darker.json +84 -0
  154. package/dist/themes/gruvbox-dark-hard.json +84 -0
  155. package/dist/themes/gruvbox-dark.json +84 -0
  156. package/dist/themes/gruvbox-material.json +84 -0
  157. package/dist/themes/guezwhoz.json +84 -0
  158. package/dist/themes/hacktober.json +84 -0
  159. package/dist/themes/hardcore.json +84 -0
  160. package/dist/themes/havn-skumring.json +84 -0
  161. package/dist/themes/ic-orange-ppl.json +84 -0
  162. package/dist/themes/iterm2-smoooooth.json +84 -0
  163. package/dist/themes/iterm2-tango-dark.json +84 -0
  164. package/dist/themes/japanesque.json +84 -0
  165. package/dist/themes/jellybeans.json +84 -0
  166. package/dist/themes/kanagawa-wave.json +84 -0
  167. package/dist/themes/kurokula.json +84 -0
  168. package/dist/themes/later-this-evening.json +84 -0
  169. package/dist/themes/lovelace.json +84 -0
  170. package/dist/themes/material-darker.json +84 -0
  171. package/dist/themes/matte-black.json +84 -0
  172. package/dist/themes/mellow.json +84 -0
  173. package/dist/themes/miasma.json +84 -0
  174. package/dist/themes/nord.json +80 -0
  175. package/dist/themes/nvim-dark.json +84 -0
  176. package/dist/themes/one-dark.json +80 -0
  177. package/dist/themes/popping-and-locking.json +84 -0
  178. package/dist/themes/rose-pine.json +80 -0
  179. package/dist/themes/sea-shells.json +84 -0
  180. package/dist/themes/sleepy-hollow.json +84 -0
  181. package/dist/themes/smyck.json +84 -0
  182. package/dist/themes/solarized-dark.json +80 -0
  183. package/dist/themes/tokyo-night.json +80 -0
  184. package/dist/themes/tomorrow-night-bright.json +84 -0
  185. package/dist/themes/tomorrow-night-burns.json +84 -0
  186. package/dist/themes/tomorrow-night.json +84 -0
  187. package/dist/themes/twilight.json +84 -0
  188. package/dist/themes/vague.json +84 -0
  189. package/dist/themes/vesper.json +84 -0
  190. package/dist/themes/xcode-dark-hc.json +84 -0
  191. package/docs/custom-provider.md +0 -1
  192. package/package.json +3 -3
  193. package/dist/extensions/auto-model/auto-model.test.ts +0 -18
  194. package/dist/extensions/auto-model/classifier.ts +0 -23
  195. package/dist/extensions/auto-model/config.ts +0 -51
  196. package/dist/extensions/auto-model/index.ts +0 -22
  197. package/dist/extensions/auto-model/lifecycle.test.ts +0 -24
  198. package/dist/extensions/pi-powerline-footer/.github/workflows/test.yml +0 -27
  199. package/dist/extensions/pi-powerline-footer/CHANGELOG.md +0 -687
  200. package/dist/extensions/pi-powerline-footer/CONTRIBUTING.md +0 -16
  201. package/dist/extensions/pi-powerline-footer/README.md +0 -513
  202. package/dist/extensions/pi-powerline-footer/banner.png +0 -0
  203. package/dist/extensions/pi-powerline-footer/bash-mode/completion.ts +0 -586
  204. package/dist/extensions/pi-powerline-footer/bash-mode/editor.ts +0 -583
  205. package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +0 -204
  206. package/dist/extensions/pi-powerline-footer/bash-mode/shell-session.ts +0 -286
  207. package/dist/extensions/pi-powerline-footer/bash-mode/transcript.ts +0 -108
  208. package/dist/extensions/pi-powerline-footer/bash-mode/types.ts +0 -60
  209. package/dist/extensions/pi-powerline-footer/cd-command.ts +0 -190
  210. package/dist/extensions/pi-powerline-footer/colors.ts +0 -70
  211. package/dist/extensions/pi-powerline-footer/context-usage.ts +0 -156
  212. package/dist/extensions/pi-powerline-footer/currency-rates.ts +0 -142
  213. package/dist/extensions/pi-powerline-footer/editor-composition.ts +0 -32
  214. package/dist/extensions/pi-powerline-footer/editor-performance.ts +0 -99
  215. package/dist/extensions/pi-powerline-footer/git-status.ts +0 -329
  216. package/dist/extensions/pi-powerline-footer/guide.ts +0 -220
  217. package/dist/extensions/pi-powerline-footer/icons.ts +0 -190
  218. package/dist/extensions/pi-powerline-footer/index.ts +0 -3428
  219. package/dist/extensions/pi-powerline-footer/lifecycle.ts +0 -10
  220. package/dist/extensions/pi-powerline-footer/package.json +0 -49
  221. package/dist/extensions/pi-powerline-footer/paths.ts +0 -22
  222. package/dist/extensions/pi-powerline-footer/powerline-config.ts +0 -446
  223. package/dist/extensions/pi-powerline-footer/presets.ts +0 -102
  224. package/dist/extensions/pi-powerline-footer/queue/store.ts +0 -313
  225. package/dist/extensions/pi-powerline-footer/queue/types.ts +0 -53
  226. package/dist/extensions/pi-powerline-footer/render-scheduler.ts +0 -29
  227. package/dist/extensions/pi-powerline-footer/segments.ts +0 -570
  228. package/dist/extensions/pi-powerline-footer/separators.ts +0 -57
  229. package/dist/extensions/pi-powerline-footer/session-usage.ts +0 -44
  230. package/dist/extensions/pi-powerline-footer/shortcuts.ts +0 -61
  231. package/dist/extensions/pi-powerline-footer/tests/bash-mode.test.ts +0 -1905
  232. package/dist/extensions/pi-powerline-footer/tests/cd-command.test.ts +0 -144
  233. package/dist/extensions/pi-powerline-footer/tests/context-usage.test.ts +0 -156
  234. package/dist/extensions/pi-powerline-footer/tests/custom-items.test.ts +0 -344
  235. package/dist/extensions/pi-powerline-footer/tests/editor-composition.test.ts +0 -48
  236. package/dist/extensions/pi-powerline-footer/tests/editor-performance.test.ts +0 -43
  237. package/dist/extensions/pi-powerline-footer/tests/editor-responsiveness.test.ts +0 -136
  238. package/dist/extensions/pi-powerline-footer/tests/fixed-editor-border.test.ts +0 -39
  239. package/dist/extensions/pi-powerline-footer/tests/git-optional-locks.test.ts +0 -64
  240. package/dist/extensions/pi-powerline-footer/tests/git-status.test.ts +0 -92
  241. package/dist/extensions/pi-powerline-footer/tests/guide.test.ts +0 -129
  242. package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +0 -116
  243. package/dist/extensions/pi-powerline-footer/tests/mode-aware-trigger.test.ts +0 -79
  244. package/dist/extensions/pi-powerline-footer/tests/paths.test.ts +0 -63
  245. package/dist/extensions/pi-powerline-footer/tests/queue-store.test.ts +0 -65
  246. package/dist/extensions/pi-powerline-footer/tests/quit-cursor.test.ts +0 -14
  247. package/dist/extensions/pi-powerline-footer/tests/remaining-regressions.test.ts +0 -173
  248. package/dist/extensions/pi-powerline-footer/tests/session-usage.test.ts +0 -47
  249. package/dist/extensions/pi-powerline-footer/tests/stash-shortcut.test.ts +0 -37
  250. package/dist/extensions/pi-powerline-footer/tests/thinking-segment.test.ts +0 -76
  251. package/dist/extensions/pi-powerline-footer/tests/token-stats.test.ts +0 -249
  252. package/dist/extensions/pi-powerline-footer/tests/tps.test.ts +0 -39
  253. package/dist/extensions/pi-powerline-footer/tests/usage-display.test.ts +0 -237
  254. package/dist/extensions/pi-powerline-footer/tests/welcome.test.ts +0 -199
  255. package/dist/extensions/pi-powerline-footer/tests/working-vibes.test.ts +0 -317
  256. package/dist/extensions/pi-powerline-footer/theme.example.json +0 -24
  257. package/dist/extensions/pi-powerline-footer/theme.json +0 -12
  258. package/dist/extensions/pi-powerline-footer/theme.ts +0 -241
  259. package/dist/extensions/pi-powerline-footer/token-stats.ts +0 -304
  260. package/dist/extensions/pi-powerline-footer/tsconfig.json +0 -13
  261. package/dist/extensions/pi-powerline-footer/types.ts +0 -235
  262. package/dist/extensions/pi-powerline-footer/welcome-dismiss.ts +0 -34
  263. package/dist/extensions/pi-powerline-footer/welcome.ts +0 -709
  264. package/dist/extensions/pi-powerline-footer/working-vibes.ts +0 -721
  265. package/dist/extensions/preview-tools-disabled.test.ts +0 -37
  266. package/dist/extensions/preview-tools-disabled.ts +0 -11
  267. package/dist/themes/powerline-footer/theme.json +0 -33
@@ -0,0 +1,1402 @@
1
+ import { stripVTControlCharacters } from "node:util";
2
+ import { buildSessionContext } from "@earendil-works/pi-coding-agent";
3
+ import { describe, expect, it } from "vitest";
4
+ import {
5
+ formatTurnSummary,
6
+ InteractionMetricsTracker,
7
+ isTurnSummaryData,
8
+ parseAssistantMessageTokens,
9
+ renderTurnSummaryEntry,
10
+ subtractThoughtIntervalsWithinCap,
11
+ TURN_SUMMARY_ENTRY_TYPE,
12
+ } from "../extensions/zentui/interaction-summary";
13
+
14
+ const stripTerminalSequences = stripVTControlCharacters;
15
+
16
+ type AssistantOptions = {
17
+ responseId?: string;
18
+ timestamp?: number;
19
+ provider?: string;
20
+ model?: string;
21
+ stopReason?: string;
22
+ content?: unknown[];
23
+ };
24
+
25
+ function assistant(input: number, output: number, options: AssistantOptions = {}) {
26
+ return {
27
+ role: "assistant" as const,
28
+ usage: { input, output, cacheRead: 999, cacheWrite: 888, cost: { total: 1 } },
29
+ content: options.content ?? [],
30
+ api: "test",
31
+ provider: options.provider ?? "provider",
32
+ model: options.model ?? "model",
33
+ stopReason: options.stopReason ?? "stop",
34
+ timestamp: options.timestamp ?? 1,
35
+ responseId: options.responseId,
36
+ };
37
+ }
38
+
39
+ function delta(
40
+ type: "text_delta" | "thinking_delta",
41
+ contentIndex: number,
42
+ value: string,
43
+ cumulative?: string,
44
+ ) {
45
+ const content: unknown[] = [];
46
+ if (cumulative !== undefined) {
47
+ content[contentIndex] =
48
+ type === "text_delta"
49
+ ? { type: "text", text: cumulative }
50
+ : { type: "thinking", thinking: cumulative };
51
+ }
52
+ return {
53
+ type,
54
+ contentIndex,
55
+ delta: value,
56
+ partial: assistant(0, 0, { content }),
57
+ } as never;
58
+ }
59
+
60
+ function toolDelta(contentIndex: number, value: string, includeBlock = true) {
61
+ const content: unknown[] = [];
62
+ if (includeBlock) {
63
+ content[contentIndex] = { type: "toolCall", id: "call-1", name: "bash", arguments: {} };
64
+ }
65
+ return {
66
+ type: "toolcall_delta",
67
+ contentIndex,
68
+ delta: value,
69
+ partial: assistant(0, 0, { content }),
70
+ } as never;
71
+ }
72
+
73
+ function accepted(
74
+ tokens: { input: number; output: number },
75
+ source: "final" | "last-snapshot" | "no-snapshot" = "final",
76
+ displayTokens = { ...tokens, outputApproximate: false },
77
+ ) {
78
+ return { status: "accepted", tokens, displayTokens, source };
79
+ }
80
+
81
+ describe("live interaction token display", () => {
82
+ it("starts terminal-only streams at zero approximate and estimates text plus thinking", () => {
83
+ const tracker = new InteractionMetricsTracker();
84
+ tracker.agentStart();
85
+ tracker.turnStart();
86
+ expect(tracker.messageUpdate(assistant(0, 0)).displayTokens).toEqual({
87
+ input: 0,
88
+ output: 0,
89
+ outputApproximate: true,
90
+ });
91
+ expect(
92
+ tracker.messageUpdate(assistant(0, 0), delta("text_delta", 0, "abc", "abc")).displayTokens,
93
+ ).toEqual({ input: 0, output: 1, outputApproximate: true });
94
+ expect(
95
+ tracker.messageUpdate(assistant(0, 0), delta("thinking_delta", 1, "defgh", "defgh"))
96
+ .displayTokens,
97
+ ).toEqual({ input: 0, output: 2, outputApproximate: true });
98
+ });
99
+
100
+ it("counts cumulative Unicode code points and ignores unchanged or stale snapshots", () => {
101
+ const tracker = new InteractionMetricsTracker();
102
+ tracker.agentStart();
103
+ tracker.turnStart();
104
+ const first = tracker.messageUpdate(assistant(0, 0), delta("text_delta", 0, "💡ab", "💡ab"));
105
+ expect(first.displayTokens).toEqual({ input: 0, output: 1, outputApproximate: true });
106
+ expect(
107
+ tracker.messageUpdate(assistant(0, 0), delta("text_delta", 0, "💡ab", "💡ab")).usageChanged,
108
+ ).toBe(false);
109
+ expect(
110
+ tracker.messageUpdate(assistant(0, 0), delta("text_delta", 0, "x", "💡a")).usageChanged,
111
+ ).toBe(false);
112
+ });
113
+
114
+ it("counts split surrogate pairs once in raw fallback and saturates estimates", () => {
115
+ const tracker = new InteractionMetricsTracker();
116
+ tracker.agentStart();
117
+ tracker.turnStart();
118
+ tracker.messageUpdate(assistant(0, 0), delta("text_delta", 0, "\ud83d"));
119
+ expect(
120
+ tracker.messageUpdate(assistant(0, 0), delta("text_delta", 0, "\udca1abc")).displayTokens,
121
+ ).toEqual({ input: 0, output: 1, outputApproximate: true });
122
+ const huge = new InteractionMetricsTracker();
123
+ huge.agentStart();
124
+ huge.turnStart();
125
+ huge.messageUpdate(assistant(0, Number.MAX_SAFE_INTEGER));
126
+ expect(
127
+ huge.messageUpdate(assistant(0, Number.MAX_SAFE_INTEGER), delta("text_delta", 0, "more"))
128
+ .displayTokens,
129
+ ).toEqual({ input: 0, output: Number.MAX_SAFE_INTEGER, outputApproximate: true });
130
+ });
131
+
132
+ it("anchors estimates to advancing provider output and final corrects downward", () => {
133
+ const tracker = new InteractionMetricsTracker();
134
+ tracker.agentStart();
135
+ tracker.turnStart();
136
+ tracker.messageUpdate(assistant(10, 2), delta("text_delta", 0, "abcdefgh", "abcdefgh"));
137
+ expect(
138
+ tracker.messageUpdate(assistant(10, 2), delta("text_delta", 0, "ijkl", "abcdefghijkl"))
139
+ .displayTokens,
140
+ ).toEqual({ input: 10, output: 3, outputApproximate: true });
141
+ expect(tracker.messageUpdate(assistant(9, 1)).displayTokens).toEqual({
142
+ input: 10,
143
+ output: 3,
144
+ outputApproximate: true,
145
+ });
146
+ expect(tracker.messageEnd(assistant(10, 2))).toEqual(accepted({ input: 10, output: 2 }));
147
+ expect(tracker.currentTokens()).toEqual({ input: 10, output: 2 });
148
+ });
149
+
150
+ it("retains live estimate state across non-idle partition while settling provider-only totals", () => {
151
+ const tracker = new InteractionMetricsTracker();
152
+ tracker.agentStart(0);
153
+ tracker.turnStart();
154
+ tracker.messageEnd(assistant(5, 1, { responseId: "settled" }));
155
+ tracker.agentEnd(10);
156
+ tracker.agentStart(20);
157
+ tracker.turnStart();
158
+ tracker.messageUpdate(
159
+ assistant(0, 0, { responseId: "live" }),
160
+ delta("text_delta", 0, "abcdefgh", "abcdefgh"),
161
+ );
162
+ expect(tracker.settle(false, 30)).toMatchObject({
163
+ summary: { input: 5, output: 1 },
164
+ nextTokens: { input: 0, output: 2, outputApproximate: true },
165
+ });
166
+ expect(tracker.currentDisplayTokens()).toEqual({
167
+ input: 0,
168
+ output: 2,
169
+ outputApproximate: true,
170
+ });
171
+ });
172
+
173
+ it("handles sparse cumulative thinking indexes and divergent snapshots conservatively", () => {
174
+ const tracker = new InteractionMetricsTracker();
175
+ tracker.agentStart();
176
+ tracker.turnStart();
177
+ expect(
178
+ tracker.messageUpdate(assistant(0, 0), delta("thinking_delta", 7, "abcd", "abcd"))
179
+ .displayTokens,
180
+ ).toEqual({ input: 0, output: 1, outputApproximate: true });
181
+ expect(
182
+ tracker.messageUpdate(assistant(0, 0), delta("thinking_delta", 7, "efgh", "abcdefgh"))
183
+ .displayTokens,
184
+ ).toEqual({ input: 0, output: 2, outputApproximate: true });
185
+ expect(
186
+ tracker.messageUpdate(assistant(0, 0), delta("thinking_delta", 7, "zzzz", "abcdzzzz"))
187
+ .usageChanged,
188
+ ).toBe(false);
189
+ expect(tracker.diagnostics()).toMatchObject({
190
+ contentBlocks: 1,
191
+ retainedContentUnits: 8,
192
+ estimateIncomplete: true,
193
+ });
194
+ });
195
+
196
+ it("joins cumulative split surrogates across an unchanged pending snapshot", () => {
197
+ const tracker = new InteractionMetricsTracker();
198
+ tracker.agentStart();
199
+ tracker.turnStart();
200
+ tracker.messageUpdate(assistant(0, 0), delta("text_delta", 0, "\ud83d", "\ud83d"));
201
+ expect(
202
+ tracker.messageUpdate(assistant(0, 0), delta("text_delta", 0, "", "\ud83d")).usageChanged,
203
+ ).toBe(false);
204
+ expect(
205
+ tracker.messageUpdate(assistant(0, 0), delta("text_delta", 0, "\udca1abc", "💡abc"))
206
+ .displayTokens,
207
+ ).toEqual({ input: 0, output: 1, outputApproximate: true });
208
+ });
209
+
210
+ it("validates malformed event boundaries before any mutation", () => {
211
+ for (const event of [
212
+ { type: "thinking_delta", contentIndex: 0, delta: "x" },
213
+ { type: "thinking_delta", contentIndex: 0, delta: 1, partial: { content: [] } },
214
+ { type: "thinking_delta", contentIndex: 0, delta: "x", partial: { content: {} } },
215
+ {
216
+ type: "thinking_delta",
217
+ contentIndex: 0,
218
+ delta: "x",
219
+ partial: { content: [{ type: "text", text: "x" }] },
220
+ },
221
+ { type: "thinking_start", contentIndex: 65_536, partial: { content: [] } },
222
+ ] as const) {
223
+ const tracker = new InteractionMetricsTracker();
224
+ tracker.agentStart(0);
225
+ tracker.turnStart(0);
226
+ expect(() => tracker.messageUpdate(assistant(-1, 1), event as never, 10)).not.toThrow();
227
+ expect(tracker.currentTokens()).toEqual({ input: 0, output: 0 });
228
+ expect(tracker.currentThought(20)).toEqual({ durationMs: 0, active: false });
229
+ expect(tracker.diagnostics().contentBlocks).toBe(0);
230
+ }
231
+ });
232
+
233
+ it("releases bounded estimator state on closure", () => {
234
+ const tracker = new InteractionMetricsTracker();
235
+ tracker.agentStart();
236
+ tracker.turnStart();
237
+ tracker.messageUpdate(assistant(0, 0), delta("text_delta", 0, "abcdefgh", "abcdefgh"));
238
+ expect(tracker.diagnostics()).toMatchObject({ activeResponses: 1, contentBlocks: 1 });
239
+ expect(tracker.messageEnd(assistant(2, 1))).toEqual(accepted({ input: 2, output: 1 }));
240
+ expect(tracker.diagnostics()).toMatchObject({
241
+ activeResponses: 0,
242
+ contentBlocks: 0,
243
+ retainedContentUnits: 0,
244
+ });
245
+ });
246
+
247
+ it("processes progressive cumulative snapshots incrementally with bounded retention", () => {
248
+ const tracker = new InteractionMetricsTracker();
249
+ tracker.agentStart();
250
+ tracker.turnStart();
251
+ let cumulative = "";
252
+ const started = performance.now();
253
+ for (let index = 0; index < 20_000; index++) {
254
+ cumulative += "x";
255
+ tracker.messageUpdate(assistant(0, 0), delta("text_delta", 0, "x", cumulative));
256
+ }
257
+ expect(performance.now() - started).toBeLessThan(5_000);
258
+ expect(tracker.currentDisplayTokens()).toEqual({
259
+ input: 0,
260
+ output: 5_000,
261
+ outputApproximate: true,
262
+ });
263
+ expect(tracker.diagnostics()).toMatchObject({
264
+ contentBlocks: 1,
265
+ maxContentTailUnits: 64,
266
+ retainedContentUnits: 64,
267
+ });
268
+ });
269
+
270
+ it("bounds block, delta, snapshot, generated, and thought estimator state without losing final usage", () => {
271
+ const tracker = new InteractionMetricsTracker();
272
+ tracker.agentStart(0);
273
+ tracker.turnStart(0);
274
+ for (let index = 0; index < 129; index++) {
275
+ expect(() =>
276
+ tracker.messageUpdate(assistant(0, 0), delta("text_delta", index, "x")),
277
+ ).not.toThrow();
278
+ }
279
+ expect(tracker.diagnostics().contentBlocks).toBe(128);
280
+ expect(() =>
281
+ tracker.messageUpdate(assistant(0, 0), delta("text_delta", 0, "x".repeat(65_537))),
282
+ ).not.toThrow();
283
+ expect(() =>
284
+ tracker.messageUpdate(assistant(0, 0), delta("text_delta", 0, "x", "x".repeat(1_048_577))),
285
+ ).not.toThrow();
286
+ for (let index = 0; index < 65; index++) {
287
+ expect(() =>
288
+ tracker.messageUpdate(
289
+ assistant(0, 0),
290
+ delta("text_delta", index % 128, "x".repeat(65_536)),
291
+ ),
292
+ ).not.toThrow();
293
+ }
294
+ for (let index = 0; index < 257; index++) {
295
+ expect(() =>
296
+ tracker.messageUpdate(
297
+ assistant(0, 0),
298
+ {
299
+ type: "thinking_start",
300
+ contentIndex: index,
301
+ partial: { content: [] },
302
+ } as never,
303
+ index,
304
+ ),
305
+ ).not.toThrow();
306
+ }
307
+ expect(tracker.diagnostics()).toMatchObject({
308
+ contentBlocks: 128,
309
+ thoughtBlocks: 256,
310
+ estimateIncomplete: true,
311
+ });
312
+ expect(tracker.messageEnd(assistant(11, 7), 300)).toEqual(accepted({ input: 11, output: 7 }));
313
+ });
314
+
315
+ it("counts every raw tool-call chunk independently and exact final usage reconciles it", () => {
316
+ const tracker = new InteractionMetricsTracker();
317
+ tracker.agentStart(0);
318
+ tracker.turnStart(0);
319
+ expect(tracker.messageUpdate(assistant(0, 0), toolDelta(0, "abcdefgh"), 10)).toMatchObject({
320
+ displayTokens: { input: 0, output: 2, outputApproximate: true },
321
+ usageChanged: true,
322
+ thoughtChanged: false,
323
+ });
324
+ expect(
325
+ tracker.messageUpdate(assistant(0, 0), toolDelta(0, "abcdefgh"), 20).displayTokens,
326
+ ).toEqual({ input: 0, output: 4, outputApproximate: true });
327
+ expect(tracker.currentThought(30)).toEqual({ durationMs: 0, active: false });
328
+ expect(tracker.diagnostics()).toMatchObject({ contentBlocks: 1, thoughtBlocks: 0 });
329
+ expect(tracker.messageEnd(assistant(7, 1), 40)).toEqual(accepted({ input: 7, output: 1 }));
330
+ });
331
+
332
+ it("ceilings mixed text, thinking, and tool-call output together", () => {
333
+ const tracker = new InteractionMetricsTracker();
334
+ tracker.agentStart(0);
335
+ tracker.turnStart(0);
336
+ tracker.messageUpdate(assistant(0, 0), delta("text_delta", 0, "abc", "abc"));
337
+ tracker.messageUpdate(assistant(0, 0), delta("thinking_delta", 1, "def", "def"));
338
+ expect(tracker.messageUpdate(assistant(0, 0), toolDelta(2, "ghi")).displayTokens).toEqual({
339
+ input: 0,
340
+ output: 3,
341
+ outputApproximate: true,
342
+ });
343
+ expect(tracker.diagnostics().contentBlocks).toBe(3);
344
+ });
345
+
346
+ it("rejects malformed tool deltas atomically and bounds oversized tool-call state", () => {
347
+ for (const event of [
348
+ { type: "toolcall_delta", contentIndex: 0, delta: "x" },
349
+ { type: "toolcall_delta", contentIndex: 0, delta: 1, partial: { content: [] } },
350
+ { type: "toolcall_delta", contentIndex: 0, delta: "x", partial: { content: [] } },
351
+ { type: "toolcall_delta", contentIndex: 0, delta: "x", partial: { content: {} } },
352
+ {
353
+ type: "toolcall_delta",
354
+ contentIndex: 0,
355
+ delta: "x",
356
+ partial: { content: [{ type: "text", text: "x" }] },
357
+ },
358
+ ] as const) {
359
+ const tracker = new InteractionMetricsTracker();
360
+ tracker.agentStart();
361
+ tracker.turnStart();
362
+ expect(tracker.messageUpdate(assistant(0, 0), event as never).usageChanged).toBe(false);
363
+ expect(tracker.diagnostics()).toMatchObject({ contentBlocks: 0, estimateIncomplete: false });
364
+ }
365
+
366
+ const oversized = new InteractionMetricsTracker();
367
+ oversized.agentStart();
368
+ oversized.turnStart();
369
+ expect(
370
+ oversized.messageUpdate(assistant(0, 0), toolDelta(0, "x".repeat(65_537))).usageChanged,
371
+ ).toBe(true);
372
+ expect(oversized.currentDisplayTokens()).toEqual({
373
+ input: 0,
374
+ output: 0,
375
+ outputApproximate: true,
376
+ });
377
+ expect(oversized.diagnostics()).toMatchObject({
378
+ contentBlocks: 0,
379
+ estimateIncomplete: true,
380
+ });
381
+ expect(oversized.messageEnd(assistant(11, 7))).toEqual(accepted({ input: 11, output: 7 }));
382
+ });
383
+
384
+ it("caps tool-call blocks and preserves exact final reconciliation", () => {
385
+ const tracker = new InteractionMetricsTracker();
386
+ tracker.agentStart();
387
+ tracker.turnStart();
388
+ for (let index = 0; index < 129; index++) {
389
+ tracker.messageUpdate(assistant(0, 0), toolDelta(index, "x"));
390
+ }
391
+ expect(tracker.diagnostics()).toMatchObject({
392
+ contentBlocks: 128,
393
+ estimateIncomplete: true,
394
+ });
395
+ expect(tracker.messageEnd(assistant(9, 4))).toEqual(accepted({ input: 9, output: 4 }));
396
+ expect(tracker.diagnostics()).toMatchObject({ contentBlocks: 0, retainedContentUnits: 0 });
397
+ });
398
+
399
+ it("never commits an estimate on malformed closure", () => {
400
+ const tracker = new InteractionMetricsTracker();
401
+ tracker.agentStart();
402
+ tracker.turnStart();
403
+ tracker.messageUpdate(assistant(0, 0), delta("text_delta", 0, "abcdefgh", "abcdefgh"));
404
+ expect(tracker.messageEnd(assistant(-1, 1))).toEqual(
405
+ accepted({ input: 0, output: 0 }, "no-snapshot", {
406
+ input: 0,
407
+ output: 2,
408
+ outputApproximate: true,
409
+ }),
410
+ );
411
+ expect(tracker.currentTokens()).toEqual({ input: 0, output: 0 });
412
+ });
413
+ });
414
+
415
+ describe("interaction metrics tracker", () => {
416
+ it("promotes a response-less stream to a normalized ID and ignores its duplicate final", () => {
417
+ const tracker = new InteractionMetricsTracker();
418
+ tracker.agentStart(1_000);
419
+ tracker.turnStart();
420
+ expect(
421
+ tracker.messageUpdate(assistant(100, 4, { timestamp: 1, provider: "first", model: "first" }))
422
+ .displayTokens,
423
+ ).toEqual({ input: 100, output: 4, outputApproximate: false });
424
+ expect(
425
+ tracker.messageUpdate(
426
+ assistant(120, 7, {
427
+ responseId: " response-a ",
428
+ timestamp: 2,
429
+ provider: "second",
430
+ model: "second",
431
+ }),
432
+ ).displayTokens,
433
+ ).toEqual({ input: 120, output: 7, outputApproximate: false });
434
+ expect(
435
+ tracker.messageEnd(assistant(150, 9, { responseId: "response-a", timestamp: 3 })),
436
+ ).toEqual(accepted({ input: 150, output: 9 }));
437
+ expect(
438
+ tracker.messageEnd(assistant(999, 99, { responseId: " response-a ", timestamp: 4 })),
439
+ ).toEqual({ status: "rejected" });
440
+ });
441
+
442
+ it("deduplicates the same ID even after a new turn slot", () => {
443
+ const tracker = new InteractionMetricsTracker();
444
+ tracker.agentStart();
445
+ tracker.turnStart();
446
+ tracker.messageEnd(assistant(10, 1, { responseId: "a" }));
447
+ tracker.turnStart();
448
+ expect(tracker.messageEnd(assistant(10, 1, { responseId: " a " }))).toEqual({
449
+ status: "duplicate",
450
+ });
451
+ });
452
+
453
+ it("keeps short IDs exact and treats overlong IDs as anonymous", () => {
454
+ const tracker = new InteractionMetricsTracker();
455
+ const first = `${"a".repeat(126)}-1`;
456
+ const second = `${"a".repeat(126)}-2`;
457
+ tracker.agentStart();
458
+ tracker.turnStart();
459
+ expect(tracker.messageEnd(assistant(10, 1, { responseId: first }))).toEqual(
460
+ accepted({ input: 10, output: 1 }),
461
+ );
462
+ tracker.turnStart();
463
+ expect(tracker.messageEnd(assistant(20, 2, { responseId: second }))).toEqual(
464
+ accepted({ input: 30, output: 3 }),
465
+ );
466
+ tracker.turnStart();
467
+ expect(tracker.messageEnd(assistant(99, 99, { responseId: first }))).toEqual({
468
+ status: "duplicate",
469
+ });
470
+
471
+ const overlong = "x".repeat(129);
472
+ tracker.turnStart();
473
+ expect(tracker.messageEnd(assistant(3, 4, { responseId: overlong }))).toEqual(
474
+ accepted({ input: 33, output: 7 }),
475
+ );
476
+ expect(tracker.diagnostics().responseIds).toBe(2);
477
+ });
478
+
479
+ it("rejects multi-megabyte response IDs before normalization or retention", () => {
480
+ const tracker = new InteractionMetricsTracker();
481
+ const overlong = ` ${"x".repeat(2 * 1024 * 1024)} `;
482
+ tracker.agentStart();
483
+ tracker.turnStart();
484
+ const startedAt = performance.now();
485
+ expect(tracker.messageUpdate(assistant(2, 1, { responseId: overlong })).displayTokens).toEqual({
486
+ input: 2,
487
+ output: 1,
488
+ outputApproximate: false,
489
+ });
490
+ expect(tracker.messageEnd(assistant(7, 3, { responseId: overlong }))).toEqual(
491
+ accepted({ input: 7, output: 3 }),
492
+ );
493
+ expect(performance.now() - startedAt).toBeLessThan(1_000);
494
+ expect(tracker.diagnostics().responseIds).toBe(0);
495
+ expect(tracker.messageEnd(assistant(99, 99, { responseId: overlong }))).toEqual({
496
+ status: "rejected",
497
+ });
498
+ });
499
+
500
+ it("counts a reused response ID independently in a later low-level run", () => {
501
+ const tracker = new InteractionMetricsTracker();
502
+ tracker.agentStart();
503
+ tracker.turnStart();
504
+ tracker.messageEnd(assistant(10, 1, { responseId: "reused" }));
505
+ tracker.agentEnd();
506
+ tracker.agentStart();
507
+ tracker.turnStart();
508
+ expect(tracker.messageEnd(assistant(20, 2, { responseId: " reused " }))).toEqual(
509
+ accepted({
510
+ input: 30,
511
+ output: 3,
512
+ }),
513
+ );
514
+ });
515
+
516
+ it("rejects distinct finals until turn_start authorizes each slot", () => {
517
+ const tracker = new InteractionMetricsTracker();
518
+ tracker.agentStart();
519
+ expect(tracker.messageEnd(assistant(10, 1, { responseId: "a", timestamp: 7 }))).toEqual({
520
+ status: "rejected",
521
+ });
522
+ tracker.turnStart();
523
+ expect(tracker.messageEnd(assistant(20, 2, { responseId: "b", timestamp: 7 }))).toEqual(
524
+ accepted({ input: 20, output: 2 }),
525
+ );
526
+ });
527
+
528
+ it("counts response-less same-metadata responses only when turn_start separates them", () => {
529
+ const tracker = new InteractionMetricsTracker();
530
+ tracker.agentStart();
531
+ tracker.turnStart();
532
+ tracker.messageEnd(assistant(10, 1, { timestamp: 7 }));
533
+ expect(tracker.messageEnd(assistant(10, 1, { timestamp: 7 }))).toEqual({ status: "rejected" });
534
+ tracker.turnStart();
535
+ expect(tracker.messageEnd(assistant(10, 1, { timestamp: 7 }))).toEqual(
536
+ accepted({
537
+ input: 20,
538
+ output: 2,
539
+ }),
540
+ );
541
+ });
542
+
543
+ it("deduplicates a streaming error final despite changed metadata and timestamp", () => {
544
+ const tracker = new InteractionMetricsTracker();
545
+ tracker.agentStart();
546
+ tracker.turnStart();
547
+ tracker.messageUpdate(
548
+ assistant(5, 1, {
549
+ responseId: "error-response",
550
+ provider: "first",
551
+ model: "first",
552
+ timestamp: 1,
553
+ }),
554
+ );
555
+ expect(
556
+ tracker.messageEnd(
557
+ assistant(8, 2, {
558
+ responseId: "error-response",
559
+ provider: "second",
560
+ model: "second",
561
+ stopReason: "error",
562
+ timestamp: 2,
563
+ }),
564
+ ),
565
+ ).toEqual(accepted({ input: 8, output: 2 }));
566
+ expect(
567
+ tracker.messageEnd(
568
+ assistant(80, 20, {
569
+ responseId: "error-response",
570
+ provider: "third",
571
+ model: "third",
572
+ stopReason: "error",
573
+ timestamp: 3,
574
+ }),
575
+ ),
576
+ ).toEqual({ status: "rejected" });
577
+ });
578
+
579
+ it("deduplicates a response-less aborted streaming final", () => {
580
+ const tracker = new InteractionMetricsTracker();
581
+ tracker.agentStart();
582
+ tracker.turnStart();
583
+ tracker.messageUpdate(assistant(4, 1, { stopReason: "aborted", timestamp: 1 }));
584
+ expect(tracker.messageEnd(assistant(6, 2, { stopReason: "aborted", timestamp: 2 }))).toEqual(
585
+ accepted({
586
+ input: 6,
587
+ output: 2,
588
+ }),
589
+ );
590
+ expect(tracker.messageEnd(assistant(60, 20, { stopReason: "aborted", timestamp: 3 }))).toEqual({
591
+ status: "rejected",
592
+ });
593
+ });
594
+
595
+ it("displays cumulative usage throughout a three-response tool loop", () => {
596
+ const tracker = new InteractionMetricsTracker();
597
+ tracker.agentStart();
598
+ for (const [input, output, expectedUpdate, expectedFinal, responseId, stopReason] of [
599
+ [
600
+ 10,
601
+ 1,
602
+ { input: 9, output: 1, outputApproximate: false },
603
+ { input: 10, output: 1 },
604
+ "a",
605
+ "toolUse",
606
+ ],
607
+ [
608
+ 20,
609
+ 2,
610
+ { input: 29, output: 3, outputApproximate: false },
611
+ { input: 30, output: 3 },
612
+ "b",
613
+ "toolUse",
614
+ ],
615
+ [
616
+ 30,
617
+ 3,
618
+ { input: 59, output: 6, outputApproximate: false },
619
+ { input: 60, output: 6 },
620
+ "c",
621
+ "stop",
622
+ ],
623
+ ] as const) {
624
+ tracker.turnStart();
625
+ expect(
626
+ tracker.messageUpdate(assistant(input - 1, output, { responseId, stopReason }))
627
+ .displayTokens,
628
+ ).toEqual(expectedUpdate);
629
+ expect(tracker.messageEnd(assistant(input, output, { responseId, stopReason }))).toEqual(
630
+ accepted(expectedFinal),
631
+ );
632
+ }
633
+ expect(tracker.currentTokens()).toEqual({ input: 60, output: 6 });
634
+ });
635
+
636
+ it("retains committed totals for a next response's initial zero snapshot", () => {
637
+ const tracker = new InteractionMetricsTracker();
638
+ tracker.agentStart();
639
+ tracker.turnStart();
640
+ tracker.messageEnd(assistant(10, 1, { responseId: "first" }));
641
+ tracker.turnStart();
642
+ expect(tracker.messageUpdate(assistant(0, 0, { responseId: "second" })).displayTokens).toEqual({
643
+ input: 10,
644
+ output: 1,
645
+ outputApproximate: true,
646
+ });
647
+ expect(tracker.messageUpdate(assistant(7, 2, { responseId: "second" })).displayTokens).toEqual({
648
+ input: 17,
649
+ output: 3,
650
+ outputApproximate: false,
651
+ });
652
+ });
653
+
654
+ it.each([undefined, "final-id"])(
655
+ "accepts authorized final-only usage with ID %s",
656
+ (responseId) => {
657
+ const tracker = new InteractionMetricsTracker();
658
+ tracker.agentStart();
659
+ tracker.turnStart();
660
+ expect(tracker.messageEnd(assistant(4, 2, { responseId }))).toEqual(
661
+ accepted({ input: 4, output: 2 }),
662
+ );
663
+ },
664
+ );
665
+
666
+ it("shows repeated initial zero updates once and ignores placeholder regression", () => {
667
+ const tracker = new InteractionMetricsTracker();
668
+ tracker.agentStart();
669
+ tracker.turnStart();
670
+ expect(tracker.messageUpdate(assistant(0, 0, { responseId: "stream" })).displayTokens).toEqual({
671
+ input: 0,
672
+ output: 0,
673
+ outputApproximate: true,
674
+ });
675
+ expect(tracker.messageUpdate(assistant(0, 0, { responseId: "stream" })).usageChanged).toBe(
676
+ false,
677
+ );
678
+ expect(tracker.currentTokens()).toEqual({ input: 0, output: 0 });
679
+ expect(tracker.messageUpdate(assistant(8, 2, { responseId: "stream" })).displayTokens).toEqual({
680
+ input: 8,
681
+ output: 2,
682
+ outputApproximate: false,
683
+ });
684
+ expect(tracker.messageUpdate(assistant(0, 0, { responseId: "stream" })).displayTokens).toEqual({
685
+ input: 8,
686
+ output: 2,
687
+ outputApproximate: false,
688
+ });
689
+ expect(tracker.currentTokens()).toEqual({ input: 8, output: 2 });
690
+ });
691
+
692
+ it("commits a zero-only final exactly once", () => {
693
+ const tracker = new InteractionMetricsTracker();
694
+ tracker.agentStart(1_000);
695
+ tracker.turnStart();
696
+ expect(
697
+ tracker.messageUpdate(assistant(0, 0, { responseId: "zero-only" })).displayTokens,
698
+ ).toEqual({ input: 0, output: 0, outputApproximate: true });
699
+ expect(tracker.messageEnd(assistant(0, 0, { responseId: "zero-only" }))).toEqual(
700
+ accepted({
701
+ input: 0,
702
+ output: 0,
703
+ }),
704
+ );
705
+ expect(tracker.messageEnd(assistant(9, 3, { responseId: "zero-only" }))).toEqual({
706
+ status: "rejected",
707
+ });
708
+ tracker.agentEnd();
709
+ expect(tracker.settle(true, 2_000)?.summary).toEqual({
710
+ durationMs: 1_000,
711
+ thoughtDurationMs: 0,
712
+ input: 0,
713
+ output: 0,
714
+ });
715
+ });
716
+
717
+ it("rejects invalid and non-assistant updates without changing accounting", () => {
718
+ const tracker = new InteractionMetricsTracker();
719
+ tracker.agentStart();
720
+ tracker.turnStart();
721
+ tracker.messageUpdate(assistant(8, 2));
722
+ expect(tracker.messageUpdate(assistant(-1, 4)).usageChanged).toBe(false);
723
+ expect(
724
+ tracker.messageUpdate({ role: "user", usage: { input: 99, output: 99 } }).usageChanged,
725
+ ).toBe(false);
726
+ expect(tracker.currentTokens()).toEqual({ input: 8, output: 2 });
727
+ });
728
+
729
+ it.each([
730
+ [assistant(0, 0), { input: 0, output: 0 }],
731
+ [assistant(-1, 0), undefined],
732
+ [assistant(0, 1.5), undefined],
733
+ [assistant(Number.POSITIVE_INFINITY, 0), undefined],
734
+ [assistant(Number.MAX_SAFE_INTEGER + 1, 0), undefined],
735
+ [{ role: "user", usage: { input: 0, output: 0 } }, undefined],
736
+ ])("strictly parses assistant usage %#", (message, expected) => {
737
+ expect(parseAssistantMessageTokens(message)).toEqual(expected);
738
+ });
739
+
740
+ it("saturates token fields independently and settles valid data with a clamped duration", () => {
741
+ const tracker = new InteractionMetricsTracker();
742
+ tracker.agentStart(1_000);
743
+ tracker.turnStart();
744
+ expect(tracker.messageEnd(assistant(-1, 4, { responseId: "invalid-negative" }))).toEqual(
745
+ accepted({ input: 0, output: 0 }, "no-snapshot"),
746
+ );
747
+ expect(
748
+ tracker.messageEnd(
749
+ assistant(Number.MAX_SAFE_INTEGER + 1, 4, { responseId: "invalid-unsafe" }),
750
+ ),
751
+ ).toEqual({ status: "rejected" });
752
+ tracker.turnStart();
753
+ tracker.messageEnd(assistant(Number.MAX_SAFE_INTEGER - 2, 10, { responseId: "first-valid" }));
754
+ tracker.turnStart();
755
+ expect(
756
+ tracker.messageEnd(
757
+ assistant(5, Number.MAX_SAFE_INTEGER - 20, { responseId: "second-valid" }),
758
+ ),
759
+ ).toEqual(
760
+ accepted({
761
+ input: Number.MAX_SAFE_INTEGER,
762
+ output: Number.MAX_SAFE_INTEGER - 10,
763
+ }),
764
+ );
765
+ tracker.turnStart();
766
+ expect(tracker.messageEnd(assistant(0, 20, { responseId: "third-valid" }))).toEqual(
767
+ accepted({
768
+ input: Number.MAX_SAFE_INTEGER,
769
+ output: Number.MAX_SAFE_INTEGER,
770
+ }),
771
+ );
772
+ tracker.agentEnd();
773
+ const settled = tracker.settle(true, Number.POSITIVE_INFINITY);
774
+ expect(settled?.summary).toEqual({
775
+ durationMs: Number.MAX_SAFE_INTEGER,
776
+ thoughtDurationMs: 0,
777
+ input: Number.MAX_SAFE_INTEGER,
778
+ output: Number.MAX_SAFE_INTEGER,
779
+ });
780
+ expect(
781
+ isTurnSummaryData({
782
+ version: 1,
783
+ durationMs: settled?.summary.durationMs,
784
+ input: settled?.summary.input,
785
+ output: settled?.summary.output,
786
+ }),
787
+ ).toBe(true);
788
+ });
789
+
790
+ it("spans low-level runs and resets only after settlement", () => {
791
+ const tracker = new InteractionMetricsTracker();
792
+ expect(tracker.agentStart(1_000)).toEqual({ interactionStarted: true });
793
+ tracker.turnStart();
794
+ tracker.messageEnd(assistant(10, 2, { responseId: "a" }));
795
+ tracker.agentEnd();
796
+ expect(tracker.agentStart(2_000)).toEqual({ interactionStarted: false });
797
+ tracker.turnStart();
798
+ tracker.messageEnd(assistant(20, 3, { responseId: "b" }));
799
+ tracker.agentEnd();
800
+ expect(tracker.settle(true, 3_000)?.summary).toEqual({
801
+ durationMs: 2_000,
802
+ thoughtDurationMs: 0,
803
+ input: 30,
804
+ output: 5,
805
+ });
806
+ expect(tracker.settle(true)).toBeUndefined();
807
+ tracker.agentStart(50_000);
808
+ tracker.shutdown();
809
+ expect(tracker.settle(true)).toBeUndefined();
810
+ });
811
+
812
+ it.each(["error", "aborted"])(
813
+ "commits the last valid snapshot once when a matching %s final has malformed usage",
814
+ (stopReason) => {
815
+ const tracker = new InteractionMetricsTracker();
816
+ tracker.agentStart(1_000);
817
+ tracker.turnStart();
818
+ tracker.messageUpdate(assistant(10, 1, { responseId: "a" }));
819
+ const malformed = assistant(-1, 99, { responseId: "a", stopReason });
820
+ expect(tracker.messageEnd(malformed)).toEqual(
821
+ accepted({ input: 10, output: 1 }, "last-snapshot"),
822
+ );
823
+ expect(tracker.currentTokens()).toEqual({ input: 10, output: 1 });
824
+ expect(tracker.messageEnd(malformed)).toEqual({ status: "rejected" });
825
+ expect(tracker.currentTokens()).toEqual({ input: 10, output: 1 });
826
+
827
+ tracker.turnStart();
828
+ expect(tracker.messageEnd(assistant(20, 2, { responseId: "b" }))).toEqual(
829
+ accepted({
830
+ input: 30,
831
+ output: 3,
832
+ }),
833
+ );
834
+ tracker.agentEnd();
835
+ expect(tracker.settle(true, 2_000)?.summary).toEqual({
836
+ durationMs: 1_000,
837
+ thoughtDurationMs: 0,
838
+ input: 30,
839
+ output: 3,
840
+ });
841
+ },
842
+ );
843
+
844
+ it("preserves a live estimate when a malformed final has no valid snapshot", () => {
845
+ const tracker = new InteractionMetricsTracker();
846
+ tracker.agentStart();
847
+ tracker.turnStart();
848
+ tracker.messageUpdate(
849
+ assistant(0, 0, { responseId: "invalid" }),
850
+ delta("thinking_delta", 0, "abcdefgh", "abcdefgh"),
851
+ );
852
+ expect(tracker.messageEnd(assistant(-1, 1, { responseId: "invalid" }))).toEqual(
853
+ accepted({ input: 0, output: 0 }, "no-snapshot", {
854
+ input: 0,
855
+ output: 2,
856
+ outputApproximate: true,
857
+ }),
858
+ );
859
+ expect(tracker.currentTokens()).toEqual({ input: 0, output: 0 });
860
+ expect(tracker.currentDisplayTokens()).toEqual({
861
+ input: 0,
862
+ output: 2,
863
+ outputApproximate: true,
864
+ });
865
+ tracker.turnStart();
866
+ expect(tracker.messageEnd(assistant(20, 2, { responseId: "b" }))).toEqual(
867
+ accepted({
868
+ input: 20,
869
+ output: 2,
870
+ }),
871
+ );
872
+ });
873
+
874
+ it("preserves an estimate above a lower provider anchor on malformed final", () => {
875
+ const tracker = new InteractionMetricsTracker();
876
+ tracker.agentStart();
877
+ tracker.turnStart();
878
+ tracker.messageUpdate(assistant(10, 1, { responseId: "anchored" }));
879
+ tracker.messageUpdate(
880
+ assistant(10, 1, { responseId: "anchored" }),
881
+ delta("text_delta", 0, "abcdefghijkl", "abcdefghijkl"),
882
+ );
883
+ expect(tracker.messageEnd(assistant(-1, 9, { responseId: "anchored" }))).toEqual(
884
+ accepted({ input: 10, output: 1 }, "last-snapshot", {
885
+ input: 10,
886
+ output: 4,
887
+ outputApproximate: true,
888
+ }),
889
+ );
890
+ tracker.agentEnd();
891
+ expect(tracker.settle(true)?.summary).toMatchObject({ input: 10, output: 1 });
892
+ });
893
+
894
+ it("commits an unfinalized valid snapshot before opening the next response", () => {
895
+ const tracker = new InteractionMetricsTracker();
896
+ tracker.agentStart();
897
+ tracker.turnStart();
898
+ tracker.messageUpdate(assistant(10, 1, { responseId: "a" }));
899
+ tracker.turnStart();
900
+ expect(tracker.messageEnd(assistant(20, 2, { responseId: "b" }))).toEqual(
901
+ accepted({
902
+ input: 30,
903
+ output: 3,
904
+ }),
905
+ );
906
+ });
907
+
908
+ it("rejects mismatched updates and finals without modifying or closing the active response", () => {
909
+ const tracker = new InteractionMetricsTracker();
910
+ tracker.agentStart(0);
911
+ tracker.turnStart(0);
912
+ tracker.messageUpdate(
913
+ assistant(10, 1, { responseId: "a" }),
914
+ {
915
+ type: "thinking_start",
916
+ contentIndex: 0,
917
+ partial: assistant(0, 0),
918
+ } as never,
919
+ 0,
920
+ );
921
+ expect(tracker.messageUpdate(assistant(99, 9, { responseId: "b" }), undefined, 10)).toEqual({
922
+ displayTokens: { input: 10, output: 1, outputApproximate: false },
923
+ usageChanged: false,
924
+ thoughtChanged: false,
925
+ });
926
+ expect(tracker.messageEnd(assistant(99, 9, { responseId: "b" }), 20)).toEqual({
927
+ status: "rejected",
928
+ });
929
+ expect(tracker.currentThought(30)).toEqual({ durationMs: 30, active: true });
930
+ expect(tracker.messageEnd(assistant(12, 2, { responseId: "a" }), 40)).toEqual(
931
+ accepted({
932
+ input: 12,
933
+ output: 2,
934
+ }),
935
+ );
936
+ expect(tracker.currentThought(50)).toEqual({ durationMs: 40, active: false });
937
+ });
938
+
939
+ it("does not let a mismatched malformed final close or commit the active response", () => {
940
+ const tracker = new InteractionMetricsTracker();
941
+ tracker.agentStart(0);
942
+ tracker.turnStart(0);
943
+ tracker.messageUpdate(
944
+ assistant(10, 1, { responseId: "a" }),
945
+ {
946
+ type: "thinking_start",
947
+ contentIndex: 0,
948
+ partial: assistant(0, 0),
949
+ } as never,
950
+ 0,
951
+ );
952
+ expect(tracker.messageEnd(assistant(-1, 9, { responseId: "b" }), 20)).toEqual({
953
+ status: "rejected",
954
+ });
955
+ expect(tracker.currentTokens()).toEqual({ input: 10, output: 1 });
956
+ expect(tracker.currentThought(30)).toEqual({ durationMs: 30, active: true });
957
+ expect(tracker.messageEnd(assistant(-1, 9, { responseId: "a" }), 40)).toEqual(
958
+ accepted({ input: 10, output: 1 }, "last-snapshot"),
959
+ );
960
+ expect(tracker.currentTokens()).toEqual({ input: 10, output: 1 });
961
+ expect(tracker.currentThought(50)).toEqual({ durationMs: 40, active: false });
962
+ tracker.turnStart();
963
+ expect(tracker.messageEnd(assistant(20, 2, { responseId: "b" }))).toEqual(
964
+ accepted({
965
+ input: 30,
966
+ output: 3,
967
+ }),
968
+ );
969
+ });
970
+
971
+ it("does not let a stale committed ID promote a fresh response-less turn slot", () => {
972
+ const tracker = new InteractionMetricsTracker();
973
+ tracker.agentStart();
974
+ tracker.turnStart();
975
+ tracker.messageEnd(assistant(10, 1, { responseId: "a" }));
976
+ tracker.turnStart();
977
+ expect(tracker.messageUpdate(assistant(99, 9, { responseId: "a" })).usageChanged).toBe(false);
978
+ expect(tracker.messageEnd(assistant(20, 2, { responseId: "b" }))).toEqual(
979
+ accepted({
980
+ input: 30,
981
+ output: 3,
982
+ }),
983
+ );
984
+ });
985
+
986
+ it("keeps authorized live and exact accounting at estimator and response-ID caps", () => {
987
+ const tracker = new InteractionMetricsTracker();
988
+ tracker.agentStart();
989
+ for (let index = 0; index < 128; index++) {
990
+ tracker.turnStart();
991
+ tracker.messageUpdate(
992
+ assistant(0, 0, { responseId: `estimated-${index}` }),
993
+ delta("text_delta", 0, "abcd", "abcd"),
994
+ );
995
+ tracker.messageEnd(assistant(1, 1, { responseId: `estimated-${index}` }));
996
+ }
997
+ tracker.turnStart();
998
+ expect(
999
+ tracker.messageUpdate(
1000
+ assistant(0, 0, { responseId: "exact-only" }),
1001
+ delta("text_delta", 0, "abcd", "abcd"),
1002
+ ).displayTokens,
1003
+ ).toEqual({ input: 128, output: 128, outputApproximate: true });
1004
+ expect(tracker.messageEnd(assistant(2, 3, { responseId: "exact-only" }))).toEqual(
1005
+ accepted({ input: 130, output: 131 }),
1006
+ );
1007
+
1008
+ for (let index = 129; index < 256; index++) {
1009
+ tracker.turnStart();
1010
+ tracker.messageEnd(assistant(1, 1, { responseId: `exact-${index}` }));
1011
+ }
1012
+ expect(tracker.diagnostics().responseIds).toBe(256);
1013
+ tracker.turnStart();
1014
+ expect(tracker.messageEnd(assistant(3, 4, { responseId: "overflow-id" }))).toEqual(
1015
+ accepted({ input: 260, output: 262 }),
1016
+ );
1017
+ tracker.turnStart();
1018
+ expect(
1019
+ tracker.messageUpdate(
1020
+ assistant(99, 99, { responseId: "overflow-id" }),
1021
+ delta("text_delta", 0, "replay", "replay"),
1022
+ ),
1023
+ ).toMatchObject({ usageChanged: false });
1024
+ expect(tracker.messageEnd(assistant(99, 99, { responseId: "overflow-id" }))).toEqual({
1025
+ status: "duplicate",
1026
+ });
1027
+ expect(tracker.messageEnd(assistant(5, 6, { responseId: "fresh-after-overflow" }))).toEqual(
1028
+ accepted({ input: 265, output: 268 }),
1029
+ );
1030
+ expect(tracker.messageEnd(assistant(99, 99, { responseId: "fresh-after-overflow" }))).toEqual({
1031
+ status: "rejected",
1032
+ });
1033
+ expect(tracker.messageEnd(assistant(99, 99))).toEqual({ status: "rejected" });
1034
+ expect(tracker.messageEnd(assistant(99, 99, { responseId: "overflow-unsolicited" }))).toEqual({
1035
+ status: "rejected",
1036
+ });
1037
+
1038
+ tracker.turnStart();
1039
+ expect(tracker.messageEnd(assistant(1, 1))).toEqual(accepted({ input: 266, output: 269 }));
1040
+ expect(tracker.messageEnd(assistant(99, 99))).toEqual({ status: "rejected" });
1041
+
1042
+ tracker.turnStart();
1043
+ expect(
1044
+ tracker.messageUpdate(
1045
+ assistant(4, 2, { responseId: "authorized-at-cap" }),
1046
+ delta("text_delta", 0, "abcdefgh", "abcdefgh"),
1047
+ ).displayTokens,
1048
+ ).toEqual({ input: 270, output: 271, outputApproximate: true });
1049
+ expect(tracker.messageUpdate(assistant(99, 99, { responseId: "mismatch" })).usageChanged).toBe(
1050
+ false,
1051
+ );
1052
+ expect(tracker.messageEnd(assistant(99, 99, { responseId: "mismatch" }))).toEqual({
1053
+ status: "rejected",
1054
+ });
1055
+ expect(tracker.messageEnd(assistant(9, 9, { responseId: "authorized-at-cap" }))).toEqual(
1056
+ accepted({ input: 275, output: 278 }),
1057
+ );
1058
+ expect(tracker.diagnostics().responseIds).toBe(256);
1059
+ expect(tracker.messageEnd(assistant(99, 99, { responseId: "authorized-at-cap" }))).toEqual({
1060
+ status: "rejected",
1061
+ });
1062
+ expect(
1063
+ tracker.messageUpdate(assistant(99, 99, { responseId: "unknown" }), {
1064
+ type: "unknown",
1065
+ } as never).usageChanged,
1066
+ ).toBe(false);
1067
+ });
1068
+
1069
+ it("bounds run state while preserving folded exact totals", () => {
1070
+ const tracker = new InteractionMetricsTracker();
1071
+ for (let index = 0; index < 40; index++) {
1072
+ tracker.agentStart(index);
1073
+ tracker.turnStart(index);
1074
+ tracker.messageEnd(assistant(1, 2, { responseId: `run-${index}` }));
1075
+ tracker.agentEnd(index + 1);
1076
+ }
1077
+ expect(tracker.currentTokens()).toEqual({ input: 40, output: 80 });
1078
+ expect(tracker.diagnostics().runs).toBeLessThanOrEqual(32);
1079
+ });
1080
+
1081
+ it("ignores late events after agent_end and accepts them only after a real next agent_start", () => {
1082
+ const tracker = new InteractionMetricsTracker();
1083
+ tracker.agentStart(0);
1084
+ tracker.turnStart(0);
1085
+ tracker.messageEnd(assistant(10, 1, { responseId: "a" }));
1086
+ tracker.agentEnd(10);
1087
+ expect(tracker.messageUpdate(assistant(90, 9, { responseId: "late" })).usageChanged).toBe(
1088
+ false,
1089
+ );
1090
+ expect(tracker.messageEnd(assistant(90, 9, { responseId: "late" }))).toEqual({
1091
+ status: "rejected",
1092
+ });
1093
+ expect(tracker.currentTokens()).toEqual({ input: 10, output: 1 });
1094
+ tracker.agentStart(20);
1095
+ tracker.turnStart(20);
1096
+ expect(tracker.messageEnd(assistant(20, 2, { responseId: "late" }))).toEqual(
1097
+ accepted({
1098
+ input: 30,
1099
+ output: 3,
1100
+ }),
1101
+ );
1102
+ });
1103
+
1104
+ it("retains a live response ID and snapshot across non-idle partitioning", () => {
1105
+ const tracker = new InteractionMetricsTracker();
1106
+ tracker.agentStart(1_000);
1107
+ tracker.turnStart();
1108
+ tracker.messageEnd(assistant(10, 1, { responseId: "settled" }));
1109
+ tracker.agentEnd();
1110
+ tracker.agentStart(5_000);
1111
+ tracker.turnStart();
1112
+ tracker.messageUpdate(assistant(7, 2, { responseId: " live " }));
1113
+ expect(tracker.settle(false, 6_000)).toEqual({
1114
+ summary: { durationMs: 5_000, thoughtDurationMs: 0, input: 10, output: 1 },
1115
+ nextStartedAt: 5_000,
1116
+ nextTokens: { input: 7, output: 2, outputApproximate: false },
1117
+ nextThought: { durationMs: 0, active: false },
1118
+ });
1119
+ expect(tracker.messageEnd(assistant(9, 3, { responseId: "live", timestamp: 99 }))).toEqual(
1120
+ accepted({
1121
+ input: 9,
1122
+ output: 3,
1123
+ }),
1124
+ );
1125
+ expect(tracker.messageEnd(assistant(9, 3, { responseId: "live" }))).toEqual({
1126
+ status: "rejected",
1127
+ });
1128
+ });
1129
+ });
1130
+
1131
+ describe("thought duration tracking", () => {
1132
+ const thinking = (
1133
+ type: "thinking_start" | "thinking_delta" | "thinking_end",
1134
+ contentIndex: number,
1135
+ ) =>
1136
+ ({
1137
+ type,
1138
+ contentIndex,
1139
+ partial: assistant(0, 0),
1140
+ ...(type === "thinking_delta" ? { delta: "x" } : {}),
1141
+ ...(type === "thinking_end" ? { content: "x" } : {}),
1142
+ }) as never;
1143
+
1144
+ it("retains a bounded exact lower bound after adversarial subtraction", () => {
1145
+ const result = subtractThoughtIntervalsWithinCap(
1146
+ [{ start: 0, end: 513 }],
1147
+ Array.from({ length: 257 }, (_, index) => ({
1148
+ start: index * 2 + 1,
1149
+ end: index * 2 + 2,
1150
+ })),
1151
+ );
1152
+ expect(result.incomplete).toBe(true);
1153
+ expect(result.intervals).toHaveLength(256);
1154
+ expect(result.intervals[0]).toEqual({ start: 0, end: 1 });
1155
+ expect(result.intervals.at(-1)).toEqual({ start: 510, end: 511 });
1156
+ for (let index = 0; index < result.intervals.length; index++) {
1157
+ const interval = result.intervals[index];
1158
+ expect(interval.end).toBeGreaterThan(interval.start);
1159
+ expect(interval.start).toBeGreaterThanOrEqual(0);
1160
+ expect(interval.end).toBeLessThanOrEqual(513);
1161
+ if (index > 0) expect(interval.start).toBeGreaterThan(result.intervals[index - 1].end);
1162
+ }
1163
+ });
1164
+
1165
+ it("counts the union of overlapping blocks and updates open elapsed from receipt time", () => {
1166
+ let now = 1_000;
1167
+ const tracker = new InteractionMetricsTracker(() => now);
1168
+ tracker.agentStart();
1169
+ tracker.turnStart();
1170
+ tracker.messageUpdate(assistant(0, 0, { responseId: "a" }), thinking("thinking_start", 0));
1171
+ now = 2_000;
1172
+ tracker.messageUpdate(assistant(0, 0, { responseId: "a" }), thinking("thinking_start", 1));
1173
+ now = 3_000;
1174
+ expect(tracker.currentThought()).toEqual({ durationMs: 2_000, active: true });
1175
+ tracker.messageUpdate(assistant(0, 0, { responseId: "a" }), thinking("thinking_end", 0));
1176
+ now = 4_000;
1177
+ tracker.messageUpdate(assistant(0, 0, { responseId: "a" }), thinking("thinking_end", 1));
1178
+ expect(tracker.currentThought()).toEqual({ durationMs: 3_000, active: false });
1179
+ });
1180
+
1181
+ it("keeps an exact lower-bound union after the strict thought interval cap", () => {
1182
+ const tracker = new InteractionMetricsTracker();
1183
+ tracker.agentStart(0);
1184
+ for (let index = 0; index < 256; index++) {
1185
+ tracker.turnStart(index * 4);
1186
+ tracker.messageUpdate(
1187
+ assistant(0, 0, { responseId: `thought-${index}` }),
1188
+ thinking("thinking_start", 0),
1189
+ index * 4,
1190
+ );
1191
+ tracker.messageUpdate(
1192
+ assistant(0, 0, { responseId: `thought-${index}` }),
1193
+ thinking("thinking_end", 0),
1194
+ index * 4 + 1,
1195
+ );
1196
+ tracker.messageEnd(assistant(0, 0, { responseId: `thought-${index}` }), index * 4 + 1);
1197
+ }
1198
+ expect(tracker.diagnostics().thoughtIntervals).toBe(256);
1199
+ expect(tracker.currentThought(2_000)).toEqual({ durationMs: 256, active: false });
1200
+
1201
+ tracker.turnStart(2_000);
1202
+ tracker.messageUpdate(
1203
+ assistant(0, 0, { responseId: "overflow" }),
1204
+ thinking("thinking_start", 0),
1205
+ 2_000,
1206
+ );
1207
+ tracker.messageUpdate(
1208
+ assistant(0, 0, { responseId: "overflow" }),
1209
+ thinking("thinking_end", 0),
1210
+ 2_001,
1211
+ );
1212
+ expect(tracker.diagnostics().thoughtIntervals).toBe(256);
1213
+ expect(tracker.currentThought(2_001)).toEqual({ durationMs: 256, active: false });
1214
+
1215
+ tracker.turnStart(500);
1216
+ tracker.messageUpdate(
1217
+ assistant(0, 0, { responseId: "covered-overlap" }),
1218
+ thinking("thinking_start", 0),
1219
+ 500,
1220
+ );
1221
+ tracker.messageUpdate(
1222
+ assistant(0, 0, { responseId: "covered-overlap" }),
1223
+ thinking("thinking_end", 0),
1224
+ 505,
1225
+ );
1226
+ expect(tracker.diagnostics().thoughtIntervals).toBe(256);
1227
+ expect(tracker.currentThought(2_100)).toEqual({ durationMs: 256, active: false });
1228
+
1229
+ tracker.turnStart(2_002);
1230
+ tracker.messageUpdate(
1231
+ assistant(0, 0, { responseId: "gap-after-incomplete" }),
1232
+ thinking("thinking_start", 0),
1233
+ 2_002,
1234
+ );
1235
+ tracker.messageUpdate(
1236
+ assistant(0, 0, { responseId: "gap-after-incomplete" }),
1237
+ thinking("thinking_end", 0),
1238
+ 2_003,
1239
+ );
1240
+ expect(tracker.diagnostics().thoughtIntervals).toBe(256);
1241
+ expect(tracker.currentThought(2_100)).toEqual({ durationMs: 256, active: false });
1242
+ expect(tracker.settle(true, 2_100)?.summary.thoughtDurationMs).toBe(256);
1243
+ expect(tracker.currentThought()).toBeUndefined();
1244
+ });
1245
+
1246
+ it("handles duplicates, delta without start, missing ends, and reused content indexes", () => {
1247
+ let now = 10;
1248
+ const tracker = new InteractionMetricsTracker(() => now);
1249
+ tracker.agentStart();
1250
+ tracker.turnStart();
1251
+ tracker.messageUpdate(assistant(0, 0, { responseId: "a" }), thinking("thinking_delta", 0));
1252
+ tracker.messageUpdate(assistant(0, 0, { responseId: "a" }), thinking("thinking_start", 0));
1253
+ now = 20;
1254
+ tracker.messageEnd(assistant(1, 1, { responseId: "a" }));
1255
+ tracker.turnStart();
1256
+ now = 30;
1257
+ tracker.messageUpdate(assistant(0, 0, { responseId: "b" }), thinking("thinking_start", 0));
1258
+ now = 40;
1259
+ tracker.agentEnd();
1260
+ expect(tracker.currentThought()).toEqual({ durationMs: 20, active: false });
1261
+ });
1262
+
1263
+ it("ignores late thought after agent_end", () => {
1264
+ const tracker = new InteractionMetricsTracker();
1265
+ tracker.agentStart(0);
1266
+ tracker.agentEnd(10);
1267
+ const result = tracker.messageUpdate(
1268
+ assistant(0, 0, { responseId: "late" }),
1269
+ thinking("thinking_start", 0),
1270
+ 20,
1271
+ );
1272
+ expect(result.thoughtChanged).toBe(false);
1273
+ expect(tracker.currentThought(30)).toEqual({ durationMs: 0, active: false });
1274
+ });
1275
+
1276
+ it("partitions ended thought without double-counting a concurrent retained run", () => {
1277
+ const tracker = new InteractionMetricsTracker();
1278
+ tracker.agentStart(0);
1279
+ tracker.turnStart(0);
1280
+ tracker.messageUpdate(assistant(0, 0, { responseId: "old" }), thinking("thinking_start", 0), 0);
1281
+ tracker.agentEnd(100);
1282
+ tracker.agentStart(50);
1283
+ tracker.turnStart(50);
1284
+ tracker.messageUpdate(
1285
+ assistant(0, 0, { responseId: "live" }),
1286
+ thinking("thinking_start", 0),
1287
+ 50,
1288
+ );
1289
+ const split = tracker.settle(false, 100);
1290
+ expect(split?.summary.thoughtDurationMs).toBe(100);
1291
+ expect(split?.nextThought).toEqual({ durationMs: 0, active: true });
1292
+ expect(tracker.currentThought(150)).toEqual({ durationMs: 50, active: true });
1293
+ });
1294
+ });
1295
+
1296
+ describe("turn summary entry", () => {
1297
+ it("validates versioned numeric data and formats exact persistent text", () => {
1298
+ const data = { version: 1 as const, durationMs: 42_999, input: 27_000, output: 1_400 };
1299
+ expect(isTurnSummaryData(data)).toBe(true);
1300
+ expect(formatTurnSummary(data)).toBe(" Turn took 42s · ↑27k ↓1.4k");
1301
+ const rendered = renderTurnSummaryEntry(
1302
+ { data },
1303
+ {},
1304
+ { fg: (_color: string, text: string) => `\x1b[2m${text}\x1b[0m` },
1305
+ );
1306
+ expect(stripTerminalSequences(rendered?.render(100).join("\n") ?? "").trimEnd()).toBe(
1307
+ " Turn took 42s · ↑27k ↓1.4k",
1308
+ );
1309
+ });
1310
+
1311
+ it("validates and renders v2 with its persisted style independently of the current theme", () => {
1312
+ const data = {
1313
+ version: 2 as const,
1314
+ durationMs: 1,
1315
+ input: 2,
1316
+ output: 3,
1317
+ stylePrefix: "\x1b[1;38;5;202m",
1318
+ };
1319
+ expect(isTurnSummaryData(data)).toBe(true);
1320
+ const rendered = renderTurnSummaryEntry(
1321
+ { data },
1322
+ { colorSource: "theme", workingLineHigh: "green" },
1323
+ { fg: () => "changed-theme" },
1324
+ )?.render(100)[0];
1325
+ expect(rendered).toContain("\x1b[1;38;5;202m Turn took 0s · ↑2 ↓3\x1b[0m");
1326
+ expect(rendered).not.toContain("changed-theme");
1327
+ });
1328
+
1329
+ it("validates v3 exactly and formats thought while omitting zero", () => {
1330
+ const data = {
1331
+ version: 3 as const,
1332
+ durationMs: 56_999,
1333
+ thoughtDurationMs: 10_999,
1334
+ input: 7_100,
1335
+ output: 779,
1336
+ stylePrefix: "\x1b[1;36m",
1337
+ };
1338
+ expect(isTurnSummaryData(data)).toBe(true);
1339
+ expect(formatTurnSummary(data)).toBe(" Turn took 56s · thought for 10s · ↑7.1k ↓779");
1340
+ expect(formatTurnSummary({ ...data, thoughtDurationMs: 0 })).toBe(
1341
+ " Turn took 56s · ↑7.1k ↓779",
1342
+ );
1343
+ expect(isTurnSummaryData({ ...data, thoughtDurationMs: -1 })).toBe(false);
1344
+ expect(isTurnSummaryData({ ...data, extra: true })).toBe(false);
1345
+ expect(isTurnSummaryData({ ...data, stylePrefix: "\x1b]8;;bad\x07" })).toBe(false);
1346
+ });
1347
+
1348
+ it("accepts four safe SGR sequences but rejects a fifth sequence and injected controls", () => {
1349
+ const safe = {
1350
+ version: 2 as const,
1351
+ durationMs: 1,
1352
+ input: 2,
1353
+ output: 3,
1354
+ stylePrefix: "\x1b[38;5;202m\x1b[4m\x1b[3m\x1b[1m",
1355
+ };
1356
+ expect(isTurnSummaryData(safe)).toBe(true);
1357
+ const rendered = renderTurnSummaryEntry({ data: safe }, {}, { fg: (_color, text) => text })
1358
+ ?.render(100)
1359
+ .join("\n");
1360
+ expect(rendered).toContain(`${safe.stylePrefix} Turn took 0s · ↑2 ↓3\x1b[0m`);
1361
+ expect(isTurnSummaryData({ ...safe, stylePrefix: `${safe.stylePrefix}\x1b[2m` })).toBe(false);
1362
+ expect(isTurnSummaryData({ ...safe, stylePrefix: `${safe.stylePrefix}\x1b]8;;bad\x07` })).toBe(
1363
+ false,
1364
+ );
1365
+ expect(
1366
+ isTurnSummaryData({
1367
+ ...safe,
1368
+ stylePrefix: "\x1b[38;2;0000000255;0000000255;0000000255m".repeat(4),
1369
+ }),
1370
+ ).toBe(false);
1371
+ });
1372
+
1373
+ it.each([
1374
+ null,
1375
+ {},
1376
+ { version: 2, durationMs: 1, input: 1, output: 1 },
1377
+ { version: 2, durationMs: 1, input: 1, output: 1, stylePrefix: "\x1b[0m" },
1378
+ { version: 2, durationMs: 1, input: 1, output: 1, stylePrefix: "\x1b[31mprint" },
1379
+ { version: 2, durationMs: 1, input: 1, output: 1, stylePrefix: "\x1b]8;;bad\x07" },
1380
+ { version: 2, durationMs: 1, input: 1, output: 1, stylePrefix: "\x1b[2J" },
1381
+ { version: 2, durationMs: 1, input: 1, output: 1, stylePrefix: "\x1b[31m", extra: true },
1382
+ { version: 1, durationMs: -1, input: 1, output: 1 },
1383
+ { version: 1, durationMs: 1, input: 1.5, output: 1 },
1384
+ { version: 1, durationMs: 1, input: 1, output: 1, extra: true },
1385
+ ])("rejects invalid renderer data %#", (data) => {
1386
+ expect(renderTurnSummaryEntry({ data }, {}, { fg: (_color, text) => text })).toBeUndefined();
1387
+ });
1388
+
1389
+ it("proves custom entries persist while contributing zero model messages", () => {
1390
+ const entry = {
1391
+ type: "custom" as const,
1392
+ id: "summary",
1393
+ parentId: null,
1394
+ timestamp: new Date(0).toISOString(),
1395
+ customType: TURN_SUMMARY_ENTRY_TYPE,
1396
+ data: { version: 1, durationMs: 1, input: 0, output: 0 },
1397
+ };
1398
+ const entries = [entry];
1399
+ expect(entries[0]?.data).toEqual(entry.data);
1400
+ expect(buildSessionContext(entries).messages).toEqual([]);
1401
+ });
1402
+ });