@genesislcap/ai-assistant 15.19.0 → 15.19.1

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 (288) hide show
  1. package/package.json +17 -18
  2. package/api-extractor.json +0 -11
  3. package/dist/dts/components/chat-driver/chat-driver.compact.test.d.ts +0 -2
  4. package/dist/dts/components/chat-driver/chat-driver.compact.test.d.ts.map +0 -1
  5. package/dist/dts/components/chat-driver/chat-driver.invocation-scope.test.d.ts +0 -2
  6. package/dist/dts/components/chat-driver/chat-driver.invocation-scope.test.d.ts.map +0 -1
  7. package/dist/dts/components/chat-driver/chat-driver.test.d.ts +0 -2
  8. package/dist/dts/components/chat-driver/chat-driver.test.d.ts.map +0 -1
  9. package/dist/dts/components/chat-driver/chat-driver.thinking-policy.test.d.ts +0 -2
  10. package/dist/dts/components/chat-driver/chat-driver.thinking-policy.test.d.ts.map +0 -1
  11. package/dist/dts/components/chat-driver/chat-driver.trace-capture.test.d.ts +0 -2
  12. package/dist/dts/components/chat-driver/chat-driver.trace-capture.test.d.ts.map +0 -1
  13. package/dist/dts/components/chat-driver/chat-driver.turn-usage.test.d.ts +0 -2
  14. package/dist/dts/components/chat-driver/chat-driver.turn-usage.test.d.ts.map +0 -1
  15. package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.test.d.ts +0 -2
  16. package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.test.d.ts.map +0 -1
  17. package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts +0 -2
  18. package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts.map +0 -1
  19. package/dist/dts/components/orchestrating-driver/orchestrating-driver.cost.test.d.ts +0 -2
  20. package/dist/dts/components/orchestrating-driver/orchestrating-driver.cost.test.d.ts.map +0 -1
  21. package/dist/dts/components/orchestrating-driver/orchestrating-driver.pin.test.d.ts +0 -2
  22. package/dist/dts/components/orchestrating-driver/orchestrating-driver.pin.test.d.ts.map +0 -1
  23. package/dist/dts/components/settings-modal/settings-modal.styles.test.d.ts +0 -2
  24. package/dist/dts/components/settings-modal/settings-modal.styles.test.d.ts.map +0 -1
  25. package/dist/dts/components/settings-modal/settings-modal.template.test.d.ts +0 -2
  26. package/dist/dts/components/settings-modal/settings-modal.template.test.d.ts.map +0 -1
  27. package/dist/dts/config/define-stateful-agent.test.d.ts +0 -2
  28. package/dist/dts/config/define-stateful-agent.test.d.ts.map +0 -1
  29. package/dist/dts/config/validate-providers.test.d.ts +0 -2
  30. package/dist/dts/config/validate-providers.test.d.ts.map +0 -1
  31. package/dist/dts/main/blocked-state.test.d.ts +0 -2
  32. package/dist/dts/main/blocked-state.test.d.ts.map +0 -1
  33. package/dist/dts/main/budget-meter.test.d.ts +0 -2
  34. package/dist/dts/main/budget-meter.test.d.ts.map +0 -1
  35. package/dist/dts/main/cost-session-banking.test.d.ts +0 -2
  36. package/dist/dts/main/cost-session-banking.test.d.ts.map +0 -1
  37. package/dist/dts/main/file-attachments.test.d.ts +0 -2
  38. package/dist/dts/main/file-attachments.test.d.ts.map +0 -1
  39. package/dist/dts/main/interaction-cost.test.d.ts +0 -2
  40. package/dist/dts/main/interaction-cost.test.d.ts.map +0 -1
  41. package/dist/dts/main/main.styles.test.d.ts +0 -2
  42. package/dist/dts/main/main.styles.test.d.ts.map +0 -1
  43. package/dist/dts/main/persistence-broken-sources.test.d.ts +0 -2
  44. package/dist/dts/main/persistence-broken-sources.test.d.ts.map +0 -1
  45. package/dist/dts/main/popout-interaction-gate.test.d.ts +0 -2
  46. package/dist/dts/main/popout-interaction-gate.test.d.ts.map +0 -1
  47. package/dist/dts/state/ai-assistant-slice.test.d.ts +0 -2
  48. package/dist/dts/state/ai-assistant-slice.test.d.ts.map +0 -1
  49. package/dist/dts/state/debug-event-log.test.d.ts +0 -2
  50. package/dist/dts/state/debug-event-log.test.d.ts.map +0 -1
  51. package/dist/dts/state/driver-registry.test.d.ts +0 -2
  52. package/dist/dts/state/driver-registry.test.d.ts.map +0 -1
  53. package/dist/dts/state/interaction-context.test.d.ts +0 -2
  54. package/dist/dts/state/interaction-context.test.d.ts.map +0 -1
  55. package/dist/dts/state/persistence/diagnostics-cursors.test.d.ts +0 -2
  56. package/dist/dts/state/persistence/diagnostics-cursors.test.d.ts.map +0 -1
  57. package/dist/dts/state/persistence/diagnostics.test.d.ts +0 -2
  58. package/dist/dts/state/persistence/diagnostics.test.d.ts.map +0 -1
  59. package/dist/dts/state/persistence/session-persistence-provider.test.d.ts +0 -2
  60. package/dist/dts/state/persistence/session-persistence-provider.test.d.ts.map +0 -1
  61. package/dist/dts/state/persistence/session-persistence.integration.test.d.ts +0 -2
  62. package/dist/dts/state/persistence/session-persistence.integration.test.d.ts.map +0 -1
  63. package/dist/dts/state/persistence/session-persister.test.d.ts +0 -2
  64. package/dist/dts/state/persistence/session-persister.test.d.ts.map +0 -1
  65. package/dist/dts/state/persistence/session-snapshot.test.d.ts +0 -2
  66. package/dist/dts/state/persistence/session-snapshot.test.d.ts.map +0 -1
  67. package/dist/dts/state/persistence/stateful-restore.e2e.test.d.ts +0 -2
  68. package/dist/dts/state/persistence/stateful-restore.e2e.test.d.ts.map +0 -1
  69. package/dist/dts/utils/animation-exclusivity.test.d.ts +0 -2
  70. package/dist/dts/utils/animation-exclusivity.test.d.ts.map +0 -1
  71. package/dist/dts/utils/collect-session-models.test.d.ts +0 -2
  72. package/dist/dts/utils/collect-session-models.test.d.ts.map +0 -1
  73. package/dist/dts/utils/condense-history.test.d.ts +0 -2
  74. package/dist/dts/utils/condense-history.test.d.ts.map +0 -1
  75. package/dist/dts/utils/cost-session-history.test.d.ts +0 -2
  76. package/dist/dts/utils/cost-session-history.test.d.ts.map +0 -1
  77. package/dist/dts/utils/derive-cost-session-title.test.d.ts +0 -2
  78. package/dist/dts/utils/derive-cost-session-title.test.d.ts.map +0 -1
  79. package/dist/dts/utils/flatten-sub-agent-messages.test.d.ts +0 -2
  80. package/dist/dts/utils/flatten-sub-agent-messages.test.d.ts.map +0 -1
  81. package/dist/dts/utils/history-transform.test.d.ts +0 -2
  82. package/dist/dts/utils/history-transform.test.d.ts.map +0 -1
  83. package/dist/dts/utils/message-partition.test.d.ts +0 -2
  84. package/dist/dts/utils/message-partition.test.d.ts.map +0 -1
  85. package/dist/dts/utils/resolve-cost-history-config.test.d.ts +0 -2
  86. package/dist/dts/utils/resolve-cost-history-config.test.d.ts.map +0 -1
  87. package/dist/dts/utils/resolve-preference-baseline.test.d.ts +0 -2
  88. package/dist/dts/utils/resolve-preference-baseline.test.d.ts.map +0 -1
  89. package/dist/dts/utils/strip-agent-handlers.test.d.ts +0 -2
  90. package/dist/dts/utils/strip-agent-handlers.test.d.ts.map +0 -1
  91. package/dist/dts/utils/sum-costs.test.d.ts +0 -2
  92. package/dist/dts/utils/sum-costs.test.d.ts.map +0 -1
  93. package/dist/dts/utils/sum-tokens.test.d.ts +0 -2
  94. package/dist/dts/utils/sum-tokens.test.d.ts.map +0 -1
  95. package/dist/dts/utils/sum-usage.test.d.ts +0 -2
  96. package/dist/dts/utils/sum-usage.test.d.ts.map +0 -1
  97. package/dist/dts/utils/usage-rows.test.d.ts +0 -2
  98. package/dist/dts/utils/usage-rows.test.d.ts.map +0 -1
  99. package/dist/esm/components/chat-driver/chat-driver.compact.test.js +0 -93
  100. package/dist/esm/components/chat-driver/chat-driver.invocation-scope.test.js +0 -238
  101. package/dist/esm/components/chat-driver/chat-driver.test.js +0 -2770
  102. package/dist/esm/components/chat-driver/chat-driver.thinking-policy.test.js +0 -137
  103. package/dist/esm/components/chat-driver/chat-driver.trace-capture.test.js +0 -200
  104. package/dist/esm/components/chat-driver/chat-driver.turn-usage.test.js +0 -268
  105. package/dist/esm/components/chat-interaction-wrapper/chat-interaction-wrapper.test.js +0 -104
  106. package/dist/esm/components/orchestrating-driver/orchestrating-driver.budget.test.js +0 -312
  107. package/dist/esm/components/orchestrating-driver/orchestrating-driver.cost.test.js +0 -76
  108. package/dist/esm/components/orchestrating-driver/orchestrating-driver.pin.test.js +0 -61
  109. package/dist/esm/components/settings-modal/settings-modal.styles.test.js +0 -80
  110. package/dist/esm/components/settings-modal/settings-modal.template.test.js +0 -91
  111. package/dist/esm/config/define-stateful-agent.test.js +0 -53
  112. package/dist/esm/config/validate-providers.test.js +0 -100
  113. package/dist/esm/main/blocked-state.test.js +0 -1065
  114. package/dist/esm/main/budget-meter.test.js +0 -359
  115. package/dist/esm/main/cost-session-banking.test.js +0 -538
  116. package/dist/esm/main/file-attachments.test.js +0 -164
  117. package/dist/esm/main/interaction-cost.test.js +0 -107
  118. package/dist/esm/main/main.styles.test.js +0 -161
  119. package/dist/esm/main/persistence-broken-sources.test.js +0 -180
  120. package/dist/esm/main/popout-interaction-gate.test.js +0 -54
  121. package/dist/esm/state/ai-assistant-slice.test.js +0 -229
  122. package/dist/esm/state/debug-event-log.test.js +0 -117
  123. package/dist/esm/state/driver-registry.test.js +0 -55
  124. package/dist/esm/state/interaction-context.test.js +0 -78
  125. package/dist/esm/state/persistence/diagnostics-cursors.test.js +0 -30
  126. package/dist/esm/state/persistence/diagnostics.test.js +0 -280
  127. package/dist/esm/state/persistence/session-persistence-provider.test.js +0 -110
  128. package/dist/esm/state/persistence/session-persistence.integration.test.js +0 -113
  129. package/dist/esm/state/persistence/session-persister.test.js +0 -613
  130. package/dist/esm/state/persistence/session-snapshot.test.js +0 -256
  131. package/dist/esm/state/persistence/stateful-restore.e2e.test.js +0 -158
  132. package/dist/esm/utils/animation-exclusivity.test.js +0 -52
  133. package/dist/esm/utils/collect-session-models.test.js +0 -53
  134. package/dist/esm/utils/condense-history.test.js +0 -547
  135. package/dist/esm/utils/cost-session-history.test.js +0 -174
  136. package/dist/esm/utils/derive-cost-session-title.test.js +0 -37
  137. package/dist/esm/utils/flatten-sub-agent-messages.test.js +0 -139
  138. package/dist/esm/utils/history-transform.test.js +0 -158
  139. package/dist/esm/utils/message-partition.test.js +0 -90
  140. package/dist/esm/utils/resolve-cost-history-config.test.js +0 -32
  141. package/dist/esm/utils/resolve-preference-baseline.test.js +0 -69
  142. package/dist/esm/utils/strip-agent-handlers.test.js +0 -81
  143. package/dist/esm/utils/sum-costs.test.js +0 -114
  144. package/dist/esm/utils/sum-tokens.test.js +0 -51
  145. package/dist/esm/utils/sum-usage.test.js +0 -164
  146. package/dist/esm/utils/usage-rows.test.js +0 -189
  147. package/dist/tsconfig.tsbuildinfo +0 -1
  148. package/license.txt +0 -46
  149. package/sandbox/tsconfig.json +0 -8
  150. package/src/channel/ai-activity-bus.ts +0 -209
  151. package/src/channel/ai-activity-channel.ts +0 -146
  152. package/src/chat-driver-node.ts +0 -159
  153. package/src/components/activity-halo/activity-halo.ts +0 -106
  154. package/src/components/agent-picker/agent-picker.constants.ts +0 -2
  155. package/src/components/agent-picker/agent-picker.styles.ts +0 -24
  156. package/src/components/agent-picker/agent-picker.template.ts +0 -54
  157. package/src/components/agent-picker/agent-picker.ts +0 -99
  158. package/src/components/agent-picker/index.ts +0 -1
  159. package/src/components/ai-driver/ai-driver.ts +0 -166
  160. package/src/components/ai-driver/index.ts +0 -1
  161. package/src/components/chat-bubble/chat-bubble.styles.ts +0 -194
  162. package/src/components/chat-bubble/chat-bubble.template.ts +0 -67
  163. package/src/components/chat-bubble/chat-bubble.ts +0 -436
  164. package/src/components/chat-bubble/index.ts +0 -1
  165. package/src/components/chat-driver/align-event-globals.ts +0 -23
  166. package/src/components/chat-driver/chat-driver.compact.test.ts +0 -131
  167. package/src/components/chat-driver/chat-driver.invocation-scope.test.ts +0 -285
  168. package/src/components/chat-driver/chat-driver.test.ts +0 -3679
  169. package/src/components/chat-driver/chat-driver.thinking-policy.test.ts +0 -185
  170. package/src/components/chat-driver/chat-driver.trace-capture.test.ts +0 -251
  171. package/src/components/chat-driver/chat-driver.ts +0 -3925
  172. package/src/components/chat-driver/chat-driver.turn-usage.test.ts +0 -362
  173. package/src/components/chat-driver/index.ts +0 -1
  174. package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.styles.ts +0 -7
  175. package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.template.ts +0 -6
  176. package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.test.ts +0 -144
  177. package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.ts +0 -243
  178. package/src/components/chat-interaction-wrapper/index.ts +0 -3
  179. package/src/components/chat-markdown/chat-markdown.ts +0 -89
  180. package/src/components/chat-markdown/index.ts +0 -1
  181. package/src/components/flowing-waves-indicator.ts +0 -286
  182. package/src/components/halo-overlay.ts +0 -184
  183. package/src/components/orchestrating-driver/index.ts +0 -1
  184. package/src/components/orchestrating-driver/orchestrating-driver.budget.test.ts +0 -438
  185. package/src/components/orchestrating-driver/orchestrating-driver.cost.test.ts +0 -107
  186. package/src/components/orchestrating-driver/orchestrating-driver.pin.test.ts +0 -96
  187. package/src/components/orchestrating-driver/orchestrating-driver.ts +0 -813
  188. package/src/components/plasma-orb-indicator.ts +0 -281
  189. package/src/components/popout-manager/index.ts +0 -1
  190. package/src/components/popout-manager/popout-manager.ts +0 -212
  191. package/src/components/settings-modal/index.ts +0 -2
  192. package/src/components/settings-modal/settings-modal.styles.test.ts +0 -94
  193. package/src/components/settings-modal/settings-modal.styles.ts +0 -853
  194. package/src/components/settings-modal/settings-modal.template.test.ts +0 -124
  195. package/src/components/settings-modal/settings-modal.template.ts +0 -943
  196. package/src/components/waves-indicator.ts +0 -241
  197. package/src/config/config.ts +0 -608
  198. package/src/config/define-stateful-agent.test.ts +0 -64
  199. package/src/config/define-stateful-agent.ts +0 -642
  200. package/src/config/fallback-agents.ts +0 -29
  201. package/src/config/index.ts +0 -2
  202. package/src/config/validate-providers.test.ts +0 -148
  203. package/src/config/validate-providers.ts +0 -58
  204. package/src/index.ts +0 -29
  205. package/src/main/blocked-state.test.ts +0 -1444
  206. package/src/main/budget-meter.test.ts +0 -477
  207. package/src/main/cost-session-banking.test.ts +0 -679
  208. package/src/main/file-attachments.test.ts +0 -215
  209. package/src/main/index.ts +0 -3
  210. package/src/main/interaction-cost.test.ts +0 -140
  211. package/src/main/main.styles.test.ts +0 -216
  212. package/src/main/main.styles.ts +0 -995
  213. package/src/main/main.template.ts +0 -1119
  214. package/src/main/main.ts +0 -5432
  215. package/src/main/main.types.ts +0 -225
  216. package/src/main/persistence-broken-sources.test.ts +0 -219
  217. package/src/main/popout-interaction-gate.test.ts +0 -87
  218. package/src/provider/ai-provider-switcher.ts +0 -40
  219. package/src/provider/assistant-app-settings.ts +0 -84
  220. package/src/state/ai-assistant-slice.test.ts +0 -336
  221. package/src/state/ai-assistant-slice.ts +0 -604
  222. package/src/state/debug-event-log.test.ts +0 -154
  223. package/src/state/debug-event-log.ts +0 -358
  224. package/src/state/driver-registry.test.ts +0 -70
  225. package/src/state/driver-registry.ts +0 -79
  226. package/src/state/interaction-context.test.ts +0 -90
  227. package/src/state/interaction-context.ts +0 -101
  228. package/src/state/persistence/build-timeline-entries.ts +0 -66
  229. package/src/state/persistence/diagnostics-cursors.test.ts +0 -53
  230. package/src/state/persistence/diagnostics-cursors.ts +0 -80
  231. package/src/state/persistence/diagnostics.test.ts +0 -346
  232. package/src/state/persistence/diagnostics.ts +0 -220
  233. package/src/state/persistence/index.ts +0 -10
  234. package/src/state/persistence/persister-registry.ts +0 -55
  235. package/src/state/persistence/session-persistence-provider.test.ts +0 -136
  236. package/src/state/persistence/session-persistence-provider.ts +0 -226
  237. package/src/state/persistence/session-persistence.integration.test.ts +0 -141
  238. package/src/state/persistence/session-persister.test.ts +0 -787
  239. package/src/state/persistence/session-persister.ts +0 -917
  240. package/src/state/persistence/session-snapshot.test.ts +0 -352
  241. package/src/state/persistence/session-snapshot.ts +0 -344
  242. package/src/state/persistence/stateful-restore.e2e.test.ts +0 -229
  243. package/src/state/session-store.ts +0 -56
  244. package/src/styles/ai-colours.ts +0 -30
  245. package/src/styles/menu.ts +0 -187
  246. package/src/styles/settings-section.ts +0 -40
  247. package/src/suggestions/chat-suggestions.ts +0 -186
  248. package/src/tags/index.ts +0 -1
  249. package/src/types/ai-chat-widget.ts +0 -67
  250. package/src/types/interaction-context.ts +0 -82
  251. package/src/utils/animated-panel-toggle.ts +0 -62
  252. package/src/utils/animation-exclusivity.test.ts +0 -72
  253. package/src/utils/animation-exclusivity.ts +0 -40
  254. package/src/utils/banked-usage-baselines.ts +0 -90
  255. package/src/utils/collect-session-models.test.ts +0 -82
  256. package/src/utils/collect-session-models.ts +0 -65
  257. package/src/utils/condense-history.test.ts +0 -746
  258. package/src/utils/condense-history.ts +0 -448
  259. package/src/utils/cost-session-history.test.ts +0 -226
  260. package/src/utils/cost-session-history.ts +0 -218
  261. package/src/utils/derive-cost-session-title.test.ts +0 -63
  262. package/src/utils/derive-cost-session-title.ts +0 -76
  263. package/src/utils/flatten-sub-agent-messages.test.ts +0 -163
  264. package/src/utils/flatten-sub-agent-messages.ts +0 -88
  265. package/src/utils/format-usd.ts +0 -26
  266. package/src/utils/history-transform.test.ts +0 -228
  267. package/src/utils/history-transform.ts +0 -163
  268. package/src/utils/index.ts +0 -3
  269. package/src/utils/logger.ts +0 -3
  270. package/src/utils/message-partition.test.ts +0 -129
  271. package/src/utils/message-partition.ts +0 -115
  272. package/src/utils/resolve-cost-history-config.test.ts +0 -38
  273. package/src/utils/resolve-cost-history-config.ts +0 -38
  274. package/src/utils/resolve-preference-baseline.test.ts +0 -84
  275. package/src/utils/resolve-preference-baseline.ts +0 -80
  276. package/src/utils/strip-agent-handlers.test.ts +0 -99
  277. package/src/utils/strip-agent-handlers.ts +0 -53
  278. package/src/utils/sum-costs.test.ts +0 -136
  279. package/src/utils/sum-costs.ts +0 -18
  280. package/src/utils/sum-tokens.test.ts +0 -93
  281. package/src/utils/sum-tokens.ts +0 -68
  282. package/src/utils/sum-usage.test.ts +0 -191
  283. package/src/utils/sum-usage.ts +0 -208
  284. package/src/utils/tool-fold.ts +0 -181
  285. package/src/utils/usage-rows.test.ts +0 -237
  286. package/src/utils/usage-rows.ts +0 -187
  287. package/src/utils/with-timeout.ts +0 -24
  288. package/tsconfig.json +0 -9
