@qduc/term2 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (546) hide show
  1. package/dist/agent.d.ts +9 -0
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +139 -68
  4. package/dist/agent.js.map +1 -1
  5. package/dist/agent.test.js +179 -16
  6. package/dist/agent.test.js.map +1 -1
  7. package/dist/app.d.ts +16 -3
  8. package/dist/app.d.ts.map +1 -1
  9. package/dist/app.js +186 -23
  10. package/dist/app.js.map +1 -1
  11. package/dist/app.large-uncached-warning.test.d.ts +2 -0
  12. package/dist/app.large-uncached-warning.test.d.ts.map +1 -0
  13. package/dist/app.large-uncached-warning.test.js +15 -0
  14. package/dist/app.large-uncached-warning.test.js.map +1 -0
  15. package/dist/app.startup-banner.test.js +3 -19
  16. package/dist/app.startup-banner.test.js.map +1 -1
  17. package/dist/cli.js +170 -71
  18. package/dist/cli.js.map +1 -1
  19. package/dist/components/Banner.d.ts.map +1 -1
  20. package/dist/components/Banner.js +35 -17
  21. package/dist/components/Banner.js.map +1 -1
  22. package/dist/components/BottomArea.d.ts +7 -1
  23. package/dist/components/BottomArea.d.ts.map +1 -1
  24. package/dist/components/BottomArea.js +8 -6
  25. package/dist/components/BottomArea.js.map +1 -1
  26. package/dist/components/BottomArea.test.js +14 -1
  27. package/dist/components/BottomArea.test.js.map +1 -1
  28. package/dist/components/CommandMessage.d.ts.map +1 -1
  29. package/dist/components/CommandMessage.js +31 -1
  30. package/dist/components/CommandMessage.js.map +1 -1
  31. package/dist/components/CommandMessage.test.js +150 -31
  32. package/dist/components/CommandMessage.test.js.map +1 -1
  33. package/dist/components/Common/MenuContainer.d.ts +20 -0
  34. package/dist/components/Common/MenuContainer.d.ts.map +1 -0
  35. package/dist/components/Common/MenuContainer.js +52 -0
  36. package/dist/components/Common/MenuContainer.js.map +1 -0
  37. package/dist/components/Common/ScrollableTabBar.d.ts +26 -0
  38. package/dist/components/Common/ScrollableTabBar.d.ts.map +1 -0
  39. package/dist/components/Common/ScrollableTabBar.js +26 -0
  40. package/dist/components/Common/ScrollableTabBar.js.map +1 -0
  41. package/dist/components/Common/compute-visible-tabs.d.ts +16 -0
  42. package/dist/components/Common/compute-visible-tabs.d.ts.map +1 -0
  43. package/dist/components/Common/compute-visible-tabs.js +53 -0
  44. package/dist/components/Common/compute-visible-tabs.js.map +1 -0
  45. package/dist/components/Common/compute-visible-tabs.test.d.ts +2 -0
  46. package/dist/components/Common/compute-visible-tabs.test.d.ts.map +1 -0
  47. package/dist/components/Common/compute-visible-tabs.test.js +97 -0
  48. package/dist/components/Common/compute-visible-tabs.test.js.map +1 -0
  49. package/dist/components/HandoffConfirmationPrompt.d.ts +9 -0
  50. package/dist/components/HandoffConfirmationPrompt.d.ts.map +1 -0
  51. package/dist/components/HandoffConfirmationPrompt.js +38 -0
  52. package/dist/components/HandoffConfirmationPrompt.js.map +1 -0
  53. package/dist/components/HandoffConfirmationPrompt.test.d.ts +2 -0
  54. package/dist/components/HandoffConfirmationPrompt.test.d.ts.map +1 -0
  55. package/dist/components/HandoffConfirmationPrompt.test.js +12 -0
  56. package/dist/components/HandoffConfirmationPrompt.test.js.map +1 -0
  57. package/dist/components/Input/PopupManager.d.ts +5 -1
  58. package/dist/components/Input/PopupManager.d.ts.map +1 -1
  59. package/dist/components/Input/PopupManager.js +2 -2
  60. package/dist/components/Input/PopupManager.js.map +1 -1
  61. package/dist/components/Input/popup-key-navigation.js +2 -2
  62. package/dist/components/Input/popup-key-navigation.js.map +1 -1
  63. package/dist/components/Input/popup-key-navigation.test.js +5 -3
  64. package/dist/components/Input/popup-key-navigation.test.js.map +1 -1
  65. package/dist/components/Input/popup-props.d.ts.map +1 -1
  66. package/dist/components/Input/popup-props.js +4 -0
  67. package/dist/components/Input/popup-props.js.map +1 -1
  68. package/dist/components/Input/popup-props.test.d.ts +2 -0
  69. package/dist/components/Input/popup-props.test.d.ts.map +1 -0
  70. package/dist/components/Input/popup-props.test.js +70 -0
  71. package/dist/components/Input/popup-props.test.js.map +1 -0
  72. package/dist/components/InputBox.d.ts +0 -1
  73. package/dist/components/InputBox.d.ts.map +1 -1
  74. package/dist/components/InputBox.js +66 -14
  75. package/dist/components/InputBox.js.map +1 -1
  76. package/dist/components/InputBox.test.js +192 -12
  77. package/dist/components/InputBox.test.js.map +1 -1
  78. package/dist/components/MessageList.d.ts +1 -0
  79. package/dist/components/MessageList.d.ts.map +1 -1
  80. package/dist/components/MessageList.js.map +1 -1
  81. package/dist/components/ModelSelectionMenu.d.ts.map +1 -1
  82. package/dist/components/ModelSelectionMenu.debug.test.d.ts +2 -0
  83. package/dist/components/ModelSelectionMenu.debug.test.d.ts.map +1 -0
  84. package/dist/components/ModelSelectionMenu.debug.test.js.map +1 -0
  85. package/dist/components/ModelSelectionMenu.js +29 -125
  86. package/dist/components/ModelSelectionMenu.js.map +1 -1
  87. package/dist/components/ModelSelectionMenu.test.js +9 -5
  88. package/dist/components/ModelSelectionMenu.test.js.map +1 -1
  89. package/dist/components/PathSelectionMenu.d.ts.map +1 -1
  90. package/dist/components/PathSelectionMenu.js +15 -55
  91. package/dist/components/PathSelectionMenu.js.map +1 -1
  92. package/dist/components/SettingsSelectionMenu.d.ts +4 -1
  93. package/dist/components/SettingsSelectionMenu.d.ts.map +1 -1
  94. package/dist/components/SettingsSelectionMenu.js +48 -104
  95. package/dist/components/SettingsSelectionMenu.js.map +1 -1
  96. package/dist/components/SettingsSelectionMenu.scroll.test.js +140 -0
  97. package/dist/components/SettingsSelectionMenu.scroll.test.js.map +1 -1
  98. package/dist/components/SettingsSelectionMenu.test.js +38 -5
  99. package/dist/components/SettingsSelectionMenu.test.js.map +1 -1
  100. package/dist/components/SettingsValueSelectionMenu.d.ts.map +1 -1
  101. package/dist/components/SettingsValueSelectionMenu.js +24 -51
  102. package/dist/components/SettingsValueSelectionMenu.js.map +1 -1
  103. package/dist/components/SlashCommandMenu.d.ts +1 -0
  104. package/dist/components/SlashCommandMenu.d.ts.map +1 -1
  105. package/dist/components/SlashCommandMenu.js +11 -14
  106. package/dist/components/SlashCommandMenu.js.map +1 -1
  107. package/dist/components/StatusBar.d.ts +2 -0
  108. package/dist/components/StatusBar.d.ts.map +1 -1
  109. package/dist/components/StatusBar.js +35 -2
  110. package/dist/components/StatusBar.js.map +1 -1
  111. package/dist/components/StatusBar.test.js +32 -0
  112. package/dist/components/StatusBar.test.js.map +1 -1
  113. package/dist/components/UndoSelectionMenu.d.ts.map +1 -1
  114. package/dist/components/UndoSelectionMenu.js +6 -33
  115. package/dist/components/UndoSelectionMenu.js.map +1 -1
  116. package/dist/context/InputContext.stability.test.js +9 -7
  117. package/dist/context/InputContext.stability.test.js.map +1 -1
  118. package/dist/env-setup.js +1 -0
  119. package/dist/env-setup.js.map +1 -1
  120. package/dist/hooks/use-app-commands.d.ts +22 -2
  121. package/dist/hooks/use-app-commands.d.ts.map +1 -1
  122. package/dist/hooks/use-app-commands.js +43 -3
  123. package/dist/hooks/use-app-commands.js.map +1 -1
  124. package/dist/hooks/use-app-commands.test.js +135 -1
  125. package/dist/hooks/use-app-commands.test.js.map +1 -1
  126. package/dist/hooks/use-conversation.clear.test.d.ts +2 -0
  127. package/dist/hooks/use-conversation.clear.test.d.ts.map +1 -0
  128. package/dist/hooks/use-conversation.clear.test.js +54 -0
  129. package/dist/hooks/use-conversation.clear.test.js.map +1 -0
  130. package/dist/hooks/use-conversation.d.ts +25 -2
  131. package/dist/hooks/use-conversation.d.ts.map +1 -1
  132. package/dist/hooks/use-conversation.js +90 -12
  133. package/dist/hooks/use-conversation.js.map +1 -1
  134. package/dist/hooks/use-escape-key.d.ts +8 -1
  135. package/dist/hooks/use-escape-key.d.ts.map +1 -1
  136. package/dist/hooks/use-escape-key.js +15 -4
  137. package/dist/hooks/use-escape-key.js.map +1 -1
  138. package/dist/hooks/use-escape-key.test.js +135 -28
  139. package/dist/hooks/use-escape-key.test.js.map +1 -1
  140. package/dist/hooks/use-input-history.test.js +14 -8
  141. package/dist/hooks/use-input-history.test.js.map +1 -1
  142. package/dist/hooks/use-mode-handlers.d.ts +12 -1
  143. package/dist/hooks/use-mode-handlers.d.ts.map +1 -1
  144. package/dist/hooks/use-mode-handlers.js +31 -1
  145. package/dist/hooks/use-mode-handlers.js.map +1 -1
  146. package/dist/hooks/use-model-selection.d.ts +6 -1
  147. package/dist/hooks/use-model-selection.d.ts.map +1 -1
  148. package/dist/hooks/use-model-selection.js +50 -8
  149. package/dist/hooks/use-model-selection.js.map +1 -1
  150. package/dist/hooks/use-model-selection.test.js +306 -50
  151. package/dist/hooks/use-model-selection.test.js.map +1 -1
  152. package/dist/hooks/use-path-completion.d.ts +4 -0
  153. package/dist/hooks/use-path-completion.d.ts.map +1 -1
  154. package/dist/hooks/use-path-completion.js +23 -17
  155. package/dist/hooks/use-path-completion.js.map +1 -1
  156. package/dist/hooks/use-runtime-settings.d.ts.map +1 -1
  157. package/dist/hooks/use-runtime-settings.js +7 -2
  158. package/dist/hooks/use-runtime-settings.js.map +1 -1
  159. package/dist/hooks/use-runtime-settings.test.d.ts +2 -0
  160. package/dist/hooks/use-runtime-settings.test.d.ts.map +1 -0
  161. package/dist/hooks/use-runtime-settings.test.js +45 -0
  162. package/dist/hooks/use-runtime-settings.test.js.map +1 -0
  163. package/dist/hooks/use-selection.d.ts +6 -2
  164. package/dist/hooks/use-selection.d.ts.map +1 -1
  165. package/dist/hooks/use-selection.js +53 -16
  166. package/dist/hooks/use-selection.js.map +1 -1
  167. package/dist/hooks/use-selection.test.d.ts +2 -0
  168. package/dist/hooks/use-selection.test.d.ts.map +1 -0
  169. package/dist/hooks/use-selection.test.js +132 -0
  170. package/dist/hooks/use-selection.test.js.map +1 -0
  171. package/dist/hooks/use-settings-completion.d.ts +19 -0
  172. package/dist/hooks/use-settings-completion.d.ts.map +1 -1
  173. package/dist/hooks/use-settings-completion.js +106 -18
  174. package/dist/hooks/use-settings-completion.js.map +1 -1
  175. package/dist/hooks/use-settings-completion.scroll.test.js +68 -29
  176. package/dist/hooks/use-settings-completion.scroll.test.js.map +1 -1
  177. package/dist/hooks/use-settings-completion.test.js +36 -1
  178. package/dist/hooks/use-settings-completion.test.js.map +1 -1
  179. package/dist/hooks/use-settings-value-completion.d.ts +7 -1
  180. package/dist/hooks/use-settings-value-completion.d.ts.map +1 -1
  181. package/dist/hooks/use-settings-value-completion.js +18 -7
  182. package/dist/hooks/use-settings-value-completion.js.map +1 -1
  183. package/dist/hooks/use-slash-commands.d.ts +5 -0
  184. package/dist/hooks/use-slash-commands.d.ts.map +1 -1
  185. package/dist/hooks/use-slash-commands.js +22 -2
  186. package/dist/hooks/use-slash-commands.js.map +1 -1
  187. package/dist/hooks/use-trigger-detection.test.js +9 -6
  188. package/dist/hooks/use-trigger-detection.test.js.map +1 -1
  189. package/dist/hooks/use-undo-selection.d.ts +4 -0
  190. package/dist/hooks/use-undo-selection.d.ts.map +1 -1
  191. package/dist/hooks/use-undo-selection.js +18 -1
  192. package/dist/hooks/use-undo-selection.js.map +1 -1
  193. package/dist/inspect_zod.d.ts +2 -0
  194. package/dist/inspect_zod.d.ts.map +1 -0
  195. package/dist/inspect_zod.js +16 -0
  196. package/dist/inspect_zod.js.map +1 -0
  197. package/dist/lib/openai-agent-client.chat.test.js +40 -0
  198. package/dist/lib/openai-agent-client.chat.test.js.map +1 -1
  199. package/dist/lib/openai-agent-client.d.ts +18 -4
  200. package/dist/lib/openai-agent-client.d.ts.map +1 -1
  201. package/dist/lib/openai-agent-client.js +228 -32
  202. package/dist/lib/openai-agent-client.js.map +1 -1
  203. package/dist/lib/openai-agent-client.public-methods.test.js +193 -0
  204. package/dist/lib/openai-agent-client.public-methods.test.js.map +1 -1
  205. package/dist/lib/openai-agent-client.test.js +71 -1
  206. package/dist/lib/openai-agent-client.test.js.map +1 -1
  207. package/dist/lib/retry-executor.js +45 -7
  208. package/dist/lib/retry-executor.js.map +1 -1
  209. package/dist/lib/retry-executor.test.js +63 -0
  210. package/dist/lib/retry-executor.test.js.map +1 -1
  211. package/dist/lib/tool-invoke.d.ts +3 -2
  212. package/dist/lib/tool-invoke.d.ts.map +1 -1
  213. package/dist/lib/tool-invoke.js +213 -10
  214. package/dist/lib/tool-invoke.js.map +1 -1
  215. package/dist/lib/tool-invoke.test.js +103 -0
  216. package/dist/lib/tool-invoke.test.js.map +1 -1
  217. package/dist/non-interactive.d.ts +1 -0
  218. package/dist/non-interactive.d.ts.map +1 -1
  219. package/dist/non-interactive.js +3 -1
  220. package/dist/non-interactive.js.map +1 -1
  221. package/dist/non-interactive.test.js +8 -1
  222. package/dist/non-interactive.test.js.map +1 -1
  223. package/dist/prompts/codex.md +105 -97
  224. package/dist/prompts/fragments/gpt-5.3-codex.md +7 -0
  225. package/dist/prompts/fragments/gpt-5.4-small.md +6 -0
  226. package/dist/prompts/fragments/gpt-5.4.md +7 -0
  227. package/dist/prompts/fragments/gpt-5.5.md +7 -0
  228. package/dist/prompts/gpt-5-modern.md +96 -54
  229. package/dist/prompts/plan-mode-info.md +14 -3
  230. package/dist/prompts/prompt-constructor.d.ts +19 -0
  231. package/dist/prompts/prompt-constructor.d.ts.map +1 -0
  232. package/dist/prompts/prompt-constructor.js +53 -0
  233. package/dist/prompts/prompt-constructor.js.map +1 -0
  234. package/dist/prompts/prompt-constructor.test.d.ts +2 -0
  235. package/dist/prompts/prompt-constructor.test.d.ts.map +1 -0
  236. package/dist/prompts/prompt-constructor.test.js +99 -0
  237. package/dist/prompts/prompt-constructor.test.js.map +1 -0
  238. package/dist/prompts/prompt-constructor.test.ts +112 -0
  239. package/dist/prompts/prompt-constructor.ts +91 -0
  240. package/dist/prompts/prompt-profiles.d.ts +17 -0
  241. package/dist/prompts/prompt-profiles.d.ts.map +1 -0
  242. package/dist/prompts/prompt-profiles.js +61 -0
  243. package/dist/prompts/prompt-profiles.js.map +1 -0
  244. package/dist/prompts/prompt-profiles.ts +77 -0
  245. package/dist/prompts/prompt-selector.d.ts.map +1 -1
  246. package/dist/prompts/prompt-selector.js +3 -21
  247. package/dist/prompts/prompt-selector.js.map +1 -1
  248. package/dist/prompts/prompt-selector.ts +3 -25
  249. package/dist/prompts/reasoning-efficiency.d.ts.map +1 -1
  250. package/dist/prompts/reasoning-efficiency.js +2 -0
  251. package/dist/prompts/reasoning-efficiency.js.map +1 -1
  252. package/dist/prompts/reasoning-efficiency.test.d.ts +2 -0
  253. package/dist/prompts/reasoning-efficiency.test.d.ts.map +1 -0
  254. package/dist/prompts/reasoning-efficiency.test.js +11 -0
  255. package/dist/prompts/reasoning-efficiency.test.js.map +1 -0
  256. package/dist/prompts/reasoning-efficiency.test.ts +12 -0
  257. package/dist/prompts/reasoning-efficiency.ts +2 -0
  258. package/dist/prompts/search-via-shell.test.js +19 -16
  259. package/dist/prompts/search-via-shell.test.js.map +1 -1
  260. package/dist/prompts/search-via-shell.test.ts +20 -16
  261. package/dist/prompts/subagent-delegation.test.js +12 -10
  262. package/dist/prompts/subagent-delegation.test.js.map +1 -1
  263. package/dist/prompts/subagent-delegation.test.ts +13 -10
  264. package/dist/prompts/worktree-hygiene.md +9 -0
  265. package/dist/providers/ai-sdk-logging-fetch.d.ts +1 -1
  266. package/dist/providers/ai-sdk-logging-fetch.d.ts.map +1 -1
  267. package/dist/providers/ai-sdk-logging-fetch.js +145 -47
  268. package/dist/providers/ai-sdk-logging-fetch.js.map +1 -1
  269. package/dist/providers/ai-sdk-logging-fetch.test.js +202 -5
  270. package/dist/providers/ai-sdk-logging-fetch.test.js.map +1 -1
  271. package/dist/providers/codex-responses-model.d.ts +20 -0
  272. package/dist/providers/codex-responses-model.d.ts.map +1 -0
  273. package/dist/providers/codex-responses-model.js +170 -0
  274. package/dist/providers/codex-responses-model.js.map +1 -0
  275. package/dist/providers/codex-responses-model.test.d.ts +2 -0
  276. package/dist/providers/codex-responses-model.test.d.ts.map +1 -0
  277. package/dist/providers/codex-responses-model.test.js +240 -0
  278. package/dist/providers/codex-responses-model.test.js.map +1 -0
  279. package/dist/providers/codex.provider.d.ts +29 -0
  280. package/dist/providers/codex.provider.d.ts.map +1 -0
  281. package/dist/providers/codex.provider.js +486 -0
  282. package/dist/providers/codex.provider.js.map +1 -0
  283. package/dist/providers/codex.provider.test.d.ts +2 -0
  284. package/dist/providers/codex.provider.test.d.ts.map +1 -0
  285. package/dist/providers/codex.provider.test.js +634 -0
  286. package/dist/providers/codex.provider.test.js.map +1 -0
  287. package/dist/providers/custom-provider-adapter.test.js +56 -0
  288. package/dist/providers/custom-provider-adapter.test.js.map +1 -1
  289. package/dist/providers/fallback-responses-model.d.ts +20 -0
  290. package/dist/providers/fallback-responses-model.d.ts.map +1 -0
  291. package/dist/providers/fallback-responses-model.js +277 -0
  292. package/dist/providers/fallback-responses-model.js.map +1 -0
  293. package/dist/providers/fallback-responses-model.test.d.ts +2 -0
  294. package/dist/providers/fallback-responses-model.test.d.ts.map +1 -0
  295. package/dist/providers/fallback-responses-model.test.js +279 -0
  296. package/dist/providers/fallback-responses-model.test.js.map +1 -0
  297. package/dist/providers/fetch/compose.d.ts +7 -0
  298. package/dist/providers/fetch/compose.d.ts.map +1 -0
  299. package/dist/providers/fetch/compose.js +20 -0
  300. package/dist/providers/fetch/compose.js.map +1 -0
  301. package/dist/providers/fetch/composer.d.ts +13 -0
  302. package/dist/providers/fetch/composer.d.ts.map +1 -0
  303. package/dist/providers/fetch/composer.js +15 -0
  304. package/dist/providers/fetch/composer.js.map +1 -0
  305. package/dist/providers/fetch/composer.test.d.ts +2 -0
  306. package/dist/providers/fetch/composer.test.d.ts.map +1 -0
  307. package/dist/providers/fetch/composer.test.js +448 -0
  308. package/dist/providers/fetch/composer.test.js.map +1 -0
  309. package/dist/providers/fetch/logging-middleware.d.ts +16 -0
  310. package/dist/providers/fetch/logging-middleware.d.ts.map +1 -0
  311. package/dist/providers/fetch/logging-middleware.js +214 -0
  312. package/dist/providers/fetch/logging-middleware.js.map +1 -0
  313. package/dist/providers/index.d.ts +1 -0
  314. package/dist/providers/index.d.ts.map +1 -1
  315. package/dist/providers/index.js +1 -0
  316. package/dist/providers/index.js.map +1 -1
  317. package/dist/providers/llama-cpp.provider.d.ts +3 -0
  318. package/dist/providers/llama-cpp.provider.d.ts.map +1 -0
  319. package/dist/providers/llama-cpp.provider.js +34 -0
  320. package/dist/providers/llama-cpp.provider.js.map +1 -0
  321. package/dist/providers/openai-compatible-middleware.d.ts +7 -0
  322. package/dist/providers/openai-compatible-middleware.d.ts.map +1 -0
  323. package/dist/providers/openai-compatible-middleware.js +133 -0
  324. package/dist/providers/openai-compatible-middleware.js.map +1 -0
  325. package/dist/providers/openai-compatible-models.d.ts +6 -0
  326. package/dist/providers/openai-compatible-models.d.ts.map +1 -0
  327. package/dist/providers/openai-compatible-models.js +17 -0
  328. package/dist/providers/openai-compatible-models.js.map +1 -0
  329. package/dist/providers/openai-compatible-response-normalizer.d.ts +7 -0
  330. package/dist/providers/openai-compatible-response-normalizer.d.ts.map +1 -0
  331. package/dist/providers/openai-compatible-response-normalizer.js +58 -0
  332. package/dist/providers/openai-compatible-response-normalizer.js.map +1 -0
  333. package/dist/providers/openai-compatible.provider.d.ts +4 -1
  334. package/dist/providers/openai-compatible.provider.d.ts.map +1 -1
  335. package/dist/providers/openai-compatible.provider.js +68 -324
  336. package/dist/providers/openai-compatible.provider.js.map +1 -1
  337. package/dist/providers/openai-compatible.provider.test.js +106 -1
  338. package/dist/providers/openai-compatible.provider.test.js.map +1 -1
  339. package/dist/providers/openai.provider.js +53 -1
  340. package/dist/providers/openai.provider.js.map +1 -1
  341. package/dist/providers/opencode.provider.d.ts +19 -0
  342. package/dist/providers/opencode.provider.d.ts.map +1 -0
  343. package/dist/providers/opencode.provider.js +45 -0
  344. package/dist/providers/opencode.provider.js.map +1 -0
  345. package/dist/providers/openrouter.provider.d.ts +2 -1
  346. package/dist/providers/openrouter.provider.d.ts.map +1 -1
  347. package/dist/providers/openrouter.provider.js +39 -22
  348. package/dist/providers/openrouter.provider.js.map +1 -1
  349. package/dist/providers/openrouter.provider.request-preprocessing.test.d.ts +2 -0
  350. package/dist/providers/openrouter.provider.request-preprocessing.test.d.ts.map +1 -0
  351. package/dist/providers/openrouter.provider.request-preprocessing.test.js +51 -0
  352. package/dist/providers/openrouter.provider.request-preprocessing.test.js.map +1 -0
  353. package/dist/providers/registry.d.ts +1 -0
  354. package/dist/providers/registry.d.ts.map +1 -1
  355. package/dist/providers/registry.js.map +1 -1
  356. package/dist/providers/registry.test.js +11 -2
  357. package/dist/providers/registry.test.js.map +1 -1
  358. package/dist/scripts/eval-auto-approval/dataset.d.ts +3 -3
  359. package/dist/services/approval-flow-coordinator.d.ts.map +1 -1
  360. package/dist/services/approval-flow-coordinator.js +17 -1
  361. package/dist/services/approval-flow-coordinator.js.map +1 -1
  362. package/dist/services/approval-flow-coordinator.test.js +22 -0
  363. package/dist/services/approval-flow-coordinator.test.js.map +1 -1
  364. package/dist/services/command-message-streaming.d.ts.map +1 -1
  365. package/dist/services/command-message-streaming.js +0 -3
  366. package/dist/services/command-message-streaming.js.map +1 -1
  367. package/dist/services/command-message-streaming.test.js +4 -2
  368. package/dist/services/command-message-streaming.test.js.map +1 -1
  369. package/dist/services/conversation-clear-save.test.d.ts +2 -0
  370. package/dist/services/conversation-clear-save.test.d.ts.map +1 -0
  371. package/dist/services/conversation-clear-save.test.js +57 -0
  372. package/dist/services/conversation-clear-save.test.js.map +1 -0
  373. package/dist/services/conversation-events.d.ts +44 -1
  374. package/dist/services/conversation-events.d.ts.map +1 -1
  375. package/dist/services/conversation-history-repair.d.ts +25 -0
  376. package/dist/services/conversation-history-repair.d.ts.map +1 -0
  377. package/dist/services/conversation-history-repair.js +227 -0
  378. package/dist/services/conversation-history-repair.js.map +1 -0
  379. package/dist/services/conversation-history-repair.test.d.ts +2 -0
  380. package/dist/services/conversation-history-repair.test.d.ts.map +1 -0
  381. package/dist/services/conversation-history-repair.test.js +76 -0
  382. package/dist/services/conversation-history-repair.test.js.map +1 -0
  383. package/dist/services/conversation-integration.test.js +3 -1
  384. package/dist/services/conversation-integration.test.js.map +1 -1
  385. package/dist/services/conversation-log-events.d.ts +107 -0
  386. package/dist/services/conversation-log-events.d.ts.map +1 -0
  387. package/dist/services/conversation-log-events.js +12 -0
  388. package/dist/services/conversation-log-events.js.map +1 -0
  389. package/dist/services/conversation-log-writer.d.ts +40 -0
  390. package/dist/services/conversation-log-writer.d.ts.map +1 -0
  391. package/dist/services/conversation-log-writer.js +264 -0
  392. package/dist/services/conversation-log-writer.js.map +1 -0
  393. package/dist/services/conversation-persistence-types.d.ts +14 -0
  394. package/dist/services/conversation-persistence-types.d.ts.map +1 -0
  395. package/dist/services/conversation-persistence-types.js +2 -0
  396. package/dist/services/conversation-persistence-types.js.map +1 -0
  397. package/dist/services/conversation-persistence.d.ts +40 -38
  398. package/dist/services/conversation-persistence.d.ts.map +1 -1
  399. package/dist/services/conversation-persistence.js +312 -71
  400. package/dist/services/conversation-persistence.js.map +1 -1
  401. package/dist/services/conversation-persistence.test.js +485 -355
  402. package/dist/services/conversation-persistence.test.js.map +1 -1
  403. package/dist/services/conversation-replay.d.ts +26 -0
  404. package/dist/services/conversation-replay.d.ts.map +1 -0
  405. package/dist/services/conversation-replay.js +252 -0
  406. package/dist/services/conversation-replay.js.map +1 -0
  407. package/dist/services/conversation-replay.test.d.ts +2 -0
  408. package/dist/services/conversation-replay.test.d.ts.map +1 -0
  409. package/dist/services/conversation-replay.test.js +153 -0
  410. package/dist/services/conversation-replay.test.js.map +1 -0
  411. package/dist/services/conversation-result-builder.d.ts.map +1 -1
  412. package/dist/services/conversation-result-builder.js +8 -2
  413. package/dist/services/conversation-result-builder.js.map +1 -1
  414. package/dist/services/conversation-result-builder.test.js +53 -0
  415. package/dist/services/conversation-result-builder.test.js.map +1 -1
  416. package/dist/services/conversation-retry-policy.d.ts +24 -0
  417. package/dist/services/conversation-retry-policy.d.ts.map +1 -1
  418. package/dist/services/conversation-retry-policy.js +68 -4
  419. package/dist/services/conversation-retry-policy.js.map +1 -1
  420. package/dist/services/conversation-retry-policy.test.js +99 -2
  421. package/dist/services/conversation-retry-policy.test.js.map +1 -1
  422. package/dist/services/conversation-service.d.ts +12 -2
  423. package/dist/services/conversation-service.d.ts.map +1 -1
  424. package/dist/services/conversation-service.js +29 -3
  425. package/dist/services/conversation-service.js.map +1 -1
  426. package/dist/services/conversation-session.auto-approval.test.js +2 -1
  427. package/dist/services/conversation-session.auto-approval.test.js.map +1 -1
  428. package/dist/services/conversation-session.d.ts +17 -1
  429. package/dist/services/conversation-session.d.ts.map +1 -1
  430. package/dist/services/conversation-session.js +580 -134
  431. package/dist/services/conversation-session.js.map +1 -1
  432. package/dist/services/conversation-store.d.ts.map +1 -1
  433. package/dist/services/conversation-store.js +6 -5
  434. package/dist/services/conversation-store.js.map +1 -1
  435. package/dist/services/conversation-store.test.js +34 -0
  436. package/dist/services/conversation-store.test.js.map +1 -1
  437. package/dist/services/input-surge-guard.d.ts +4 -0
  438. package/dist/services/input-surge-guard.d.ts.map +1 -1
  439. package/dist/services/input-surge-guard.js +2 -2
  440. package/dist/services/input-surge-guard.js.map +1 -1
  441. package/dist/services/input-surge-guard.test.js +15 -1
  442. package/dist/services/input-surge-guard.test.js.map +1 -1
  443. package/dist/services/large-uncached-input-guard.d.ts +35 -0
  444. package/dist/services/large-uncached-input-guard.d.ts.map +1 -0
  445. package/dist/services/large-uncached-input-guard.js +127 -0
  446. package/dist/services/large-uncached-input-guard.js.map +1 -0
  447. package/dist/services/large-uncached-input-guard.test.d.ts +2 -0
  448. package/dist/services/large-uncached-input-guard.test.d.ts.map +1 -0
  449. package/dist/services/large-uncached-input-guard.test.js +160 -0
  450. package/dist/services/large-uncached-input-guard.test.js.map +1 -0
  451. package/dist/services/logging-contract.d.ts +1 -1
  452. package/dist/services/logging-service.d.ts +4 -0
  453. package/dist/services/logging-service.d.ts.map +1 -1
  454. package/dist/services/logging-service.js +91 -33
  455. package/dist/services/logging-service.js.map +1 -1
  456. package/dist/services/model-service.d.ts +2 -0
  457. package/dist/services/model-service.d.ts.map +1 -1
  458. package/dist/services/model-service.js +7 -1
  459. package/dist/services/model-service.js.map +1 -1
  460. package/dist/services/provider-traffic.d.ts +87 -0
  461. package/dist/services/provider-traffic.d.ts.map +1 -0
  462. package/dist/services/provider-traffic.js +603 -0
  463. package/dist/services/provider-traffic.js.map +1 -0
  464. package/dist/services/provider-traffic.test.d.ts +2 -0
  465. package/dist/services/provider-traffic.test.d.ts.map +1 -0
  466. package/dist/services/provider-traffic.test.js +475 -0
  467. package/dist/services/provider-traffic.test.js.map +1 -0
  468. package/dist/services/rtk-service.d.ts.map +1 -1
  469. package/dist/services/rtk-service.js +1 -2
  470. package/dist/services/rtk-service.js.map +1 -1
  471. package/dist/services/service-interfaces.d.ts +16 -0
  472. package/dist/services/service-interfaces.d.ts.map +1 -1
  473. package/dist/services/settings-schema.d.ts +36 -28
  474. package/dist/services/settings-schema.d.ts.map +1 -1
  475. package/dist/services/settings-schema.js +7 -2
  476. package/dist/services/settings-schema.js.map +1 -1
  477. package/dist/services/settings-service.js +6 -6
  478. package/dist/services/settings-service.js.map +1 -1
  479. package/dist/services/settings-sources.d.ts.map +1 -1
  480. package/dist/services/settings-sources.js +1 -0
  481. package/dist/services/settings-sources.js.map +1 -1
  482. package/dist/services/shell-auto-approval-evaluator.d.ts +1 -1
  483. package/dist/services/shell-auto-approval-evaluator.d.ts.map +1 -1
  484. package/dist/services/shell-auto-approval-evaluator.js +284 -81
  485. package/dist/services/shell-auto-approval-evaluator.js.map +1 -1
  486. package/dist/services/shell-auto-approval-evaluator.test.js +215 -15
  487. package/dist/services/shell-auto-approval-evaluator.test.js.map +1 -1
  488. package/dist/services/stream-event-processor.d.ts +4 -0
  489. package/dist/services/stream-event-processor.d.ts.map +1 -1
  490. package/dist/services/stream-event-processor.js +78 -39
  491. package/dist/services/stream-event-processor.js.map +1 -1
  492. package/dist/services/stream-event-processor.test.js +71 -0
  493. package/dist/services/stream-event-processor.test.js.map +1 -1
  494. package/dist/services/subagents/subagent-manager.d.ts +2 -0
  495. package/dist/services/subagents/subagent-manager.d.ts.map +1 -1
  496. package/dist/services/subagents/subagent-manager.js +124 -21
  497. package/dist/services/subagents/subagent-manager.js.map +1 -1
  498. package/dist/services/subagents/subagent-manager.test.js +453 -0
  499. package/dist/services/subagents/subagent-manager.test.js.map +1 -1
  500. package/dist/services/terminal-result-collector.d.ts.map +1 -1
  501. package/dist/services/terminal-result-collector.js +6 -1
  502. package/dist/services/terminal-result-collector.js.map +1 -1
  503. package/dist/services/tool-call-arguments.d.ts +20 -0
  504. package/dist/services/tool-call-arguments.d.ts.map +1 -0
  505. package/dist/services/tool-call-arguments.js +49 -0
  506. package/dist/services/tool-call-arguments.js.map +1 -0
  507. package/dist/services/tool-execution-ledger.d.ts +35 -0
  508. package/dist/services/tool-execution-ledger.d.ts.map +1 -0
  509. package/dist/services/tool-execution-ledger.js +204 -0
  510. package/dist/services/tool-execution-ledger.js.map +1 -0
  511. package/dist/services/tool-execution-ledger.test.d.ts +2 -0
  512. package/dist/services/tool-execution-ledger.test.d.ts.map +1 -0
  513. package/dist/services/tool-execution-ledger.test.js +96 -0
  514. package/dist/services/tool-execution-ledger.test.js.map +1 -0
  515. package/dist/tools/apply-patch.d.ts +8 -0
  516. package/dist/tools/apply-patch.d.ts.map +1 -1
  517. package/dist/tools/apply-patch.js +157 -2
  518. package/dist/tools/apply-patch.js.map +1 -1
  519. package/dist/tools/apply-patch.test.js +81 -0
  520. package/dist/tools/apply-patch.test.js.map +1 -1
  521. package/dist/tools/search-replace.d.ts.map +1 -1
  522. package/dist/tools/search-replace.js +92 -140
  523. package/dist/tools/search-replace.js.map +1 -1
  524. package/dist/utils/command-safety/path-analysis.d.ts.map +1 -1
  525. package/dist/utils/command-safety/path-analysis.js +143 -35
  526. package/dist/utils/command-safety/path-analysis.js.map +1 -1
  527. package/dist/utils/conversation-event-handler.d.ts.map +1 -1
  528. package/dist/utils/conversation-event-handler.js +52 -4
  529. package/dist/utils/conversation-event-handler.js.map +1 -1
  530. package/dist/utils/conversation-event-handler.test.js +175 -0
  531. package/dist/utils/conversation-event-handler.test.js.map +1 -1
  532. package/dist/utils/error-helpers.d.ts +1 -0
  533. package/dist/utils/error-helpers.d.ts.map +1 -1
  534. package/dist/utils/error-helpers.js +61 -0
  535. package/dist/utils/error-helpers.js.map +1 -1
  536. package/dist/utils/error-helpers.test.js +13 -1
  537. package/dist/utils/error-helpers.test.js.map +1 -1
  538. package/dist/utils/ink-render-options.d.ts +1 -1
  539. package/dist/utils/ink-render-options.d.ts.map +1 -1
  540. package/dist/utils/ink-render-options.js +1 -1
  541. package/dist/utils/ink-render-options.js.map +1 -1
  542. package/dist/utils/streaming-session-factory.d.ts +2 -0
  543. package/dist/utils/streaming-session-factory.d.ts.map +1 -1
  544. package/dist/utils/streaming-session-factory.js +4 -0
  545. package/dist/utils/streaming-session-factory.js.map +1 -1
  546. package/package.json +2 -2
