@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,352 +0,0 @@
1
- import type { ChatMessage } from '@genesislcap/foundation-ai';
2
- import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
3
- import {
4
- PERSISTED_SESSION_VERSION,
5
- createPersistedSession,
6
- isRestorableSession,
7
- type PersistedSession,
8
- reconcileAgentSnapshots,
9
- resolveNonResumableNotice,
10
- toLoadSessionPayload,
11
- keepThroughLastUserMessage,
12
- trimToRestorableBoundary,
13
- unknownAgentSnapshots,
14
- } from './session-snapshot';
15
-
16
- const asMsg = (o: Record<string, unknown>): ChatMessage => o as unknown as ChatMessage;
17
-
18
- // GENC-1351 §5.1: the PersistedSession schema + its pure (de)serialize helpers.
19
- // These have no DOM/storage deps, so they run straight in Node.
20
-
21
- const Suite = createLogicSuite('persisted-session-schema');
22
-
23
- const base = () =>
24
- createPersistedSession({
25
- sessionKey: 'k',
26
- host: 'app.example',
27
- messages: [],
28
- pinnedAgentName: 'A',
29
- flowOwnerAgentName: null,
30
- sessionCostUsd: 2,
31
- contextTokens: 10,
32
- contextLimit: 100,
33
- activeModel: 'm',
34
- activeProviderName: 'p',
35
- activeAgentName: 'A',
36
- agentSnapshots: { A: { machine: { state: 'x', context: {} } } },
37
- });
38
-
39
- Suite('createPersistedSession stamps version + savedAt and keeps agentSnapshots', () => {
40
- const s = base();
41
- assert.is(s.version, PERSISTED_SESSION_VERSION);
42
- assert.is(typeof s.savedAt, 'string');
43
- assert.ok(s.savedAt.length > 0);
44
- assert.equal(s.agentSnapshots, { A: { machine: { state: 'x', context: {} } } });
45
- });
46
-
47
- // GENC-1464 — the blocked latch is deliberately NOT persisted. The snapshot is
48
- // per-stateKey and long-lived, so a persisted latch would survive an out-of-band
49
- // budget raise with no in-element way to clear it: the user would return to a
50
- // permanently dead composer against a budget that has headroom. Re-deriving the
51
- // state (host pre-flight on mount, or the next 402) is strictly safer than
52
- // remembering it. This test exists so a future "helpful" addition trips here and
53
- // has to read that reasoning first — see `FoundationAiAssistant.blocked`'s
54
- // Lifetime paragraph and docs/migration-GENC-1464.md §Scope.
55
- Suite('the blocked latch is deliberately not persisted', () => {
56
- const s = base() as unknown as Record<string, unknown>;
57
- assert.not.ok('blocked' in s, 'blocked must not enter the persisted snapshot');
58
- assert.not.ok('blockedReason' in s, 'blockedReason must not enter the persisted snapshot');
59
- // The per-vendor latches inherit the same lifetime, so they inherit the same
60
- // rule — a persisted per-vendor wall is exactly as stale as a persisted global
61
- // one, just harder to notice because only one vendor looks broken.
62
- assert.not.ok('blockedVendors' in s, 'blockedVendors must not enter the persisted snapshot');
63
- assert.not.ok(
64
- 'blockedVendorReasons' in s,
65
- 'blockedVendorReasons must not enter the persisted snapshot',
66
- );
67
- });
68
-
69
- Suite('createPersistedSession defaults agentSnapshots to {}', () => {
70
- const s = createPersistedSession({
71
- sessionKey: 'k',
72
- host: 'h',
73
- messages: [],
74
- pinnedAgentName: null,
75
- flowOwnerAgentName: null,
76
- sessionCostUsd: 0,
77
- });
78
- assert.equal(s.agentSnapshots, {});
79
- });
80
-
81
- Suite('isRestorableSession accepts a matching version + host', () => {
82
- assert.ok(isRestorableSession(base(), { host: 'app.example' }));
83
- });
84
-
85
- Suite('isRestorableSession rejects version mismatch, cross-host, and malformed input', () => {
86
- assert.ok(!isRestorableSession({ ...base(), version: 999 }, { host: 'app.example' }));
87
- assert.ok(!isRestorableSession(base(), { host: 'other.example' }));
88
- assert.ok(!isRestorableSession(null, { host: 'h' }));
89
- assert.ok(
90
- !isRestorableSession({ version: PERSISTED_SESSION_VERSION, host: 'h' }, { host: 'h' }),
91
- 'missing messages array → not restorable',
92
- );
93
- });
94
-
95
- Suite('toLoadSessionPayload projects only the redux-restorable fields', () => {
96
- const p = toLoadSessionPayload(base());
97
- assert.is(p.pinnedAgentName, 'A');
98
- assert.is(p.flowOwnerAgentName, null);
99
- // Cost is deliberately NOT projected — the slice re-derives it (and the token
100
- // buckets) from `messages`, so a stored counter would be a second, divergeable
101
- // source of truth.
102
- assert.is((p as { sessionCostUsd?: number }).sessionCostUsd, undefined);
103
- assert.is(p.contextTokens, 10);
104
- assert.is(p.contextLimit, 100);
105
- assert.is(p.activeModel, 'm');
106
- assert.is(p.activeProviderName, 'p');
107
- assert.equal(p.messages, [] as ChatMessage[]);
108
- // agentSnapshots + activeAgentName are applied by the host, not via the slice.
109
- assert.ok(!('agentSnapshots' in p));
110
- assert.ok(!('activeAgentName' in p));
111
- });
112
-
113
- Suite('trimToRestorableBoundary returns a clean transcript unchanged (same ref)', () => {
114
- const clean = [
115
- asMsg({ role: 'user', content: 'hi' }),
116
- asMsg({ role: 'assistant', content: 'hello' }),
117
- ];
118
- assert.is(trimToRestorableBoundary(clean), clean);
119
- assert.is(trimToRestorableBoundary([]).length, 0);
120
- });
121
-
122
- Suite(
123
- 'trimToRestorableBoundary drops a trailing unresolved interaction, keeps a resolved one',
124
- () => {
125
- const user = asMsg({ role: 'user', content: 'hi' });
126
- const pending = asMsg({
127
- role: 'assistant',
128
- content: '',
129
- interaction: { interactionId: '2', componentName: 'x', data: {} },
130
- });
131
- const resolved = asMsg({
132
- role: 'assistant',
133
- content: '',
134
- interaction: {
135
- interactionId: '1',
136
- componentName: 'x',
137
- data: {},
138
- resolved: { status: 'approved' },
139
- },
140
- });
141
- assert.is(trimToRestorableBoundary([user, pending]).length, 1);
142
- assert.is(trimToRestorableBoundary([user, resolved]).length, 2);
143
- },
144
- );
145
-
146
- Suite('trimToRestorableBoundary drops dangling + partially-answered tool calls', () => {
147
- const user = asMsg({ role: 'user', content: 'do x' });
148
- const callAB = asMsg({
149
- role: 'assistant',
150
- content: '',
151
- toolCalls: [
152
- { id: 'a', name: 'f', args: {} },
153
- { id: 'b', name: 'g', args: {} },
154
- ],
155
- });
156
- const resultA = asMsg({
157
- role: 'tool',
158
- content: '',
159
- toolResult: { toolCallId: 'a', content: 'ra' },
160
- });
161
- const resultB = asMsg({
162
- role: 'tool',
163
- content: '',
164
- toolResult: { toolCallId: 'b', content: 'rb' },
165
- });
166
- // No results → drop the whole round back to [user].
167
- assert.is(trimToRestorableBoundary([user, callAB]).length, 1);
168
- // Only A answered → B orphaned → drop the round back to [user].
169
- assert.is(trimToRestorableBoundary([user, callAB, resultA]).length, 1);
170
- // Both answered → kept (ends on a balanced tool result).
171
- assert.is(trimToRestorableBoundary([user, callAB, resultA, resultB]).length, 4);
172
- });
173
-
174
- Suite('trimToRestorableBoundary drops a trailing empty assistant placeholder', () => {
175
- const user = asMsg({ role: 'user', content: 'hi' });
176
- const placeholder = asMsg({ role: 'assistant', content: '' });
177
- assert.is(trimToRestorableBoundary([user, placeholder]).length, 1);
178
- });
179
-
180
- // GENC-1351: orphaned agent snapshots (dynamic agent arrays — e.g. genesis-create).
181
- Suite('unknownAgentSnapshots flags saved agents missing from the configured set', () => {
182
- const snap = {
183
- agentSnapshots: { 'Guided Booking': {}, Bootstrap: {} },
184
- } as unknown as PersistedSession;
185
- // Bootstrap gone from the current set → flagged; Guided Booking is known.
186
- assert.equal(unknownAgentSnapshots(snap, ['Guided Booking', 'Trades']), ['Bootstrap']);
187
- // All known → none flagged.
188
- assert.equal(unknownAgentSnapshots(snap, ['Guided Booking', 'Bootstrap']), []);
189
- // Empty known set → can't classify yet → none (avoids false positives mid-init).
190
- assert.equal(unknownAgentSnapshots(snap, []), []);
191
- });
192
-
193
- // GENC-1351 §6 — `reconcileAgentSnapshots`: the save-time guard against a
194
- // post-restore autosave wiping a mid-flow machine. The bug (regression #1): a
195
- // restore reads a good snapshot, then the restore's own history change fires an
196
- // autosave BEFORE the agent re-activates, so the live capture is empty and, left
197
- // unguarded, `{}` overwrites the machine → the flow resets on the next reload.
198
- const MACHINE = { machine: { value: 'pickInstrument', context: { counterparty: { id: 'c1' } } } };
199
-
200
- Suite(
201
- 'reconcileAgentSnapshots: REGRESSION — carries the flow-owner forward when live capture is empty',
202
- () => {
203
- // Post-restore, pre-activation: getActiveAgent() is undefined → live is {}, but
204
- // a flow is owned and the good image sits in the cache (seeded on restore).
205
- const { snapshots, cache } = reconcileAgentSnapshots({}, 'Guided Booking', {
206
- 'Guided Booking': MACHINE,
207
- });
208
- assert.equal(
209
- snapshots['Guided Booking'],
210
- MACHINE,
211
- 'the flow-owner machine is preserved, NOT wiped to {}',
212
- );
213
- assert.equal(cache['Guided Booking'], MACHINE, 'cache keeps the good image');
214
- },
215
- );
216
-
217
- Suite('reconcileAgentSnapshots: a live capture wins and advances the cache', () => {
218
- const FRESH = { machine: { value: 'enterDetails', context: {} } };
219
- const { snapshots, cache } = reconcileAgentSnapshots(
220
- { 'Guided Booking': FRESH },
221
- 'Guided Booking',
222
- {
223
- 'Guided Booking': MACHINE, // stale cached value
224
- },
225
- );
226
- assert.equal(snapshots['Guided Booking'], FRESH, 'live image is used (never the stale cache)');
227
- assert.equal(cache['Guided Booking'], FRESH, 'cache advances to the freshly-captured image');
228
- });
229
-
230
- Suite('reconcileAgentSnapshots: no flow owner → empty stays empty (no stale carry-forward)', () => {
231
- // Idle / stateless save: nothing to carry, even if the cache holds an old image.
232
- const { snapshots, cache } = reconcileAgentSnapshots({}, null, { 'Guided Booking': MACHINE });
233
- assert.equal(snapshots, {}, 'nothing carried forward without a flow owner');
234
- assert.equal(cache['Guided Booking'], MACHINE, 'an empty save never erases the cache');
235
- });
236
-
237
- Suite('reconcileAgentSnapshots: flow owner set but nothing cached → cannot fabricate state', () => {
238
- const { snapshots } = reconcileAgentSnapshots({}, 'Guided Booking', {});
239
- assert.is(
240
- snapshots['Guided Booking'],
241
- undefined,
242
- 'no image to carry → left absent, not invented',
243
- );
244
- });
245
-
246
- Suite(
247
- 'reconcileAgentSnapshots: carries the flow owner while keeping a co-captured other agent',
248
- () => {
249
- const OTHER = { machine: { value: 'x' } };
250
- const { snapshots } = reconcileAgentSnapshots({ Other: OTHER }, 'Guided Booking', {
251
- 'Guided Booking': MACHINE,
252
- });
253
- assert.equal(snapshots.Other, OTHER, 'the live non-owner image is kept');
254
- assert.equal(snapshots['Guided Booking'], MACHINE, 'the missing flow-owner is carried forward');
255
- },
256
- );
257
-
258
- Suite('reconcileAgentSnapshots: does not mutate its inputs', () => {
259
- const live = {};
260
- const cacheIn = { 'Guided Booking': MACHINE };
261
- reconcileAgentSnapshots(live, 'Guided Booking', cacheIn);
262
- assert.equal(live, {}, 'live input untouched');
263
- assert.equal(Object.keys(cacheIn), ['Guided Booking'], 'cache input untouched');
264
- });
265
-
266
- // GENC-1351 §5.9: the pure notice-resolution helper (message + pre-fill prompt).
267
- // The freeze/marker mechanics live on the element (integration), not here.
268
-
269
- Suite('resolveNonResumableNotice: custom message wins, blank falls back to default', () => {
270
- const msgs = [
271
- asMsg({ role: 'user', content: 'first' }),
272
- asMsg({ role: 'assistant', content: 'x' }),
273
- ];
274
- assert.is(
275
- resolveNonResumableNotice({
276
- notResumableMessage: 'custom',
277
- messages: msgs,
278
- defaultMessage: 'D',
279
- }).resumeMessage,
280
- 'custom',
281
- );
282
- assert.is(
283
- resolveNonResumableNotice({ notResumableMessage: ' ', messages: msgs, defaultMessage: 'D' })
284
- .resumeMessage,
285
- 'D',
286
- 'a blank custom message falls back to the default',
287
- );
288
- assert.is(
289
- resolveNonResumableNotice({ messages: msgs, defaultMessage: 'D' }).resumeMessage,
290
- 'D',
291
- 'no custom message → default',
292
- );
293
- });
294
-
295
- Suite(
296
- 'resolveNonResumableNotice: prompt prefers the captured flow prompt, else last user msg',
297
- () => {
298
- const msgs = [
299
- asMsg({ role: 'user', content: 'earlier' }),
300
- asMsg({ role: 'assistant', content: 'x' }),
301
- asMsg({ role: 'user', content: 'latest user' }),
302
- ];
303
- assert.is(
304
- resolveNonResumableNotice({
305
- flowActivationPrompt: 'the flow prompt',
306
- messages: msgs,
307
- defaultMessage: 'D',
308
- }).resumePrompt,
309
- 'the flow prompt',
310
- 'captured flow-activating prompt takes precedence',
311
- );
312
- assert.is(
313
- resolveNonResumableNotice({ flowActivationPrompt: null, messages: msgs, defaultMessage: 'D' })
314
- .resumePrompt,
315
- 'latest user',
316
- 'no flow prompt → falls back to the last user message',
317
- );
318
- assert.is(
319
- resolveNonResumableNotice({ messages: [], defaultMessage: 'D' }).resumePrompt,
320
- undefined,
321
- 'no flow prompt and no user message → undefined',
322
- );
323
- },
324
- );
325
-
326
- // GENC-1351 §5.9: the freeze fallback keeps the transcript through the last user
327
- // message, dropping the agent's in-flight turns (issue #3).
328
- Suite(
329
- 'keepThroughLastUserMessage drops trailing agent turns, keeps through the last user msg',
330
- () => {
331
- const u1 = asMsg({ role: 'user', content: 'book a flight' });
332
- const a1 = asMsg({ role: 'assistant', content: 'ok' });
333
- const u2 = asMsg({ role: 'user', content: 'show fills for T1' });
334
- const plan = asMsg({
335
- role: 'assistant',
336
- content: '',
337
- toolCalls: [{ id: 'a', name: 'f', args: {} }],
338
- });
339
- const res = asMsg({ role: 'tool', content: '' });
340
- // Trailing agent turns after the last user message → dropped.
341
- assert.equal(keepThroughLastUserMessage([u1, a1, u2, plan, res]), [u1, a1, u2]);
342
- // Already ends on a user message → unchanged (same ref).
343
- const endsUser = [u1, a1, u2];
344
- assert.is(keepThroughLastUserMessage(endsUser), endsUser);
345
- // No user message at all → unchanged (nothing to trim to).
346
- const noUser = [a1, res];
347
- assert.is(keepThroughLastUserMessage(noUser), noUser);
348
- assert.equal(keepThroughLastUserMessage([]).length, 0);
349
- },
350
- );
351
-
352
- Suite.run();
@@ -1,344 +0,0 @@
1
- import type { ChatMessage } from '@genesislcap/foundation-ai';
2
- import type { LoadSessionPayload } from '../ai-assistant-slice';
3
-
4
- /**
5
- * Schema version of the persisted session shape. Bump on an incompatible
6
- * change; a snapshot whose `version` doesn't match is discarded on load rather
7
- * than risk feeding a stale shape into restore.
8
- *
9
- * @public
10
- */
11
- export const PERSISTED_SESSION_VERSION = 1;
12
-
13
- /**
14
- * A durable, restorable snapshot of one chat session — the *restorable core* of
15
- * the debug log (GENC-1351 §5.1/D1), captured losslessly and independently of
16
- * the diagnostic timeline. Written by a `SessionPersistenceProvider` and read
17
- * back on page-reload restore.
18
- *
19
- * @public
20
- */
21
- export interface PersistedSession {
22
- /** Schema version — see {@link PERSISTED_SESSION_VERSION}. */
23
- version: number;
24
- /** ISO timestamp the snapshot was written. */
25
- savedAt: string;
26
- /** `window.location.host` at save time — restore refuses a cross-host snapshot. */
27
- host: string;
28
- /** The session this snapshot belongs to (the assistant's state key). */
29
- sessionKey: string;
30
- /** Full conversation transcript (`driver.getRawHistory()`) — the source of truth. */
31
- messages: ChatMessage[];
32
- /** Name of the agent active at save time, if any. */
33
- activeAgentName?: string;
34
- /** User's pinned agent (sticky picker choice); `null` = Auto. */
35
- pinnedAgentName: string | null;
36
- /** Agent that owned a guided flow at save time; `null` = none. */
37
- flowOwnerAgentName: string | null;
38
- /** Aggregated USD cost across the session. */
39
- sessionCostUsd: number;
40
- /** Provider-reported context-window usage on the last turn. */
41
- contextTokens?: number;
42
- /** Context-window limit for the active model. */
43
- contextLimit?: number;
44
- /** Active model id (e.g. `claude-sonnet-4-6`). */
45
- activeModel?: string;
46
- /** Name of the provider used on the last turn. */
47
- activeProviderName?: string;
48
- /**
49
- * Per-stateful-agent serialized state images (each from an agent's
50
- * `serialize()`), keyed by agent name. Handed back to the agent's
51
- * `init({ restored })` on restore. Typically holds at most the active
52
- * flow-owner — other stateful agents are disposed when switched away. The
53
- * value shape is the agent's own, hence `unknown`.
54
- */
55
- agentSnapshots: Record<string, unknown>;
56
- /**
57
- * Interrupted-flow marker (GENC-1351 §5.9). Present when the session was saved
58
- * while an agent declared its current state non-resumable (`resumable === false`).
59
- * The **conversation is still persisted and restored in full** — only the running
60
- * agent's flow-state is dropped (`agentSnapshots` empty, `flowOwnerAgentName`
61
- * null), so restore does not rehydrate the mid-flight flow. On restore the
62
- * assistant appends `message` as a normal assistant bubble and pre-fills the
63
- * composer with `prompt` so the user can re-run it.
64
- */
65
- interrupted?: {
66
- /** Shown as an assistant message on return. */
67
- message: string;
68
- /** The prompt that started the flow, pre-filled into the composer on return. */
69
- prompt?: string;
70
- };
71
- }
72
-
73
- /**
74
- * Everything needed to assemble a {@link PersistedSession} except the stamped
75
- * metadata (`version`, `savedAt`), which `createPersistedSession` fills.
76
- *
77
- * @internal
78
- */
79
- export interface PersistedSessionInput {
80
- sessionKey: string;
81
- host: string;
82
- messages: ChatMessage[];
83
- activeAgentName?: string;
84
- pinnedAgentName: string | null;
85
- flowOwnerAgentName: string | null;
86
- sessionCostUsd: number;
87
- contextTokens?: number;
88
- contextLimit?: number;
89
- activeModel?: string;
90
- activeProviderName?: string;
91
- agentSnapshots?: Record<string, unknown>;
92
- }
93
-
94
- /**
95
- * Assemble a versioned {@link PersistedSession}, stamping `version` and
96
- * `savedAt`. Pure but for the timestamp (reads the host clock at call time).
97
- *
98
- * @internal
99
- */
100
- export function createPersistedSession(input: PersistedSessionInput): PersistedSession {
101
- return {
102
- version: PERSISTED_SESSION_VERSION,
103
- savedAt: new Date().toISOString(),
104
- host: input.host,
105
- sessionKey: input.sessionKey,
106
- messages: input.messages,
107
- activeAgentName: input.activeAgentName,
108
- pinnedAgentName: input.pinnedAgentName,
109
- flowOwnerAgentName: input.flowOwnerAgentName,
110
- sessionCostUsd: input.sessionCostUsd,
111
- contextTokens: input.contextTokens,
112
- contextLimit: input.contextLimit,
113
- activeModel: input.activeModel,
114
- activeProviderName: input.activeProviderName,
115
- agentSnapshots: input.agentSnapshots ?? {},
116
- };
117
- }
118
-
119
- /**
120
- * Resolve the notice text + pre-fill prompt for an interrupted (non-resumable) flow
121
- * (GENC-1351 §5.9). Pure — the decision half of the persist gate, split out so it's
122
- * unit-testable independent of the save path:
123
- * - `resumeMessage`: the agent's `notResumableMessage` when non-blank, else the
124
- * passed default.
125
- * - `resumePrompt`: the captured flow-activating prompt, falling back to the last
126
- * user message in the transcript (covers a stateless agent that declines to
127
- * persist without owning a flow); `undefined` when neither exists.
128
- *
129
- * @internal
130
- */
131
- export function resolveNonResumableNotice(input: {
132
- notResumableMessage?: string;
133
- flowActivationPrompt?: string | null;
134
- messages: ReadonlyArray<ChatMessage>;
135
- defaultMessage: string;
136
- }): { resumeMessage: string; resumePrompt?: string } {
137
- const custom = input.notResumableMessage;
138
- const resumeMessage = custom && custom.trim() ? custom : input.defaultMessage;
139
- const resumePrompt =
140
- input.flowActivationPrompt ?? input.messages[lastUserMessageIndex(input.messages)]?.content;
141
- return { resumeMessage, resumePrompt };
142
- }
143
-
144
- /**
145
- * Index of the last `user`-role message in `messages`, or `-1` when there is none.
146
- * The single backward scan behind "what did the user last say" — used to slice the
147
- * transcript to a clean boundary (`keepThroughLastUserMessage`) and to recover a
148
- * flow-activation prompt. Allocation-free (no reversed copy).
149
- *
150
- * @internal
151
- */
152
- export function lastUserMessageIndex(messages: ReadonlyArray<ChatMessage>): number {
153
- for (let i = messages.length - 1; i >= 0; i -= 1) {
154
- if (messages[i]?.role === 'user') return i;
155
- }
156
- return -1;
157
- }
158
-
159
- /**
160
- * Type guard: is `value` a {@link PersistedSession} we can safely restore?
161
- * Gates on matching schema version, same host, and a messages array — the
162
- * minimum needed to avoid feeding a stale or cross-app blob into restore.
163
- *
164
- * @internal
165
- */
166
- export function isRestorableSession(
167
- value: unknown,
168
- opts: { host: string; version?: number },
169
- ): value is PersistedSession {
170
- if (!value || typeof value !== 'object') return false;
171
- const s = value as Partial<PersistedSession>;
172
- const expectedVersion = opts.version ?? PERSISTED_SESSION_VERSION;
173
- return (
174
- s.version === expectedVersion &&
175
- typeof s.host === 'string' &&
176
- s.host === opts.host &&
177
- Array.isArray(s.messages)
178
- );
179
- }
180
-
181
- /**
182
- * Project a {@link PersistedSession} onto the Redux `loadSession` payload (the
183
- * fields restored verbatim into the slice). Agent identity + machine snapshots
184
- * are applied by the host separately.
185
- *
186
- * @internal
187
- */
188
- export function toLoadSessionPayload(s: PersistedSession): LoadSessionPayload {
189
- return {
190
- messages: s.messages,
191
- pinnedAgentName: s.pinnedAgentName ?? null,
192
- flowOwnerAgentName: s.flowOwnerAgentName ?? null,
193
- // `sessionCostUsd` is deliberately NOT projected: the slice re-derives cost and
194
- // token totals from `messages` on load. The snapshot field is still written (its
195
- // shape is public, and it is a useful cross-check in a saved blob), but it is no
196
- // longer authoritative — a snapshot saved mid-reset could hold a zeroed total
197
- // while its transcript still shows the spend.
198
- contextTokens: s.contextTokens,
199
- contextLimit: s.contextLimit,
200
- activeModel: s.activeModel,
201
- activeProviderName: s.activeProviderName,
202
- };
203
- }
204
-
205
- /**
206
- * Trim a restored transcript back to the last point a provider will accept.
207
- *
208
- * Autosave fires on every transcript change (GENC-1351 §5.3) so it can land
209
- * mid-turn — meaning a snapshot can end on an *incomplete* turn:
210
- * • an assistant `tool_use` whose `tool_result` hasn't arrived (or only some
211
- * of its calls are answered) — an orphaned call makes the next turn invalid;
212
- * • an empty streaming/`thinking` placeholder assistant message;
213
- * • a pending (unresolved) interaction — a mid-`requestInteraction` turn that
214
- * can't be resumed (explicitly unsupported).
215
- *
216
- * Walks back from the end, dropping such trailing messages until the transcript
217
- * is balanced — every assistant tool call has its matching result — and ends on
218
- * a clean boundary. Returns the input unchanged (same reference) when already
219
- * clean. Turns a mid-turn reload into "recover to the last complete exchange"
220
- * rather than a hard drop.
221
- *
222
- * @internal
223
- */
224
- export function trimToRestorableBoundary(messages: ChatMessage[]): ChatMessage[] {
225
- let end = messages.length;
226
- while (end > 0) {
227
- const slice = messages.slice(0, end);
228
- const last = slice[end - 1];
229
- // Trailing unresolved interaction — can't be resumed.
230
- if (last.interaction && !last.interaction.resolved) {
231
- end -= 1;
232
- continue;
233
- }
234
- // Trailing empty assistant placeholder (streaming/thinking stub with no
235
- // content, tool calls, or interaction yet).
236
- if (
237
- last.role === 'assistant' &&
238
- !last.content &&
239
- !last.toolCalls?.length &&
240
- !last.interaction
241
- ) {
242
- end -= 1;
243
- continue;
244
- }
245
- // Any assistant tool call without its matching result — orphaned `tool_use`.
246
- if (hasUnansweredToolCall(slice)) {
247
- end -= 1;
248
- continue;
249
- }
250
- break;
251
- }
252
- return end === messages.length ? messages : messages.slice(0, end);
253
- }
254
-
255
- /**
256
- * Keep the transcript through the **last user message** (inclusive), dropping any
257
- * trailing assistant/tool messages after it (GENC-1351 §5.9). Used by the freeze
258
- * fallback: when a flow goes non-resumable and there's no prior resumable snapshot
259
- * to freeze at, we keep the conversation + the prompt that triggered the flow but
260
- * discard the agent's in-flight work — so a reload shows the user's request, not the
261
- * half-done agent turns. Returns the input unchanged (same ref) when it already ends
262
- * on a user message, or when there is no user message at all (nothing to trim to).
263
- *
264
- * @internal
265
- */
266
- export function keepThroughLastUserMessage(messages: ChatMessage[]): ChatMessage[] {
267
- const lastUser = lastUserMessageIndex(messages);
268
- if (lastUser === -1 || lastUser === messages.length - 1) return messages;
269
- return messages.slice(0, lastUser + 1);
270
- }
271
-
272
- /**
273
- * Names of agents the restored session holds machine/state snapshots for but
274
- * which are **not** in the currently-configured agent set — their flow cannot
275
- * resume. Common when the host's agent array is dynamic (agents gated by feature
276
- * flags, project state, or permissions; e.g. genesis-create). Callers should
277
- * surface these; the restore is otherwise non-fatal (the transcript and any
278
- * known agents still restore). Returns `[]` when `knownAgentNames` is empty —
279
- * the agent set isn't known yet, so we can't classify without false positives.
280
- *
281
- * @beta
282
- */
283
- export function unknownAgentSnapshots(
284
- snapshot: PersistedSession,
285
- knownAgentNames: Iterable<string>,
286
- ): string[] {
287
- const known = new Set(knownAgentNames);
288
- if (known.size === 0) return [];
289
- return Object.keys(snapshot.agentSnapshots ?? {}).filter((name) => !known.has(name));
290
- }
291
-
292
- /**
293
- * Reconcile the per-agent `serialize()` images to persist on a session save,
294
- * guarding against a post-restore autosave wiping a mid-flow machine (GENC-1351).
295
- *
296
- * A restore reads a good snapshot, but the restore's own history change fires an
297
- * autosave **before** the pinned stateful agent re-activates — at which point the
298
- * live capture (`getActiveAgent()?.serialize()`) is empty even though a flow is
299
- * mid-stream. Writing that empty map would overwrite the stored machine snapshot,
300
- * resetting the flow to its initial step on the *next* reload. So when a flow is
301
- * owned (`flowOwnerName`) but its live image is missing, carry the last-known-good
302
- * image forward instead of dropping it.
303
- *
304
- * Pure: the caller owns the `lastKnownGood` cache and its lifecycle (seeded on
305
- * restore, reset on clear). This decides only what to persist now and what the
306
- * cache should become — advanced only when non-empty, so an empty capture can
307
- * never erase a good cache.
308
- *
309
- * @param live - Agent images captured live this save, keyed by agent name.
310
- * @param flowOwnerName - The agent owning the in-flight flow, or `null`.
311
- * @param lastKnownGood - The last non-empty images persisted or restored.
312
- * @returns `snapshots` to persist, and the `cache` to remember for next time.
313
- *
314
- * @internal
315
- */
316
- export function reconcileAgentSnapshots(
317
- live: Record<string, unknown>,
318
- flowOwnerName: string | null,
319
- lastKnownGood: Record<string, unknown>,
320
- ): { snapshots: Record<string, unknown>; cache: Record<string, unknown> } {
321
- const snapshots = { ...live };
322
- if (flowOwnerName && snapshots[flowOwnerName] === undefined) {
323
- const carried = lastKnownGood[flowOwnerName];
324
- if (carried !== undefined) snapshots[flowOwnerName] = carried;
325
- }
326
- const cache = Object.keys(snapshots).length ? snapshots : lastKnownGood;
327
- return { snapshots, cache };
328
- }
329
-
330
- /** True if any assistant tool call in `messages` lacks a matching tool result. */
331
- function hasUnansweredToolCall(messages: ChatMessage[]): boolean {
332
- const answered = new Set<string>();
333
- for (const m of messages) {
334
- if (m.toolResult) answered.add(m.toolResult.toolCallId);
335
- }
336
- for (const m of messages) {
337
- if (m.toolCalls) {
338
- for (const tc of m.toolCalls) {
339
- if (!answered.has(tc.id)) return true;
340
- }
341
- }
342
- }
343
- return false;
344
- }