@@ -1,346 +0,0 @@
1
- import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
2
- import {
3
- assembleDebugLog,
4
- diagnosticsMessageKey,
5
- type DiagnosticEntry,
6
- withFreshMetaSnapshot,
7
- } from './diagnostics';
8
-
9
- // GENC-1351 §5.8: `assembleDebugLog` reassembles the forward-only diagnostics
10
- // stream back into the `{ readme, timeline, meta }` debug-log shape — timeline
11
- // sorted by timestamp, meta-snapshots pulled out with the latest winning.
12
-
13
- const README = ['read me'] as const;
14
- const Suite = createLogicSuite('assembleDebugLog');
15
-
16
- Suite('sorts the timeline by timestamp, tie-breaking event → turn → message', () => {
17
- const entries: DiagnosticEntry[] = [
18
- { kind: 'message', timestamp: '2026-01-01T00:00:02.000Z', content: 'later msg' },
19
- { kind: 'turn', timestamp: '2026-01-01T00:00:01.000Z', turnIndex: '0' },
20
- { kind: 'event', timestamp: '2026-01-01T00:00:01.000Z', type: 'turn.start' },
21
- { kind: 'message', timestamp: '2026-01-01T00:00:01.000Z', content: 'first msg' },
22
- ];
23
- const { timeline } = assembleDebugLog(entries, README);
24
- // Co-timestamped @ …01: event, then turn, then message; the …02 message last.
25
- assert.equal(
26
- timeline.map((e) => e.kind),
27
- ['event', 'turn', 'message', 'message'],
28
- );
29
- assert.is(timeline[3].content, 'later msg');
30
- });
31
-
32
- Suite('pulls meta-snapshots out of the timeline and surfaces the latest as `meta`', () => {
33
- const entries: DiagnosticEntry[] = [
34
- { kind: 'meta-snapshot', timestamp: '2026-01-01T00:00:01.000Z', meta: { model: 'old' } },
35
- { kind: 'message', timestamp: '2026-01-01T00:00:02.000Z', content: 'hi' },
36
- { kind: 'meta-snapshot', timestamp: '2026-01-01T00:00:03.000Z', meta: { model: 'new' } },
37
- ];
38
- const { timeline, meta } = assembleDebugLog(entries, README);
39
- assert.equal(
40
- timeline.map((e) => e.kind),
41
- ['message'],
42
- 'meta-snapshots do not leak into the timeline',
43
- );
44
- assert.equal(meta, { model: 'new' }, 'the newest meta-snapshot surfaces as top-level meta');
45
- });
46
-
47
- Suite('re-prepends the passed readme and tolerates an empty stream', () => {
48
- const empty = assembleDebugLog([], README);
49
- assert.is(empty.readme, README);
50
- assert.equal(empty.timeline, []);
51
- assert.is(empty.meta, undefined, 'no meta-snapshot → undefined meta');
52
- });
53
-
54
- Suite('reassembles two loads into one lifetime timeline (the stitch)', () => {
55
- // Load 1's appended entries + load 2's — as loadDiagnostics would return them,
56
- // in arbitrary order — stitch back chronologically.
57
- const load2: DiagnosticEntry[] = [
58
- { kind: 'message', timestamp: '2026-01-01T00:10:00.000Z', content: 'reload msg' },
59
- { kind: 'meta-snapshot', timestamp: '2026-01-01T00:10:00.000Z', meta: { load: 2 } },
60
- ];
61
- const load1: DiagnosticEntry[] = [
62
- { kind: 'message', timestamp: '2026-01-01T00:00:00.000Z', content: 'first msg' },
63
- { kind: 'meta-snapshot', timestamp: '2026-01-01T00:00:00.000Z', meta: { load: 1 } },
64
- ];
65
- const { timeline, meta } = assembleDebugLog([...load2, ...load1], README);
66
- assert.equal(
67
- timeline.filter((e) => e.kind === 'message').map((e) => e.content),
68
- ['first msg', 'reload msg'],
69
- 'messages ordered across loads regardless of input order',
70
- );
71
- assert.equal(meta, { load: 2 }, 'newest load’s meta-snapshot surfaces');
72
- });
73
-
74
- Suite('collapses exact-duplicate entries but keeps genuinely-distinct ones', () => {
75
- // Two element instances (bubble + panel) can each append the same message, and a
76
- // reload's second instance can re-append it — byte-identical copies. Two real
77
- // `assistant.connected` (from two instances) differ by `index` and must be kept.
78
- const msg: DiagnosticEntry = {
79
- kind: 'message',
80
- timestamp: '2026-01-01T00:00:01.000Z',
81
- role: 'user',
82
- content: 'hi',
83
- };
84
- const connA: DiagnosticEntry = {
85
- kind: 'event',
86
- timestamp: '2026-01-01T00:00:00.000Z',
87
- type: 'assistant.connected',
88
- index: 0,
89
- };
90
- const connB: DiagnosticEntry = { ...connA, index: 1 };
91
- const { timeline } = assembleDebugLog([msg, connA, { ...msg }, connB, connA], README);
92
- assert.is(timeline.filter((e) => e.kind === 'message').length, 1, 'identical message collapsed');
93
- assert.is(
94
- timeline.filter((e) => e.type === 'assistant.connected').length,
95
- 2,
96
- 'two distinct connects (different index) kept; the repeated connA collapsed',
97
- );
98
- });
99
-
100
- Suite('lifts the newest snapshot’s session usage to the top of the log', () => {
101
- // The four buckets + USD are the headline of a cost investigation, and `meta` is a
102
- // large block to go digging in — so they surface at the top level. Same object, not a
103
- // second derivation, and taken from the SAME snapshot that becomes `meta`.
104
- const usage = {
105
- costUsd: 1.25,
106
- uncachedInputTokens: 1000,
107
- cacheReadTokens: 9000,
108
- cacheWriteTokens: 500,
109
- outputTokens: 300,
110
- };
111
- const entries: DiagnosticEntry[] = [
112
- {
113
- kind: 'meta-snapshot',
114
- timestamp: '2026-01-01T00:00:01.000Z',
115
- meta: { context: { sessionUsage: { ...usage, costUsd: 0.01 } } },
116
- },
117
- {
118
- kind: 'meta-snapshot',
119
- timestamp: '2026-01-01T00:00:09.000Z',
120
- meta: { context: { sessionUsage: usage } },
121
- },
122
- ];
123
- assert.equal(assembleDebugLog(entries, README).sessionUsage, usage);
124
- });
125
-
126
- Suite('leaves session usage undefined when no snapshot carries it', () => {
127
- // A stream with no meta-snapshot, or one written before the field existed. Absent
128
- // rather than zeroed: "not recorded" must not read as "this session spent nothing".
129
- assert.is(assembleDebugLog([], README).sessionUsage, undefined);
130
- const noContext: DiagnosticEntry[] = [
131
- { kind: 'meta-snapshot', timestamp: '2026-01-01T00:00:01.000Z', meta: { host: 'localhost' } },
132
- ];
133
- assert.is(assembleDebugLog(noContext, README).sessionUsage, undefined);
134
- });
135
-
136
- Suite('collapses the turn pair even when its rendered systemPrompt differs', () => {
137
- // The pair is produced by two separate flushes, and `buildTimelineEntries` renders
138
- // `systemPrompt` relative to the preceding snapshots — so if the snapshot holding the
139
- // full prompt is evicted from the ring buffer between the two, the surviving turn
140
- // renders its prompt in full where the earlier copy had the '<repeated>' marker. Any
141
- // identity derived from the serialized entry breaks here and the log shows one model
142
- // call as two turns, one unpriced. Identity is turnIndex + timestamp + agentName.
143
- const unpriced: DiagnosticEntry = {
144
- kind: 'turn',
145
- turnIndex: '7',
146
- timestamp: '2026-01-01T00:00:01.000Z',
147
- agentName: 'Trade Operations',
148
- systemPrompt: '<repeated — identical to turn 0>',
149
- toolNames: [],
150
- };
151
- const priced: DiagnosticEntry = {
152
- ...unpriced,
153
- systemPrompt: 'the full prompt, now that turn 0 has been evicted',
154
- usage: {
155
- costUsd: 0.5,
156
- uncachedInputTokens: 100,
157
- cacheReadTokens: 0,
158
- cacheWriteTokens: 0,
159
- outputTokens: 20,
160
- },
161
- };
162
- const turns = assembleDebugLog([unpriced, priced], README).timeline.filter(
163
- (e) => e.kind === 'turn',
164
- );
165
- assert.is(turns.length, 1, 'still one model call');
166
- assert.equal(turns[0].usage, priced.usage, 'and it is the priced copy');
167
- });
168
-
169
- Suite('keeps same-index turns from different loads and different sub-agents apart', () => {
170
- // `turnIndex` restarts at '0' on every page load, so a lifetime log holds one turn '0'
171
- // per load; two sub-agents invoked in one parent turn share the index prefix and are
172
- // separated by `agentName` (see `forwardSubAgentSnapshots`).
173
- const load1: DiagnosticEntry = {
174
- kind: 'turn',
175
- turnIndex: '0',
176
- timestamp: '2026-01-01T00:00:01.000Z',
177
- agentName: 'Booker',
178
- toolNames: [],
179
- };
180
- const load2: DiagnosticEntry = { ...load1, timestamp: '2026-01-02T09:00:00.000Z' };
181
- const sibling: DiagnosticEntry = { ...load1, turnIndex: '3-1', agentName: 'Planner' };
182
- const cousin: DiagnosticEntry = { ...sibling, agentName: 'Grounding' };
183
- const turns = assembleDebugLog([load1, load2, sibling, cousin], README).timeline.filter(
184
- (e) => e.kind === 'turn',
185
- );
186
- assert.is(turns.length, 4, 'four distinct calls, none collapsed');
187
- });
188
-
189
- Suite('collapses the unpriced and priced copies of one turn onto the priced one', () => {
190
- // A turn entry is persisted when it is created (before its model call) and again once
191
- // `usage` is back-filled, so a stitched stream carries both. They are ONE model call
192
- // and must read as one — with the cost, or the re-emit gained nothing.
193
- const unpriced: DiagnosticEntry = {
194
- kind: 'turn',
195
- turnIndex: '0',
196
- timestamp: '2026-01-01T00:00:01.000Z',
197
- toolNames: [],
198
- };
199
- const priced: DiagnosticEntry = {
200
- ...unpriced,
201
- usage: {
202
- costUsd: 0.5,
203
- uncachedInputTokens: 100,
204
- cacheReadTokens: 0,
205
- cacheWriteTokens: 0,
206
- outputTokens: 20,
207
- },
208
- };
209
- for (const order of [
210
- [unpriced, priced],
211
- [priced, unpriced],
212
- ]) {
213
- const turns = assembleDebugLog(order, README).timeline.filter((e) => e.kind === 'turn');
214
- assert.is(turns.length, 1, 'one turn, not two');
215
- assert.equal(turns[0].usage, priced.usage, 'the priced copy wins regardless of stream order');
216
- }
217
- });
218
-
219
- Suite('keeps two genuinely-different turns that differ only in usage', () => {
220
- // The collapse keys on everything EXCEPT usage, so distinct calls stay distinct — they
221
- // differ in timestamp (and turnIndex) whatever else they share.
222
- const a: DiagnosticEntry = {
223
- kind: 'turn',
224
- turnIndex: '0',
225
- timestamp: '2026-01-01T00:00:01.000Z',
226
- toolNames: [],
227
- };
228
- const b: DiagnosticEntry = { ...a, turnIndex: '1', timestamp: '2026-01-01T00:00:02.000Z' };
229
- const turns = assembleDebugLog([a, b], README).timeline.filter((e) => e.kind === 'turn');
230
- assert.is(turns.length, 2);
231
- });
232
-
233
- Suite.run();
234
-
235
- // The download path's swap: a stored stream's `meta-snapshot`s replaced by an export-time
236
- // one. This is the fix for a SILENT bug — the persisted stream only re-appends that block
237
- // when the config signature changes, so a stitched lifetime log reported the totals frozen
238
- // at the session's first flush (near-zero spend against a transcript full of priced
239
- // messages) and nothing about it looked wrong.
240
- const FreshSuite = createLogicSuite('withFreshMetaSnapshot');
241
-
242
- const storedStream: DiagnosticEntry[] = [
243
- {
244
- kind: 'meta-snapshot',
245
- timestamp: '2026-01-01T00:00:00.000Z',
246
- meta: { context: { sessionUsage: { costUsd: 0.01 } } },
247
- dedupSignature: 'cfg-1',
248
- },
249
- { kind: 'message', timestamp: '2026-01-01T00:00:01.000Z', role: 'user', content: 'hi' },
250
- { kind: 'turn', turnIndex: '0', timestamp: '2026-01-01T00:00:02.000Z', toolNames: [] },
251
- { kind: 'event', index: 0, timestamp: '2026-01-01T00:00:03.000Z', type: 'turn.end' },
252
- ];
253
-
254
- const freshSnapshot: DiagnosticEntry = {
255
- kind: 'meta-snapshot',
256
- timestamp: '2026-01-01T02:00:00.000Z',
257
- meta: {
258
- context: {
259
- sessionUsage: {
260
- costUsd: 1.25,
261
- uncachedInputTokens: 1000,
262
- cacheReadTokens: 9000,
263
- cacheWriteTokens: 500,
264
- outputTokens: 300,
265
- },
266
- },
267
- },
268
- dedupSignature: 'cfg-1',
269
- };
270
-
271
- FreshSuite('reports the export-time totals, not the frozen stored ones', () => {
272
- const log = assembleDebugLog(withFreshMetaSnapshot(storedStream, freshSnapshot), README);
273
- assert.is(log.sessionUsage?.costUsd, 1.25, 'the fresh snapshot supplies the totals');
274
- assert.is(log.sessionUsage?.cacheReadTokens, 9000);
275
- assert.equal(log.meta, freshSnapshot.meta, 'and the whole meta block is the fresh one');
276
- });
277
-
278
- FreshSuite('keeps every non-snapshot entry', () => {
279
- const timeline = assembleDebugLog(
280
- withFreshMetaSnapshot(storedStream, freshSnapshot),
281
- README,
282
- ).timeline;
283
- assert.equal(
284
- timeline.map((e) => e.kind),
285
- ['message', 'turn', 'event'],
286
- 'the conversation is untouched — only the snapshots are swapped',
287
- );
288
- });
289
-
290
- FreshSuite('wins even when the stored snapshot claims a later time', () => {
291
- // A stream written on another machine can carry a skewed clock, and reassembly keeps
292
- // whichever snapshot claims the later timestamp — so the stale one must be REMOVED
293
- // rather than out-timestamped.
294
- const skewed: DiagnosticEntry[] = [
295
- { ...storedStream[0], timestamp: '2099-01-01T00:00:00.000Z' },
296
- ...storedStream.slice(1),
297
- ];
298
- const log = assembleDebugLog(withFreshMetaSnapshot(skewed, freshSnapshot), README);
299
- assert.is(log.sessionUsage?.costUsd, 1.25);
300
- });
301
-
302
- FreshSuite.run();
303
-
304
- // GENC-1461: the reasoning/narration/answer split (GENC-1411) emits several messages from ONE model
305
- // response, so they share `timestamp` + `role` + `subAgentOf`. The message identity key must still
306
- // tell them apart — otherwise the forward-capture dedup treats them as one and drops all but the
307
- // first (a server generation's answer message vanished from the persisted download while its
308
- // reasoning survived).
309
- const KeySuite = createLogicSuite('diagnosticsMessageKey');
310
-
311
- KeySuite('gives co-timestamped split messages (reasoning vs answer) distinct keys', () => {
312
- const base = {
313
- kind: 'message' as const,
314
- role: 'assistant',
315
- timestamp: '2026-01-01T00:00:05.000Z',
316
- };
317
- const reasoning: DiagnosticEntry = { ...base, category: 'reasoning', content: 'thinking…' };
318
- const answer: DiagnosticEntry = { ...base, content: 'the generated code' }; // no category = the answer
319
- const kReasoning = diagnosticsMessageKey(reasoning);
320
- const kAnswer = diagnosticsMessageKey(answer);
321
- assert.ok(kReasoning, 'reasoning message has a key');
322
- assert.ok(kAnswer, 'answer message has a key');
323
- assert.not.equal(
324
- kReasoning,
325
- kAnswer,
326
- 'a reasoning and an answer split from one response must not collide',
327
- );
328
- });
329
-
330
- KeySuite('still collapses a genuine re-append of the same message (same key)', () => {
331
- const m: DiagnosticEntry = {
332
- kind: 'message',
333
- role: 'assistant',
334
- timestamp: '2026-01-01T00:00:05.000Z',
335
- category: 'reasoning',
336
- content: 'thinking…',
337
- };
338
- // Two instances / a reload re-appending the SAME message must still share one key.
339
- assert.equal(diagnosticsMessageKey(m), diagnosticsMessageKey({ ...m }));
340
- });
341
-
342
- KeySuite('returns null when the entry has no timestamp (treated as always-new)', () => {
343
- assert.is(diagnosticsMessageKey({ kind: 'message', role: 'assistant' }), null);
344
- });
345
-
346
- KeySuite.run();
@@ -1,220 +0,0 @@
1
- /**
2
- * Server-saved diagnostics (GENC-1351 §5.8) — the forward-only stream that lets
3
- * the debug log persist across a whole session lifetime, not just the current
4
- * page load. This module owns the *pure* pieces: the entry type and the
5
- * reassembly back into the `{ readme, sessionUsage, timeline, meta }` debug-log
6
- * shape. The element owns capture/cadence; the provider owns storage layout.
7
- *
8
- * @packageDocumentation
9
- */
10
-
11
- import type { AggregateUsage } from '@genesislcap/foundation-ai';
12
-
13
- /**
14
- * One entry in the diagnostics stream — the debug-log timeline-entry union
15
- * (`message` / `turn` / `event`, as `getDebugLog()` builds) plus a point-in-time
16
- * **`meta-snapshot`** carrying the export-time `meta` block (agent summary,
17
- * context, active debug snapshot, …). Kept permissive: a provider stores and
18
- * returns these opaquely, and reassembly reads only `kind`, `timestamp` (for
19
- * ordering), `usage` (on a turn) and `meta` (on a snapshot).
20
- *
21
- * @public
22
- */
23
- export interface DiagnosticEntry {
24
- kind: 'message' | 'turn' | 'event' | 'meta-snapshot';
25
- /** ISO timestamp — string-comparable, so it orders the stream chronologically. */
26
- timestamp?: string;
27
- /** Present on `meta-snapshot` entries: the export-time `meta` block. */
28
- meta?: unknown;
29
- /**
30
- * Present on a `turn` entry once its model call has returned and reported usage —
31
- * the four token buckets plus USD for that one call (`TurnSnapshot.usage`).
32
- *
33
- * Read by the forward-capture delta as well as by readers: the turn entry is
34
- * created *before* the call, so a flush that lands mid-call persists it without
35
- * usage, and the delta re-emits it once (see `collectDiagnosticsDelta`) so the
36
- * priced copy reaches the stream. {@link assembleDebugLog} then collapses the pair.
37
- */
38
- usage?: AggregateUsage;
39
- /**
40
- * Present on `meta-snapshot` entries: a stable signature of the near-static config
41
- * (excludes volatile timestamp/context/debug-snapshot), used by the forward-capture
42
- * delta to skip re-appending an unchanged block (GENC-1351 §5.8). Absent on other
43
- * kinds and on pre-signature entries (the delta falls back to the full `meta` then).
44
- */
45
- dedupSignature?: string;
46
- [key: string]: unknown;
47
- }
48
-
49
- /** The reassembled debug log — the exact shape `getDebugLog()` returns. @public */
50
- export interface DebugLog {
51
- readme: readonly string[];
52
- /**
53
- * Session usage — the four token buckets plus USD cost — lifted out of the newest
54
- * `meta-snapshot`'s `meta.context.sessionUsage` so the headline spend figures sit at
55
- * the top of an exported log rather than buried under the (large) `meta` block. The
56
- * same object, not a second derivation.
57
- *
58
- * `undefined` when the stream carries no `meta-snapshot`, or one written before the
59
- * field existed. Never re-derived from the timeline: the per-request costs the
60
- * transports stamped are authoritative, and a ring-buffered timeline can have lost
61
- * entries the total still legitimately counts.
62
- *
63
- * **As current as the snapshot it came from, which is not automatically "now".** The
64
- * forward-capture delta only re-appends a `meta-snapshot` when the near-static config
65
- * signature changes, so the newest STORED snapshot of a session whose config never
66
- * changed is its first — with the totals frozen there. A caller reassembling a stored
67
- * stream on its own therefore gets stale figures unless it appends an export-time
68
- * snapshot first: see {@link withFreshMetaSnapshot}, which is what the assistant's own
69
- * download path does. `meta.timestamp` against the newest timeline entry tells you
70
- * which case you are holding.
71
- */
72
- sessionUsage?: AggregateUsage;
73
- timeline: DiagnosticEntry[];
74
- meta: unknown;
75
- }
76
-
77
- /**
78
- * Stable identity for a message diagnostic entry — `timestamp::role::subAgentOf::category`.
79
- * Used by the forward-capture delta (skip already-emitted messages) and by the
80
- * restore watermark (skip messages already present in a prior load's stream).
81
- * `null` when the entry has no timestamp → treat as always-new. Shared so the
82
- * element's diagnostics capture and the `SessionPersister`'s restore watermark
83
- * compute identity identically (GENC-1351 §5.8/§5.10).
84
- *
85
- * `category` is part of the key because the reasoning/narration/answer split (GENC-1411) emits
86
- * SEVERAL messages from one model response — the thought summary, any interstitial narration, and
87
- * the answer/tool-call message — which share `timestamp` + `role` + `subAgentOf`. Keying on those
88
- * alone collapsed them to one, so the forward-capture dropped all but the first: a server
89
- * generation's answer message vanished from the persisted debug log while its reasoning survived
90
- * (GENC-1461). `category` is stable across a message's in-place annotation (only `toolCalls` change,
91
- * which is deliberately excluded), so a genuine re-append of the same message still dedups.
92
- *
93
- * @internal
94
- */
95
- export function diagnosticsMessageKey(entry: DiagnosticEntry): string | null {
96
- return typeof entry.timestamp === 'string'
97
- ? `${entry.timestamp}::${(entry.role as string) ?? ''}::${(entry.subAgentOf as string) ?? ''}::${(entry.category as string) ?? ''}`
98
- : null;
99
- }
100
-
101
- /**
102
- * Identity of a `turn` entry — `turnIndex::timestamp::agentName`.
103
- *
104
- * Deliberately NOT the serialized entry, which two copies of the same call do not always
105
- * share. `buildTimelineEntries` renders `systemPrompt` *relative to the preceding
106
- * snapshots* (collapsing it to `<repeated — identical to turn N>` when it matches the
107
- * previous full prompt), so if the snapshot holding that full prompt is evicted from the
108
- * ring buffer between the unpriced flush and the priced one, the surviving turn renders
109
- * its prompt differently and the two serializations diverge. Both copies are in the
110
- * stream by then, so nothing would collapse them — and the log whose purpose is per-call
111
- * cost would show one model call as two turns, one of them unpriced.
112
- *
113
- * All three components are fixed when the snapshot is created and never re-derived.
114
- * `turnIndex` alone is not enough: it restarts at `'0'` on each page load, and a lifetime
115
- * log legitimately holds one turn `'0'` per load. `agentName` separates two sub-agents
116
- * invoked in the same parent turn, which share a `turnIndex` prefix by construction (see
117
- * `forwardSubAgentSnapshots`).
118
- */
119
- function turnIdentity(entry: DiagnosticEntry): string {
120
- return `turn::${String(entry.turnIndex ?? '')}::${entry.timestamp ?? ''}::${String(entry.agentName ?? '')}`;
121
- }
122
-
123
- // Tie-break co-timestamped entries by cause → call → output (event, turn, message),
124
- // matching the original in-place `getDebugLog` sort.
125
- const KIND_RANK: Record<string, number> = { event: 0, turn: 1, message: 2 };
126
- // Any non-{event,turn,message} kind (shouldn't reach the timeline — meta-snapshots
127
- // are filtered out first) sorts last.
128
- const UNKNOWN_KIND_RANK = 99;
129
-
130
- /**
131
- * Reassemble diagnostic entries into the `{ readme, sessionUsage, timeline, meta }`
132
- * debug-log shape (GENC-1351 §5.8). `timeline` = the `message`/`turn`/`event` entries sorted
133
- * by ISO `timestamp` (kind-rank tie-break). `meta` = the **newest** `meta-snapshot`'s
134
- * block only — a "state at export" photo, matching the single-page log. The older
135
- * snapshots are intentionally dropped: the per-turn evolution they would show is
136
- * already in the timeline (`turn.agentSnapshot` + `context.updated` events), so
137
- * keeping them would just repeat the bulky, near-static `agentSummary`. `readme` =
138
- * the passed (current) constant. `sessionUsage` = that same newest snapshot's
139
- * `meta.context.sessionUsage`, lifted to the top level. Pure — reused for both the
140
- * live current-page log and the reassembled lifetime log stitched from the persisted
141
- * stream, so both come out shape-identical.
142
- *
143
- * NOTE for a caller stitching a STORED stream (a headless consumer harvesting its own
144
- * lifetime log, say): the newest stored `meta-snapshot` is frozen at the last
145
- * config-signature change, so `meta` and `sessionUsage` are as old as that unless you
146
- * pass a fresh snapshot of your own — {@link withFreshMetaSnapshot} does exactly that,
147
- * and the element's download path goes through it.
148
- */
149
- export function assembleDebugLog(entries: DiagnosticEntry[], readme: readonly string[]): DebugLog {
150
- // Safety net for exact-duplicate entries: two element instances sharing one
151
- // session (bubble + popped-out panel) can each append the same entry, and a
152
- // reload's second instance can re-append restored history. Such copies are
153
- // byte-identical, so collapse by serialized identity — genuinely-distinct entries
154
- // (e.g. two real `assistant.connected` from two instances) differ in index /
155
- // placement and are kept. (The shared cursor store prevents most of this at
156
- // write time; this guards the read path regardless of what's in the stream.)
157
- //
158
- // A `turn` is identified by `turnIndex` + `timestamp` + `agentName` instead (see
159
- // `turnIdentity`), so the unpriced copy a mid-call flush persisted and the priced copy
160
- // that follows it collapse into ONE turn — the priced one — rather than reading as two
161
- // model calls.
162
- const seenAt = new Map<string, number>();
163
- let latestMeta: DiagnosticEntry | undefined;
164
- const timeline: DiagnosticEntry[] = [];
165
- for (const entry of entries) {
166
- if (entry.kind === 'meta-snapshot') {
167
- if (!latestMeta || (entry.timestamp ?? '') >= (latestMeta.timestamp ?? '')) {
168
- latestMeta = entry;
169
- }
170
- continue;
171
- }
172
- const id = entry.kind === 'turn' ? turnIdentity(entry) : JSON.stringify(entry);
173
- const at = seenAt.get(id);
174
- if (at !== undefined) {
175
- // Same entry seen twice — keep the richer copy. `usage` on a turn is the only
176
- // field that can arrive late, so it is the only upgrade there is to make.
177
- if (entry.usage && !timeline[at].usage) timeline[at] = entry;
178
- continue;
179
- }
180
- seenAt.set(id, timeline.length);
181
- timeline.push(entry);
182
- }
183
- timeline.sort((a, b) => {
184
- const ta = a.timestamp ?? '';
185
- const tb = b.timestamp ?? '';
186
- if (ta < tb) return -1;
187
- if (ta > tb) return 1;
188
- return (KIND_RANK[a.kind] ?? UNKNOWN_KIND_RANK) - (KIND_RANK[b.kind] ?? UNKNOWN_KIND_RANK);
189
- });
190
- // The session totals live inside the newest snapshot's `meta`, which is `unknown` by
191
- // design (a provider stores these opaquely) — so this narrows just the one path it
192
- // reads rather than typing the whole block.
193
- const context = (latestMeta?.meta as { context?: { sessionUsage?: AggregateUsage } } | undefined)
194
- ?.context;
195
- return { readme, sessionUsage: context?.sessionUsage, timeline, meta: latestMeta?.meta };
196
- }
197
-
198
- /**
199
- * A stored diagnostics stream with its `meta-snapshot`s replaced by `fresh` — what to
200
- * pass {@link assembleDebugLog} when reassembling a persisted stream for export.
201
- *
202
- * The stored snapshots are dropped rather than out-timestamped. `assembleDebugLog` keeps
203
- * whichever claims the later time, and a stream written on another machine can carry a
204
- * skewed clock; `fresh` is built from live state and so is the more current by
205
- * construction. Only one snapshot ever survives reassembly, so the count is unchanged —
206
- * though a snapshot taken while the driver is unwired (mid-popout) can leave
207
- * `activeFoldStack`/`activeDebugSnapshot` unset where a stored one had them. The figures
208
- * this exists for — the context block and `sessionUsage` — read the session store and
209
- * element props, which survive an unwire.
210
- *
211
- * Pure, and separate from the element for that reason: the swap it performs is the fix
212
- * for a silent bug (a lifetime log reporting near-zero spend against a transcript full of
213
- * priced messages), so it needs to be testable without mounting anything.
214
- */
215
- export function withFreshMetaSnapshot(
216
- stored: readonly DiagnosticEntry[],
217
- fresh: DiagnosticEntry,
218
- ): DiagnosticEntry[] {
219
- return [...stored.filter((e) => e.kind !== 'meta-snapshot'), fresh];
220
- }
@@ -1,10 +0,0 @@
1
- export { PERSISTED_SESSION_VERSION } from './session-snapshot';
2
- export type { PersistedSession } from './session-snapshot';
3
- export { WebStorageSessionProvider } from './session-persistence-provider';
4
- export type {
5
- SessionPersistenceProvider,
6
- SessionPersistenceConfig,
7
- SessionPreferences,
8
- } from './session-persistence-provider';
9
- export { assembleDebugLog, withFreshMetaSnapshot } from './diagnostics';
10
- export type { DiagnosticEntry, DebugLog } from './diagnostics';
@@ -1,55 +0,0 @@
1
- import type { SessionPersister } from './session-persister';
2
-
3
- /**
4
- * Module-level registry of {@link SessionPersister}s keyed by session identity
5
- * string (GENC-1351 §5.10).
6
- *
7
- * A persister's lifetime is tied to the conversation session, not the DOM — like
8
- * the driver registry. The (up to two) element views that share a session (a
9
- * collapsed bubble + a popped-out panel) look up or create the ONE persister for
10
- * their key and delegate all restore-core persistence to it, so there is a single
11
- * save timer + carry-forward cache per session (no per-view double-writes/races).
12
- *
13
- * @internal
14
- */
15
- const persisterRegistry = new Map<string, SessionPersister>();
16
-
17
- /**
18
- * Returns the existing persister for `key`, or calls `factory` to create and
19
- * register a new one. First view to connect creates it; later views share it.
20
- */
21
- export function getOrCreatePersister(
22
- key: string,
23
- factory: () => SessionPersister,
24
- ): SessionPersister {
25
- const existing = persisterRegistry.get(key);
26
- if (existing) return existing;
27
- const persister = factory();
28
- persisterRegistry.set(key, persister);
29
- return persister;
30
- }
31
-
32
- /** Returns the existing persister for `key`, or `undefined`. */
33
- export function getPersister(key: string): SessionPersister | undefined {
34
- return persisterRegistry.get(key);
35
- }
36
-
37
- /**
38
- * Removes the persister for `key` (explicit session teardown). Disposes it first
39
- * so its debounced save AND diagnostics timers are cancelled — otherwise a timer
40
- * scheduled by the outgoing session fires after the in-place switch and flushes the
41
- * NEW session's entries under the OLD key, corrupting its stream (GENC-1351 §5.12).
42
- */
43
- export function deletePersister(key: string): void {
44
- persisterRegistry.get(key)?.dispose();
45
- persisterRegistry.delete(key);
46
- }
47
-
48
- /**
49
- * Removes all entries. Exposed for test isolation only — not part of the public API.
50
- *
51
- * @internal
52
- */
53
- export function clearPersisterRegistry(): void {
54
- persisterRegistry.clear();
55
- }