@@ -3,18 +3,23 @@ import fs from 'node:fs';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import * as persistenceModule from './conversation-persistence.js';
6
+ import { createConversationLogWriter, LockConflictError } from './conversation-log-writer.js';
6
7
  let testDir = '';
7
- // Clean up all conversation files before and after each test
8
8
  function cleanupAll() {
9
9
  if (testDir && fs.existsSync(testDir)) {
10
10
  fs.rmSync(testDir, { recursive: true, force: true });
11
11
  }
12
12
  }
13
- function waitForDistinctTimestamp() {
14
- const start = Date.now();
15
- while (Date.now() - start < 10) {
16
- // busy wait for 10ms
17
- }
13
+ const stubLogger = {
14
+ error: () => { },
15
+ warn: () => { },
16
+ info: () => { },
17
+ debug: () => { },
18
+ trace: () => { },
19
+ getCorrelationId: () => undefined,
20
+ };
21
+ function emptySnapshot() {
22
+ return { history: [], previousResponseId: null, toolLedger: [] };
18
23
  }
19
24
  test.beforeEach(() => {
20
25
  testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'term2-conversations-test-'));
@@ -30,389 +35,514 @@ test.serial('generateId: returns a valid UUID', (t) => {
30
35
  t.regex(id, /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
31
36
  });
32
37
  test.serial('generateId: returns unique IDs', (t) => {
33
- const id1 = persistenceModule.generateId();
34
- const id2 = persistenceModule.generateId();
35
- t.not(id1, id2);
38
+ t.not(persistenceModule.generateId(), persistenceModule.generateId());
36
39
  });
37
40
  test.serial('getResumeCommand: returns correct format', (t) => {
38
41
  const id = 'test-uuid-123';
39
- const cmd = persistenceModule.getResumeCommand(id);
40
- t.is(cmd, 'term2 --resume test-uuid-123');
42
+ t.is(persistenceModule.getResumeCommand(id), 'term2 --resume test-uuid-123');
43
+ t.is(persistenceModule.getResumeCommand(id, 'user@host'), 'term2 --ssh user@host --resume test-uuid-123');
44
+ t.is(persistenceModule.getResumeCommand(id, 'user@host', '/path'), 'term2 --ssh user@host --remote-dir /path --resume test-uuid-123');
45
+ t.is(persistenceModule.getResumeCommand(id, 'user@host', '/path', 2222), 'term2 --ssh user@host --remote-dir /path --ssh-port 2222 --resume test-uuid-123');
41
46
  });
42
- test.serial('saveConversation: creates file and last.json pointer', (t) => {
47
+ test.serial('writer + loadConversation: round-trips a basic conversation', (t) => {
43
48
  const id = persistenceModule.generateId();
44
- const conversation = {
49
+ const writer = createConversationLogWriter({ sessionId: id, dir: testDir, logger: stubLogger });
50
+ writer.init({
45
51
  id,
46
- createdAt: new Date().toISOString(),
47
- updatedAt: new Date().toISOString(),
52
+ createdAt: '2026-05-26T00:00:00.000Z',
53
+ projectPath: '/workspace/x',
48
54
  model: 'gpt-4o',
49
- previousResponseId: null,
50
- history: [{ role: 'user', content: 'hello' }],
51
- messages: [
52
- { id: 'msg-1', sender: 'user', text: 'hello' },
53
- { id: 'msg-2', sender: 'bot', text: 'hi there', status: 'finalized' },
54
- ],
55
- };
56
- const filePath = persistenceModule.saveConversation(conversation);
57
- t.true(fs.existsSync(filePath));
58
- // Verify last.json was created
59
- const lastPath = path.join(path.dirname(filePath), 'last.json');
60
- t.true(fs.existsSync(lastPath));
61
- const lastData = JSON.parse(fs.readFileSync(lastPath, 'utf-8'));
62
- t.is(lastData.id, id);
63
- // Cleanup
64
- fs.rmSync(path.dirname(filePath), { recursive: true, force: true });
65
- });
66
- test.serial('getConversationsDirForTest: uses isolated test override', (t) => {
67
- t.is(persistenceModule.getConversationsDirForTest(), testDir);
68
- });
69
- test.serial('saveConversation: normalizes streaming bot messages to finalized', (t) => {
55
+ provider: 'openai',
56
+ });
57
+ writer.append({ type: 'user_message', message: { id: 'u1', sender: 'user', text: 'hello' } });
58
+ writer.append({
59
+ type: 'assistant_final',
60
+ message: { id: 'b1', sender: 'bot', status: 'finalized', text: 'hi there' },
61
+ finalText: 'hi there',
62
+ snapshot: {
63
+ history: [
64
+ { role: 'user', type: 'message', content: 'hello' },
65
+ { role: 'assistant', type: 'message', content: 'hi there' },
66
+ ],
67
+ previousResponseId: 'resp-1',
68
+ toolLedger: [],
69
+ model: 'gpt-4o',
70
+ provider: 'openai',
71
+ },
72
+ });
73
+ void writer.close();
74
+ const restored = persistenceModule.loadConversation(id);
75
+ t.truthy(restored);
76
+ t.is(restored.id, id);
77
+ t.is(restored.previousResponseId, 'resp-1');
78
+ t.is(restored.history.length, 2);
79
+ t.is(restored.messages.length, 2);
80
+ t.is(restored.messages[0].sender, 'user');
81
+ t.is(restored.messages[1].sender, 'bot');
82
+ });
83
+ test.serial('loadConversation: returns null for missing id', (t) => {
84
+ t.is(persistenceModule.loadConversation('nope'), null);
85
+ });
86
+ test.serial('replay: mid-turn crash with tool_started inserts recovery notice', (t) => {
70
87
  const id = persistenceModule.generateId();
71
- const conversation = {
72
- id,
73
- createdAt: new Date().toISOString(),
74
- updatedAt: new Date().toISOString(),
75
- previousResponseId: null,
76
- history: [],
77
- messages: [
78
- { id: 'msg-1', sender: 'user', text: 'hello' },
79
- { id: 'msg-2', sender: 'bot', text: 'thinking...', status: 'streaming' },
80
- ],
81
- };
82
- const filePath = persistenceModule.saveConversation(conversation);
83
- const saved = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
84
- t.is(saved.messages[1].status, 'finalized');
85
- // Cleanup
86
- fs.rmSync(path.dirname(filePath), { recursive: true, force: true });
87
- });
88
- test.serial('saveConversation: normalizes pending command messages to completed', (t) => {
88
+ const writer = createConversationLogWriter({ sessionId: id, dir: testDir, logger: stubLogger });
89
+ writer.init({ id, createdAt: '2026-05-26T00:00:00.000Z' });
90
+ // First turn completes cleanly
91
+ writer.append({ type: 'user_message', message: { id: 'u1', sender: 'user', text: 'do it' } });
92
+ writer.append({
93
+ type: 'assistant_final',
94
+ message: { id: 'b1', sender: 'bot', status: 'finalized', text: 'done' },
95
+ finalText: 'done',
96
+ snapshot: {
97
+ history: [{ role: 'user', type: 'message', content: 'do it' }],
98
+ previousResponseId: 'r1',
99
+ toolLedger: [],
100
+ },
101
+ });
102
+ // Second turn: user submits, tool starts, then crash
103
+ writer.append({ type: 'user_message', message: { id: 'u2', sender: 'user', text: 'more' } });
104
+ writer.append({ type: 'tool_started', toolCallId: 'call-1', toolName: 'shell', arguments: {} });
105
+ void writer.close();
106
+ const restored = persistenceModule.loadConversation(id);
107
+ t.truthy(restored);
108
+ t.true(restored.replayWarnings.some((w) => w.includes('interrupted')));
109
+ // Recovery system message present in history
110
+ const hasRecovery = restored.history.some((item) => item?.role === 'system' && typeof item.content === 'string' && item.content.includes('Recovered'));
111
+ t.true(hasRecovery);
112
+ // The interrupted system message is on UI messages
113
+ t.true(restored.messages.some((m) => m.sender === 'system' && String(m.text).includes('interrupted')));
114
+ });
115
+ test.serial('replay: user_message only with no assistant_final flags interruption', (t) => {
89
116
  const id = persistenceModule.generateId();
90
- const conversation = {
91
- id,
92
- createdAt: new Date().toISOString(),
93
- updatedAt: new Date().toISOString(),
94
- previousResponseId: null,
95
- history: [],
96
- messages: [{ id: 'msg-1', sender: 'command', status: 'pending', command: 'ls', toolName: 'shell' }],
97
- };
98
- const filePath = persistenceModule.saveConversation(conversation);
99
- const saved = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
100
- t.is(saved.messages[0].status, 'completed');
101
- t.is(saved.messages[0].success, false);
102
- // Cleanup
103
- fs.rmSync(path.dirname(filePath), { recursive: true, force: true });
104
- });
105
- test.serial('loadConversation: returns null for non-existent id', (t) => {
106
- const result = persistenceModule.loadConversation('non-existent-id');
107
- t.is(result, null);
108
- });
109
- test.serial('loadConversation: returns saved conversation', (t) => {
117
+ const writer = createConversationLogWriter({ sessionId: id, dir: testDir, logger: stubLogger });
118
+ writer.init({ id, createdAt: '2026-05-26T00:00:00.000Z' });
119
+ writer.append({ type: 'user_message', message: { id: 'u1', sender: 'user', text: 'hi' } });
120
+ void writer.close();
121
+ const restored = persistenceModule.loadConversation(id);
122
+ t.truthy(restored);
123
+ t.true(restored.replayWarnings.length > 0);
124
+ t.true(restored.messages.some((m) => m.sender === 'system' && String(m.text).includes('interrupted')));
125
+ });
126
+ test.serial('replay: settings_changed updates restored model', (t) => {
110
127
  const id = persistenceModule.generateId();
111
- const conversation = {
112
- id,
113
- createdAt: new Date().toISOString(),
114
- updatedAt: new Date().toISOString(),
115
- model: 'gpt-4o',
116
- previousResponseId: 'resp-123',
117
- history: [{ role: 'user', content: 'hello' }],
118
- messages: [{ id: 'msg-1', sender: 'user', text: 'hello' }],
119
- };
120
- persistenceModule.saveConversation(conversation);
121
- const loaded = persistenceModule.loadConversation(id);
122
- t.truthy(loaded);
123
- t.is(loaded.id, id);
124
- t.is(loaded.model, 'gpt-4o');
125
- t.is(loaded.previousResponseId, 'resp-123');
126
- t.is(loaded.messages.length, 1);
127
- // Cleanup
128
- const filePath = path.join(persistenceModule.getConversationsDirForTest(), `${id}.json`);
129
- fs.rmSync(path.dirname(filePath), { recursive: true, force: true });
130
- });
131
- test.serial('loadConversation: preserves saved app mode settings', (t) => {
128
+ const writer = createConversationLogWriter({ sessionId: id, dir: testDir, logger: stubLogger });
129
+ writer.init({ id, createdAt: '2026-05-26T00:00:00.000Z', model: 'gpt-4o' });
130
+ writer.append({ type: 'settings_changed', key: 'agent.model', value: 'gpt-5' });
131
+ writer.append({
132
+ type: 'assistant_final',
133
+ message: { id: 'b1', sender: 'bot', status: 'finalized', text: 'ok' },
134
+ finalText: 'ok',
135
+ snapshot: { history: [], previousResponseId: null, toolLedger: [], model: 'gpt-5' },
136
+ });
137
+ void writer.close();
138
+ const restored = persistenceModule.loadConversation(id);
139
+ t.is(restored.model, 'gpt-5');
140
+ });
141
+ test.serial('replay: undo with snapshot replaces state', (t) => {
132
142
  const id = persistenceModule.generateId();
133
- const conversation = {
134
- id,
135
- createdAt: new Date().toISOString(),
136
- updatedAt: new Date().toISOString(),
137
- appMode: {
138
- liteMode: false,
139
- mentorMode: true,
140
- planMode: true,
141
- orchestratorMode: false,
143
+ const writer = createConversationLogWriter({ sessionId: id, dir: testDir, logger: stubLogger });
144
+ writer.init({ id, createdAt: '2026-05-26T00:00:00.000Z' });
145
+ writer.append({ type: 'user_message', message: { id: 'u1', sender: 'user', text: 'first' } });
146
+ writer.append({
147
+ type: 'assistant_final',
148
+ message: { id: 'b1', sender: 'bot', status: 'finalized', text: 'A' },
149
+ finalText: 'A',
150
+ snapshot: {
151
+ history: [{ role: 'user', type: 'message', content: 'first' }],
152
+ previousResponseId: 'r1',
153
+ toolLedger: [],
154
+ },
155
+ });
156
+ writer.append({ type: 'user_message', message: { id: 'u2', sender: 'user', text: 'second' } });
157
+ writer.append({
158
+ type: 'assistant_final',
159
+ message: { id: 'b2', sender: 'bot', status: 'finalized', text: 'B' },
160
+ finalText: 'B',
161
+ snapshot: {
162
+ history: [{ role: 'user', type: 'message', content: 'first' }],
163
+ previousResponseId: 'r2',
164
+ toolLedger: [],
142
165
  },
143
- previousResponseId: null,
144
- history: [],
145
- messages: [{ id: 'msg-1', sender: 'user', text: 'hello' }],
146
- };
147
- persistenceModule.saveConversation(conversation);
148
- const loaded = persistenceModule.loadConversation(id);
149
- t.deepEqual(loaded?.appMode, {
150
- liteMode: false,
151
- mentorMode: true,
152
- planMode: true,
153
- orchestratorMode: false,
154
166
  });
167
+ writer.append({ type: 'undo', removedUserTurns: 1, snapshot: emptySnapshot() });
168
+ void writer.close();
169
+ const restored = persistenceModule.loadConversation(id);
170
+ t.is(restored.history.length, 0);
171
+ t.is(restored.previousResponseId, null);
155
172
  });
156
- test.serial('loadConversation: returns null when saved project path differs from expected path', (t) => {
173
+ test.serial('replay: corrupt line is skipped', (t) => {
157
174
  const id = persistenceModule.generateId();
158
- const conversation = {
159
- id,
160
- createdAt: new Date().toISOString(),
161
- updatedAt: new Date().toISOString(),
162
- projectPath: '/workspace/alpha',
163
- previousResponseId: null,
164
- history: [],
165
- messages: [{ id: 'msg-1', sender: 'user', text: 'hello' }],
166
- };
167
- persistenceModule.saveConversation(conversation);
168
- const loaded = persistenceModule.loadConversation(id, '/workspace/beta');
169
- t.is(loaded, null);
170
- });
171
- test.serial('loadConversationForProject: reports project mismatch for existing conversation in another path', (t) => {
175
+ fs.mkdirSync(testDir, { recursive: true });
176
+ const filePath = path.join(testDir, `${id}.jsonl`);
177
+ const goodInit = JSON.stringify({
178
+ v: 1,
179
+ seq: 1,
180
+ ts: '2026-05-26T00:00:00.000Z',
181
+ event: { type: 'session_init', id, createdAt: '2026-05-26T00:00:00.000Z' },
182
+ });
183
+ const goodFinal = JSON.stringify({
184
+ v: 1,
185
+ seq: 3,
186
+ ts: '2026-05-26T00:00:01.000Z',
187
+ event: {
188
+ type: 'assistant_final',
189
+ message: { id: 'b1', sender: 'bot', status: 'finalized', text: 'ok' },
190
+ finalText: 'ok',
191
+ snapshot: { history: [], previousResponseId: 'r1', toolLedger: [] },
192
+ },
193
+ });
194
+ fs.writeFileSync(filePath, `${goodInit}\n{not json\n${goodFinal}\n`, 'utf-8');
195
+ const restored = persistenceModule.loadConversation(id);
196
+ t.is(restored.previousResponseId, 'r1');
197
+ });
198
+ test.serial('lock: collision throws LockConflictError', (t) => {
172
199
  const id = persistenceModule.generateId();
173
- const conversation = {
174
- id,
175
- createdAt: new Date().toISOString(),
176
- updatedAt: new Date().toISOString(),
177
- projectPath: '/workspace/alpha',
178
- previousResponseId: null,
179
- history: [],
180
- messages: [{ id: 'msg-1', sender: 'user', text: 'hello' }],
181
- };
182
- persistenceModule.saveConversation(conversation);
183
- const result = persistenceModule.loadConversationForProject(id, '/workspace/beta');
184
- t.is(result.status, 'project_mismatch');
185
- if (result.status !== 'project_mismatch') {
186
- t.fail('expected project_mismatch');
187
- return;
188
- }
189
- t.is(result.conversation.id, id);
200
+ const w1 = createConversationLogWriter({ sessionId: id, dir: testDir, logger: stubLogger });
201
+ w1.init({ id, createdAt: '2026-05-26T00:00:00.000Z' });
202
+ const w2 = createConversationLogWriter({ sessionId: id, dir: testDir, logger: stubLogger });
203
+ t.throws(() => w2.init({ id, createdAt: '2026-05-26T00:00:00.000Z' }), { instanceOf: LockConflictError });
204
+ void w1.close();
205
+ });
206
+ test.serial('lock: released on writer close, second writer succeeds', async (t) => {
207
+ const id = persistenceModule.generateId();
208
+ const w1 = createConversationLogWriter({ sessionId: id, dir: testDir, logger: stubLogger });
209
+ w1.init({ id, createdAt: '2026-05-26T00:00:00.000Z' });
210
+ await w1.close();
211
+ const w2 = createConversationLogWriter({ sessionId: id, dir: testDir, logger: stubLogger });
212
+ t.notThrows(() => w2.init({ id, createdAt: '2026-05-26T00:00:00.000Z' }));
213
+ await w2.close();
190
214
  });
191
- test.serial('loadConversationForProject: reports not_found for missing conversation', (t) => {
192
- const result = persistenceModule.loadConversationForProject('missing-id', '/workspace/beta');
193
- t.is(result.status, 'not_found');
215
+ test.serial('forkConversation: copies the source jsonl to a new id', (t) => {
216
+ const srcId = persistenceModule.generateId();
217
+ const dstId = persistenceModule.generateId();
218
+ const writer = createConversationLogWriter({ sessionId: srcId, dir: testDir, logger: stubLogger });
219
+ writer.init({ id: srcId, createdAt: '2026-05-26T00:00:00.000Z' });
220
+ writer.append({
221
+ type: 'assistant_final',
222
+ message: { id: 'b1', sender: 'bot', status: 'finalized', text: 'A' },
223
+ finalText: 'A',
224
+ snapshot: {
225
+ history: [{ role: 'user', type: 'message', content: 'hi' }],
226
+ previousResponseId: 'r1',
227
+ toolLedger: [],
228
+ },
229
+ });
230
+ void writer.close();
231
+ t.true(persistenceModule.forkConversation(srcId, dstId));
232
+ const restored = persistenceModule.loadConversation(dstId);
233
+ t.is(restored.previousResponseId, 'r1');
194
234
  });
195
- test.serial('loadConversation: returns saved conversation when expected project path matches', (t) => {
235
+ test.serial('listConversations: lists sessions sorted by mtime desc', (t) => {
236
+ const id1 = persistenceModule.generateId();
237
+ const w1 = createConversationLogWriter({ sessionId: id1, dir: testDir, logger: stubLogger });
238
+ w1.init({ id: id1, createdAt: '2026-05-26T00:00:00.000Z', projectPath: '/p1' });
239
+ void w1.close();
240
+ // Sleep to ensure mtime differs
241
+ const target = Date.now() + 20;
242
+ while (Date.now() < target) {
243
+ /* spin */
244
+ }
245
+ const id2 = persistenceModule.generateId();
246
+ const w2 = createConversationLogWriter({ sessionId: id2, dir: testDir, logger: stubLogger });
247
+ w2.init({ id: id2, createdAt: '2026-05-26T00:00:00.000Z', projectPath: '/p2' });
248
+ void w2.close();
249
+ const list = persistenceModule.listConversations();
250
+ t.is(list.length, 2);
251
+ t.is(list[0].id, id2);
252
+ t.is(list[0].projectPath, '/p2');
253
+ });
254
+ test.serial('loadConversation: returns null when expected project path differs', (t) => {
196
255
  const id = persistenceModule.generateId();
197
- const conversation = {
198
- id,
199
- createdAt: new Date().toISOString(),
200
- updatedAt: new Date().toISOString(),
201
- projectPath: '/workspace/alpha',
202
- previousResponseId: null,
203
- history: [],
204
- messages: [{ id: 'msg-1', sender: 'user', text: 'hello' }],
205
- };
206
- persistenceModule.saveConversation(conversation);
207
- const loaded = persistenceModule.loadConversation(id, '/workspace/alpha');
208
- t.truthy(loaded);
209
- t.is(loaded.id, id);
210
- });
211
- test.serial('loadConversation: unscoped loads keep backward-compatible behavior', (t) => {
256
+ const writer = createConversationLogWriter({ sessionId: id, dir: testDir, logger: stubLogger });
257
+ writer.init({ id, createdAt: '2026-05-26T00:00:00.000Z', projectPath: '/workspace/alpha' });
258
+ void writer.close();
259
+ t.is(persistenceModule.loadConversation(id, '/workspace/beta'), null);
260
+ });
261
+ test.serial('loadConversationForProject: reports project mismatch', (t) => {
212
262
  const id = persistenceModule.generateId();
213
- const conversation = {
214
- id,
215
- createdAt: new Date().toISOString(),
216
- updatedAt: new Date().toISOString(),
217
- projectPath: '/workspace/alpha',
218
- previousResponseId: null,
219
- history: [],
220
- messages: [{ id: 'msg-1', sender: 'user', text: 'hello' }],
221
- };
222
- persistenceModule.saveConversation(conversation);
223
- const loaded = persistenceModule.loadConversation(id);
224
- t.truthy(loaded);
225
- t.is(loaded.id, id);
226
- });
227
- test.serial('loadLastConversation: returns null when no last.json exists', (t) => {
228
- const result = persistenceModule.loadLastConversation();
229
- t.is(result, null);
230
- });
231
- test.serial('loadLastConversation: returns the last saved conversation', (t) => {
263
+ const writer = createConversationLogWriter({ sessionId: id, dir: testDir, logger: stubLogger });
264
+ writer.init({ id, createdAt: '2026-05-26T00:00:00.000Z', projectPath: '/workspace/alpha' });
265
+ void writer.close();
266
+ const result = persistenceModule.loadConversationForProject(id, '/workspace/beta');
267
+ t.is(result.status, 'project_mismatch');
268
+ });
269
+ test.serial('loadConversationForProject: not_found for missing', (t) => {
270
+ t.is(persistenceModule.loadConversationForProject('missing', '/x').status, 'not_found');
271
+ });
272
+ test.serial('loadLastConversation: returns the last written conversation', (t) => {
232
273
  const id1 = persistenceModule.generateId();
274
+ const w1 = createConversationLogWriter({ sessionId: id1, dir: testDir, logger: stubLogger });
275
+ w1.init({ id: id1, createdAt: '2026-05-26T00:00:00.000Z' });
276
+ w1.append({ type: 'user_message', message: { id: 'u1', sender: 'user', text: 'hi' } });
277
+ void w1.close();
278
+ const target = Date.now() + 20;
279
+ while (Date.now() < target) {
280
+ /* spin */
281
+ }
233
282
  const id2 = persistenceModule.generateId();
234
- const conv1 = {
235
- id: id1,
236
- createdAt: new Date().toISOString(),
237
- updatedAt: new Date().toISOString(),
238
- previousResponseId: null,
239
- history: [],
240
- messages: [{ id: 'msg-1', sender: 'user', text: 'first' }],
241
- };
242
- const conv2 = {
243
- id: id2,
244
- createdAt: new Date().toISOString(),
245
- updatedAt: new Date().toISOString(),
246
- previousResponseId: null,
247
- history: [],
248
- messages: [{ id: 'msg-2', sender: 'user', text: 'second' }],
249
- };
250
- persistenceModule.saveConversation(conv1);
251
- persistenceModule.saveConversation(conv2);
283
+ const w2 = createConversationLogWriter({ sessionId: id2, dir: testDir, logger: stubLogger });
284
+ w2.init({ id: id2, createdAt: '2026-05-26T00:00:00.000Z' });
285
+ w2.append({ type: 'user_message', message: { id: 'u2', sender: 'user', text: 'hi2' } });
286
+ void w2.close();
252
287
  const last = persistenceModule.loadLastConversation();
253
288
  t.truthy(last);
254
289
  t.is(last.id, id2);
255
- // Cleanup
256
- fs.rmSync(persistenceModule.getConversationsDirForTest(), { recursive: true, force: true });
257
- });
258
- test.serial('loadLastConversation: returns most recent conversation for expected project path', (t) => {
259
- const projectAOlder = persistenceModule.generateId();
260
- const projectANewer = persistenceModule.generateId();
261
- const projectBNewer = persistenceModule.generateId();
262
- persistenceModule.saveConversation({
263
- id: projectAOlder,
264
- createdAt: '2024-01-01T00:00:00.000Z',
265
- updatedAt: '2024-01-01T00:00:00.000Z',
266
- projectPath: '/workspace/alpha',
267
- previousResponseId: null,
268
- history: [],
269
- messages: [{ id: 'msg-1', sender: 'user', text: 'alpha older' }],
290
+ });
291
+ test.serial('loadLastConversation: does not save last.json for empty conversation', (t) => {
292
+ const id = persistenceModule.generateId();
293
+ const writer = createConversationLogWriter({ sessionId: id, dir: testDir, logger: stubLogger });
294
+ writer.init({ id, createdAt: '2026-05-26T00:00:00.000Z' });
295
+ void writer.close();
296
+ const last = persistenceModule.loadLastConversation();
297
+ t.is(last, null);
298
+ t.false(fs.existsSync(path.join(testDir, 'last.json')));
299
+ });
300
+ test.serial('hasConversationContent: returns false for missing conversation', (t) => {
301
+ t.false(persistenceModule.hasConversationContent('non-existent-id'));
302
+ });
303
+ test.serial('hasConversationContent: returns false for empty conversation', (t) => {
304
+ const id = persistenceModule.generateId();
305
+ const writer = createConversationLogWriter({ sessionId: id, dir: testDir, logger: stubLogger });
306
+ writer.init({ id, createdAt: '2026-05-26T00:00:00.000Z' });
307
+ void writer.close();
308
+ t.false(persistenceModule.hasConversationContent(id));
309
+ });
310
+ test.serial('hasConversationContent: returns true for user_message', (t) => {
311
+ const id = persistenceModule.generateId();
312
+ const writer = createConversationLogWriter({ sessionId: id, dir: testDir, logger: stubLogger });
313
+ writer.init({ id, createdAt: '2026-05-26T00:00:00.000Z' });
314
+ writer.append({ type: 'user_message', message: { id: 'u1', sender: 'user', text: 'hi' } });
315
+ void writer.close();
316
+ t.true(persistenceModule.hasConversationContent(id));
317
+ });
318
+ test.serial('hasConversationContent: returns true for assistant_final', (t) => {
319
+ const id = persistenceModule.generateId();
320
+ const writer = createConversationLogWriter({ sessionId: id, dir: testDir, logger: stubLogger });
321
+ writer.init({ id, createdAt: '2026-05-26T00:00:00.000Z' });
322
+ writer.append({
323
+ type: 'assistant_final',
324
+ message: { id: 'a1', sender: 'bot', status: 'finalized', text: 'hello' },
325
+ finalText: 'hello',
326
+ snapshot: emptySnapshot(),
270
327
  });
271
- waitForDistinctTimestamp();
272
- persistenceModule.saveConversation({
273
- id: projectANewer,
274
- createdAt: '2024-01-02T00:00:00.000Z',
275
- updatedAt: '2024-01-02T00:00:00.000Z',
276
- projectPath: '/workspace/alpha',
277
- previousResponseId: null,
278
- history: [],
279
- messages: [{ id: 'msg-2', sender: 'user', text: 'alpha newer' }],
328
+ void writer.close();
329
+ t.true(persistenceModule.hasConversationContent(id));
330
+ });
331
+ test.serial('hasConversationContent: skips corrupt lines and finds content', (t) => {
332
+ const id = persistenceModule.generateId();
333
+ const filePath = path.join(testDir, `${id}.jsonl`);
334
+ fs.writeFileSync(filePath, '{"v":1,"seq":1,"ts":"2026-05-26T00:00:00.000Z","event":{"type":"session_init","id":"' +
335
+ id +
336
+ '","createdAt":"2026-05-26T00:00:00.000Z"}}\n' +
337
+ 'this is not json\n' +
338
+ '{"v":1,"seq":2,"ts":"2026-05-26T00:00:01.000Z","event":{"type":"user_message","message":{"id":"u1","sender":"user","text":"hi"}}}\n', 'utf-8');
339
+ t.true(persistenceModule.hasConversationContent(id));
340
+ });
341
+ test.serial('deleteConversation: removes the jsonl and clears last.json', (t) => {
342
+ const id = persistenceModule.generateId();
343
+ const writer = createConversationLogWriter({ sessionId: id, dir: testDir, logger: stubLogger });
344
+ writer.init({ id, createdAt: '2026-05-26T00:00:00.000Z' });
345
+ writer.append({ type: 'user_message', message: { id: 'u1', sender: 'user', text: 'hi' } });
346
+ void writer.close();
347
+ t.true(persistenceModule.deleteConversation(id));
348
+ t.false(fs.existsSync(path.join(testDir, `${id}.jsonl`)));
349
+ t.false(fs.existsSync(path.join(testDir, 'last.json')));
350
+ });
351
+ test.serial('subagent_completed and corresponding records omit nestedRunResult', (t) => {
352
+ const id = persistenceModule.generateId();
353
+ const writer = createConversationLogWriter({ sessionId: id, dir: testDir, logger: stubLogger });
354
+ writer.init({ id, createdAt: '2026-05-26T00:00:00.000Z' });
355
+ // 1. Log subagent_completed event
356
+ writer.append({
357
+ type: 'subagent_completed',
358
+ result: {
359
+ agentId: 'sub-agent-1',
360
+ role: 'worker',
361
+ status: 'completed',
362
+ finalText: 'Task resolved successfully',
363
+ filesChanged: ['src/app.ts'],
364
+ toolsUsed: [{ toolName: 'create_file', count: 1 }],
365
+ usage: { prompt_tokens: 120, completion_tokens: 80, total_tokens: 200 },
366
+ nestedRunResult: {
367
+ state: {
368
+ history: [{ role: 'user', content: 'test' }],
369
+ generatedItems: ['item1'],
370
+ },
371
+ },
372
+ },
280
373
  });
281
- waitForDistinctTimestamp();
282
- persistenceModule.saveConversation({
283
- id: projectBNewer,
284
- createdAt: '2024-01-03T00:00:00.000Z',
285
- updatedAt: '2024-01-03T00:00:00.000Z',
286
- projectPath: '/workspace/beta',
287
- previousResponseId: null,
288
- history: [],
289
- messages: [{ id: 'msg-3', sender: 'user', text: 'beta newer' }],
374
+ // 2. Log tool_result event with JSON string containing nestedRunResult
375
+ writer.append({
376
+ type: 'tool_result',
377
+ callId: 'call-subagent-1',
378
+ toolName: 'run_subagent',
379
+ status: 'completed',
380
+ output: JSON.stringify({
381
+ status: 'completed',
382
+ finalText: 'Result text',
383
+ nestedRunResult: { state: { internalStuff: 'hidden' } },
384
+ }),
290
385
  });
291
- const last = persistenceModule.loadLastConversation('/workspace/alpha');
292
- t.truthy(last);
293
- t.is(last.id, projectANewer);
294
- });
295
- test.serial('loadLastConversation: returns null when no conversation matches expected project path', (t) => {
296
- persistenceModule.saveConversation({
297
- id: persistenceModule.generateId(),
298
- createdAt: new Date().toISOString(),
299
- updatedAt: new Date().toISOString(),
300
- projectPath: '/workspace/alpha',
301
- previousResponseId: null,
302
- history: [],
303
- messages: [{ id: 'msg-1', sender: 'user', text: 'alpha' }],
386
+ // 3. Log assistant_final event with nestedRunResult in snapshot
387
+ writer.append({
388
+ type: 'assistant_final',
389
+ message: { id: 'b1', sender: 'bot', status: 'finalized', text: 'all done' },
390
+ finalText: 'all done',
391
+ snapshot: {
392
+ history: [
393
+ {
394
+ role: 'tool',
395
+ type: 'function_call_result',
396
+ name: 'run_subagent',
397
+ output: JSON.stringify({
398
+ status: 'completed',
399
+ finalText: 'Snapshot result text',
400
+ nestedRunResult: { state: { internalStuff: 'hidden' } },
401
+ }),
402
+ },
403
+ ],
404
+ previousResponseId: 'resp-1',
405
+ toolLedger: [
406
+ {
407
+ turnId: 'turn-1',
408
+ callId: 'call-subagent-1',
409
+ toolName: 'run_subagent',
410
+ status: 'completed',
411
+ startedAt: '2026-05-26T00:00:00.000Z',
412
+ output: {
413
+ status: 'completed',
414
+ nestedRunResult: { state: { internalStuff: 'hidden' } },
415
+ },
416
+ },
417
+ ],
418
+ },
304
419
  });
305
- const last = persistenceModule.loadLastConversation('/workspace/beta');
306
- t.is(last, null);
420
+ void writer.close();
421
+ // Load raw file contents to check what was written to disk
422
+ const filePath = path.join(testDir, `${id}.jsonl`);
423
+ const lines = fs.readFileSync(filePath, 'utf-8').trim().split('\n');
424
+ // Parse lines to check events
425
+ const envelopes = lines.map((line) => JSON.parse(line));
426
+ // Find subagent_completed
427
+ const completedEnv = envelopes.find((env) => env.event?.type === 'subagent_completed');
428
+ t.truthy(completedEnv);
429
+ t.is(completedEnv.event.result.status, 'completed');
430
+ t.is(completedEnv.event.result.finalText, 'Task resolved successfully');
431
+ t.deepEqual(completedEnv.event.result.filesChanged, ['src/app.ts']);
432
+ t.is(completedEnv.event.result.nestedRunResult, undefined);
433
+ // Find tool_result
434
+ const toolResultEnv = envelopes.find((env) => env.event?.type === 'tool_result');
435
+ t.truthy(toolResultEnv);
436
+ t.is(toolResultEnv.event.toolName, 'run_subagent');
437
+ const parsedOutput = JSON.parse(toolResultEnv.event.output);
438
+ t.is(parsedOutput.status, 'completed');
439
+ t.is(parsedOutput.finalText, 'Result text');
440
+ t.is(parsedOutput.nestedRunResult, undefined);
441
+ // Find assistant_final and check snapshot
442
+ const finalEnv = envelopes.find((env) => env.event?.type === 'assistant_final');
443
+ t.truthy(finalEnv);
444
+ const snapHistoryOutput = JSON.parse(finalEnv.event.snapshot.history[0].output);
445
+ t.is(snapHistoryOutput.status, 'completed');
446
+ t.is(snapHistoryOutput.finalText, 'Snapshot result text');
447
+ t.is(snapHistoryOutput.nestedRunResult, undefined);
448
+ t.is(finalEnv.event.snapshot.toolLedger[0].output.status, 'completed');
449
+ t.is(finalEnv.event.snapshot.toolLedger[0].output.nestedRunResult, undefined);
307
450
  });
308
- test.serial('deleteConversation: removes the conversation file', (t) => {
309
- const id = persistenceModule.generateId();
310
- const conversation = {
311
- id,
312
- createdAt: new Date().toISOString(),
313
- updatedAt: new Date().toISOString(),
314
- previousResponseId: null,
315
- history: [],
316
- messages: [],
317
- };
318
- persistenceModule.saveConversation(conversation);
319
- const result = persistenceModule.deleteConversation(id);
320
- t.true(result);
321
- t.is(persistenceModule.loadConversation(id), null);
322
- // Cleanup
323
- fs.rmSync(persistenceModule.getConversationsDirForTest(), { recursive: true, force: true });
324
- });
325
- test.serial('deleteConversation: returns false for non-existent id', (t) => {
326
- const result = persistenceModule.deleteConversation('non-existent');
327
- t.false(result);
328
- });
329
- test.serial('listConversations: returns empty array when no conversations exist', (t) => {
330
- // Ensure dir exists but is empty
331
- const dir = persistenceModule.getConversationsDirForTest();
332
- if (fs.existsSync(dir)) {
333
- fs.rmSync(dir, { recursive: true, force: true });
334
- }
335
- const list = persistenceModule.listConversations();
336
- t.deepEqual(list, []);
451
+ test.serial('saveLastConversation: stores per-project last conversation', (t) => {
452
+ const id1 = persistenceModule.generateId();
453
+ const w1 = createConversationLogWriter({ sessionId: id1, dir: testDir, logger: stubLogger });
454
+ w1.init({ id: id1, createdAt: '2026-05-26T00:00:00.000Z', projectPath: '/project-a' });
455
+ w1.append({ type: 'user_message', message: { id: 'u1', sender: 'user', text: 'hi a' } });
456
+ void w1.close();
457
+ const id2 = persistenceModule.generateId();
458
+ const w2 = createConversationLogWriter({ sessionId: id2, dir: testDir, logger: stubLogger });
459
+ w2.init({ id: id2, createdAt: '2026-05-26T00:00:00.000Z', projectPath: '/project-b' });
460
+ w2.append({ type: 'user_message', message: { id: 'u2', sender: 'user', text: 'hi b' } });
461
+ void w2.close();
462
+ const lastA = persistenceModule.loadLastConversation('/project-a');
463
+ t.truthy(lastA);
464
+ t.is(lastA.id, id1);
465
+ const lastB = persistenceModule.loadLastConversation('/project-b');
466
+ t.truthy(lastB);
467
+ t.is(lastB.id, id2);
337
468
  });
338
- test.serial('loadConversation: old save without orchestratorMode — resolves to false, no error', (t) => {
469
+ test.serial('saveLastConversation: stores per-ssh-host last conversation', (t) => {
470
+ const id1 = persistenceModule.generateId();
471
+ const w1 = createConversationLogWriter({ sessionId: id1, dir: testDir, logger: stubLogger });
472
+ w1.init({ id: id1, createdAt: '2026-05-26T00:00:00.000Z', projectPath: '/remote', sshHost: 'host-a' });
473
+ w1.append({ type: 'user_message', message: { id: 'u1', sender: 'user', text: 'hi a' } });
474
+ void w1.close();
475
+ const id2 = persistenceModule.generateId();
476
+ const w2 = createConversationLogWriter({ sessionId: id2, dir: testDir, logger: stubLogger });
477
+ w2.init({ id: id2, createdAt: '2026-05-26T00:00:00.000Z', projectPath: '/remote', sshHost: 'host-b' });
478
+ w2.append({ type: 'user_message', message: { id: 'u2', sender: 'user', text: 'hi b' } });
479
+ void w2.close();
480
+ const lastA = persistenceModule.loadLastConversation('/remote', 'host-a');
481
+ t.truthy(lastA);
482
+ t.is(lastA.id, id1);
483
+ const lastB = persistenceModule.loadLastConversation('/remote', 'host-b');
484
+ t.truthy(lastB);
485
+ t.is(lastB.id, id2);
486
+ });
487
+ test.serial('loadLastConversation: falls back to scanning when no last.json entry matches', (t) => {
339
488
  const id = persistenceModule.generateId();
340
- // Write a raw JSON file that looks like an old save (missing orchestratorMode)
341
- const dir = persistenceModule.getConversationsDirForTest();
342
- if (!fs.existsSync(dir)) {
343
- fs.mkdirSync(dir, { recursive: true });
344
- }
345
- const rawData = {
346
- id,
347
- createdAt: new Date().toISOString(),
348
- updatedAt: new Date().toISOString(),
349
- appMode: {
350
- mentorMode: false,
351
- liteMode: false,
352
- planMode: false,
353
- // orchestratorMode is intentionally absent (old format)
354
- },
355
- previousResponseId: null,
356
- history: [],
357
- messages: [],
358
- };
359
- fs.writeFileSync(path.join(dir, `${id}.json`), JSON.stringify(rawData, null, 2), 'utf-8');
360
- // loadConversation should not throw — it returns null on error
361
- const loaded = persistenceModule.loadConversation(id);
362
- t.truthy(loaded);
363
- // orchestratorMode should be absent or false-y, not throw
364
- const orchestratorMode = loaded?.appMode?.orchestratorMode;
365
- t.falsy(orchestratorMode, 'orchestratorMode missing from old save should not be truthy');
366
- });
367
- test.serial('loadConversation: save with orchestratorMode: true is correctly restored', (t) => {
489
+ const w = createConversationLogWriter({ sessionId: id, dir: testDir, logger: stubLogger });
490
+ w.init({ id, createdAt: '2026-05-26T00:00:00.000Z', projectPath: '/fallback' });
491
+ w.append({ type: 'user_message', message: { id: 'u1', sender: 'user', text: 'hi' } });
492
+ void w.close();
493
+ // Directly overwrite last.json so the entry has no projectPath
494
+ fs.writeFileSync(path.join(testDir, 'last.json'), JSON.stringify({ entries: [{ id, updatedAt: new Date().toISOString() }] }), 'utf-8');
495
+ const last = persistenceModule.loadLastConversation('/fallback');
496
+ t.truthy(last);
497
+ t.is(last.id, id);
498
+ });
499
+ test.serial('loadLastConversation: migrates old last.json format', (t) => {
368
500
  const id = persistenceModule.generateId();
369
- const conversation = {
370
- id,
371
- createdAt: new Date().toISOString(),
372
- updatedAt: new Date().toISOString(),
373
- appMode: {
374
- mentorMode: false,
375
- liteMode: false,
376
- planMode: false,
377
- orchestratorMode: true,
378
- },
379
- previousResponseId: null,
380
- history: [],
381
- messages: [],
382
- };
383
- persistenceModule.saveConversation(conversation);
384
- const loaded = persistenceModule.loadConversation(id);
385
- t.truthy(loaded);
386
- t.is(loaded?.appMode?.orchestratorMode, true);
387
- });
388
- test.serial('listConversations: returns conversations sorted by updatedAt descending', (t) => {
501
+ const w = createConversationLogWriter({ sessionId: id, dir: testDir, logger: stubLogger });
502
+ w.init({ id, createdAt: '2026-05-26T00:00:00.000Z' });
503
+ w.append({ type: 'user_message', message: { id: 'u1', sender: 'user', text: 'hi' } });
504
+ void w.close();
505
+ // Simulate old format
506
+ fs.writeFileSync(path.join(testDir, 'last.json'), JSON.stringify({ id, updatedAt: '2026-05-26T00:00:00.000Z' }), 'utf-8');
507
+ const last = persistenceModule.loadLastConversation();
508
+ t.truthy(last);
509
+ t.is(last.id, id);
510
+ });
511
+ test.serial('deleteConversation: removes only matching entry from last.json', (t) => {
389
512
  const id1 = persistenceModule.generateId();
513
+ const w1 = createConversationLogWriter({ sessionId: id1, dir: testDir, logger: stubLogger });
514
+ w1.init({ id: id1, createdAt: '2026-05-26T00:00:00.000Z', projectPath: '/project-a' });
515
+ w1.append({ type: 'user_message', message: { id: 'u1', sender: 'user', text: 'hi a' } });
516
+ void w1.close();
390
517
  const id2 = persistenceModule.generateId();
391
- const conv1 = {
392
- id: id1,
393
- createdAt: '2024-01-01T00:00:00.000Z',
394
- updatedAt: '2024-01-01T00:00:00.000Z',
395
- previousResponseId: null,
396
- history: [],
397
- messages: [],
398
- };
399
- const conv2 = {
400
- id: id2,
401
- createdAt: '2024-01-02T00:00:00.000Z',
402
- updatedAt: '2024-01-02T00:00:00.000Z',
403
- previousResponseId: null,
404
- history: [],
405
- messages: [],
406
- };
407
- // Save conv1 first
408
- persistenceModule.saveConversation(conv1);
409
- // Wait a moment so updatedAt differs
410
- waitForDistinctTimestamp();
411
- // Save conv2 second (should have later updatedAt)
412
- persistenceModule.saveConversation(conv2);
413
- const list = persistenceModule.listConversations();
414
- t.is(list.length, 2);
415
- t.is(list[0].id, id2); // Most recently saved first
416
- t.is(list[1].id, id1);
518
+ const w2 = createConversationLogWriter({ sessionId: id2, dir: testDir, logger: stubLogger });
519
+ w2.init({ id: id2, createdAt: '2026-05-26T00:00:00.000Z', projectPath: '/project-b' });
520
+ w2.append({ type: 'user_message', message: { id: 'u2', sender: 'user', text: 'hi b' } });
521
+ void w2.close();
522
+ t.true(persistenceModule.deleteConversation(id1));
523
+ t.false(fs.existsSync(path.join(testDir, `${id1}.jsonl`)));
524
+ t.true(fs.existsSync(path.join(testDir, 'last.json')));
525
+ const lastB = persistenceModule.loadLastConversation('/project-b');
526
+ t.truthy(lastB);
527
+ t.is(lastB.id, id2);
528
+ });
529
+ test.serial('saveLastConversation: updates entry when projectPath changes for same id', (t) => {
530
+ const id = persistenceModule.generateId();
531
+ const w = createConversationLogWriter({ sessionId: id, dir: testDir, logger: stubLogger });
532
+ w.init({ id, createdAt: '2026-05-26T00:00:00.000Z', projectPath: '/old-path' });
533
+ w.append({ type: 'user_message', message: { id: 'u1', sender: 'user', text: 'hi' } });
534
+ void w.close();
535
+ // Re-open same session with different project path (simulating rotate or manual update)
536
+ const w2 = createConversationLogWriter({ sessionId: id, dir: testDir, logger: stubLogger });
537
+ w2.init({ id, createdAt: '2026-05-26T00:00:00.000Z', projectPath: '/new-path' });
538
+ w2.append({ type: 'user_message', message: { id: 'u2', sender: 'user', text: 'hi again' } });
539
+ void w2.close();
540
+ t.is(persistenceModule.loadLastConversation('/old-path'), null);
541
+ const lastNew = persistenceModule.loadLastConversation('/new-path');
542
+ t.truthy(lastNew);
543
+ t.is(lastNew.id, id);
417
544
  });
545
+ // Suppress unused-event-import lint
546
+ const _ev = null;
547
+ void _ev;
418
548
  //# sourceMappingURL=conversation-persistence.test.js.map