dominds 0.1.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 (273) hide show
  1. package/LICENSE +157 -0
  2. package/README.md +250 -0
  3. package/README.zh.md +161 -0
  4. package/dist/access-control.js +253 -0
  5. package/dist/cli/create.js +263 -0
  6. package/dist/cli/read.js +84 -0
  7. package/dist/cli/tui.js +199 -0
  8. package/dist/cli/webui.js +169 -0
  9. package/dist/cli.js +227 -0
  10. package/dist/dialog-factory.js +53 -0
  11. package/dist/dialog-global-registry.js +68 -0
  12. package/dist/dialog-instance-registry.js +78 -0
  13. package/dist/dialog-run-state.js +198 -0
  14. package/dist/dialog.js +1024 -0
  15. package/dist/evt-registry.js +103 -0
  16. package/dist/index.js +8 -0
  17. package/dist/llm/client.js +69 -0
  18. package/dist/llm/defaults.yaml +386 -0
  19. package/dist/llm/driver.js +3214 -0
  20. package/dist/llm/gen/anthropic.js +611 -0
  21. package/dist/llm/gen/codex.js +375 -0
  22. package/dist/llm/gen/mock.js +326 -0
  23. package/dist/llm/gen/openai.js +470 -0
  24. package/dist/llm/gen/registry.js +26 -0
  25. package/dist/llm/gen.js +2 -0
  26. package/dist/llm/tools-projection.js +37 -0
  27. package/dist/log.js +228 -0
  28. package/dist/mcp/config.js +230 -0
  29. package/dist/mcp/sdk-client.js +129 -0
  30. package/dist/mcp/server-runtime.js +57 -0
  31. package/dist/mcp/stdio-client.js +280 -0
  32. package/dist/mcp/supervisor.js +979 -0
  33. package/dist/mcp/tool-names.js +109 -0
  34. package/dist/minds/builtin/cmdr/persona.md +3 -0
  35. package/dist/minds/builtin/dijiang/knowledge.md +287 -0
  36. package/dist/minds/builtin/dijiang/persona.md +7 -0
  37. package/dist/minds/builtin/fuxi/persona.en.md +59 -0
  38. package/dist/minds/builtin/fuxi/persona.zh.md +49 -0
  39. package/dist/minds/builtin/pangu/persona.en.md +78 -0
  40. package/dist/minds/builtin/pangu/persona.zh.md +71 -0
  41. package/dist/minds/load.js +617 -0
  42. package/dist/minds/minds-i18n.js +131 -0
  43. package/dist/minds/system-prompt.js +281 -0
  44. package/dist/persistence.js +3128 -0
  45. package/dist/problems.js +109 -0
  46. package/dist/server/api-routes.js +1031 -0
  47. package/dist/server/auth.js +180 -0
  48. package/dist/server/mime-types.js +32 -0
  49. package/dist/server/prompts-routes.js +543 -0
  50. package/dist/server/server-core.js +235 -0
  51. package/dist/server/setup-routes.js +697 -0
  52. package/dist/server/static-server.js +132 -0
  53. package/dist/server/websocket-handler.js +1011 -0
  54. package/dist/server.js +164 -0
  55. package/dist/shared/async-fifo-mutex.js +36 -0
  56. package/dist/shared/diligence.js +20 -0
  57. package/dist/shared/dotenv.js +144 -0
  58. package/dist/shared/evt.js +195 -0
  59. package/dist/shared/i18n/driver-messages.js +267 -0
  60. package/dist/shared/i18n/text.js +9 -0
  61. package/dist/shared/i18n/tool-result-messages.js +51 -0
  62. package/dist/shared/rtws-cli.js +73 -0
  63. package/dist/shared/runtime-language.js +47 -0
  64. package/dist/shared/team-mgmt-manual.js +116 -0
  65. package/dist/shared/types/context-health.js +2 -0
  66. package/dist/shared/types/dialog.js +11 -0
  67. package/dist/shared/types/i18n.js +2 -0
  68. package/dist/shared/types/index.js +26 -0
  69. package/dist/shared/types/language.js +40 -0
  70. package/dist/shared/types/problems.js +2 -0
  71. package/dist/shared/types/prompts.js +2 -0
  72. package/dist/shared/types/q4h.js +7 -0
  73. package/dist/shared/types/run-state.js +8 -0
  74. package/dist/shared/types/setup.js +2 -0
  75. package/dist/shared/types/storage.js +10 -0
  76. package/dist/shared/types/tellask.js +8 -0
  77. package/dist/shared/types/tools-registry.js +2 -0
  78. package/dist/shared/types/wire.js +12 -0
  79. package/dist/shared/utils/fmt.js +9 -0
  80. package/dist/shared/utils/html.js +20 -0
  81. package/dist/shared/utils/id.js +18 -0
  82. package/dist/shared/utils/inter-dialog-format.js +101 -0
  83. package/dist/shared/utils/time.js +13 -0
  84. package/dist/static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  85. package/dist/static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  86. package/dist/static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  87. package/dist/static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  88. package/dist/static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  89. package/dist/static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  90. package/dist/static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  91. package/dist/static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  92. package/dist/static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  93. package/dist/static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  94. package/dist/static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  95. package/dist/static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  96. package/dist/static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  97. package/dist/static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  98. package/dist/static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  99. package/dist/static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  100. package/dist/static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  101. package/dist/static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  102. package/dist/static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  103. package/dist/static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  104. package/dist/static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  105. package/dist/static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  106. package/dist/static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  107. package/dist/static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  108. package/dist/static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  109. package/dist/static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  110. package/dist/static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  111. package/dist/static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  112. package/dist/static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  113. package/dist/static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  114. package/dist/static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  115. package/dist/static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  116. package/dist/static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  117. package/dist/static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  118. package/dist/static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  119. package/dist/static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  120. package/dist/static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  121. package/dist/static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  122. package/dist/static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  123. package/dist/static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  124. package/dist/static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  125. package/dist/static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  126. package/dist/static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  127. package/dist/static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  128. package/dist/static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  129. package/dist/static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  130. package/dist/static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  131. package/dist/static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  132. package/dist/static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  133. package/dist/static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  134. package/dist/static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  135. package/dist/static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  136. package/dist/static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  137. package/dist/static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  138. package/dist/static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  139. package/dist/static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  140. package/dist/static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  141. package/dist/static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  142. package/dist/static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  143. package/dist/static/assets/_baseUniq-Crfl3d5Y.js +661 -0
  144. package/dist/static/assets/_baseUniq-Crfl3d5Y.js.map +1 -0
  145. package/dist/static/assets/arc-CbA_x9GD.js +132 -0
  146. package/dist/static/assets/arc-CbA_x9GD.js.map +1 -0
  147. package/dist/static/assets/architectureDiagram-VXUJARFQ-lcFS8ZQJ.js +8685 -0
  148. package/dist/static/assets/architectureDiagram-VXUJARFQ-lcFS8ZQJ.js.map +1 -0
  149. package/dist/static/assets/blockDiagram-VD42YOAC-B3Q36qRc.js +3608 -0
  150. package/dist/static/assets/blockDiagram-VD42YOAC-B3Q36qRc.js.map +1 -0
  151. package/dist/static/assets/c4Diagram-YG6GDRKO-Mt-aq3VH.js +2482 -0
  152. package/dist/static/assets/c4Diagram-YG6GDRKO-Mt-aq3VH.js.map +1 -0
  153. package/dist/static/assets/channel-BVr1Yke-.js +8 -0
  154. package/dist/static/assets/channel-BVr1Yke-.js.map +1 -0
  155. package/dist/static/assets/chunk-4BX2VUAB-qCIn5Iic.js +17 -0
  156. package/dist/static/assets/chunk-4BX2VUAB-qCIn5Iic.js.map +1 -0
  157. package/dist/static/assets/chunk-55IACEB6-q172NeCV.js +14 -0
  158. package/dist/static/assets/chunk-55IACEB6-q172NeCV.js.map +1 -0
  159. package/dist/static/assets/chunk-B4BG7PRW-CMJmtYzq.js +1827 -0
  160. package/dist/static/assets/chunk-B4BG7PRW-CMJmtYzq.js.map +1 -0
  161. package/dist/static/assets/chunk-DI55MBZ5-DiuwwZPL.js +1916 -0
  162. package/dist/static/assets/chunk-DI55MBZ5-DiuwwZPL.js.map +1 -0
  163. package/dist/static/assets/chunk-FMBD7UC4-06sqZTTn.js +20 -0
  164. package/dist/static/assets/chunk-FMBD7UC4-06sqZTTn.js.map +1 -0
  165. package/dist/static/assets/chunk-QN33PNHL-CnpBNkpP.js +25 -0
  166. package/dist/static/assets/chunk-QN33PNHL-CnpBNkpP.js.map +1 -0
  167. package/dist/static/assets/chunk-QZHKN3VN-CNgjMR-e.js +18 -0
  168. package/dist/static/assets/chunk-QZHKN3VN-CNgjMR-e.js.map +1 -0
  169. package/dist/static/assets/chunk-TZMSLE5B-BxtzW6--.js +109 -0
  170. package/dist/static/assets/chunk-TZMSLE5B-BxtzW6--.js.map +1 -0
  171. package/dist/static/assets/classDiagram-2ON5EDUG-29huvmn-.js +23 -0
  172. package/dist/static/assets/classDiagram-2ON5EDUG-29huvmn-.js.map +1 -0
  173. package/dist/static/assets/classDiagram-v2-WZHVMYZB-29huvmn-.js +23 -0
  174. package/dist/static/assets/classDiagram-v2-WZHVMYZB-29huvmn-.js.map +1 -0
  175. package/dist/static/assets/clone-D2OgLSSn.js +9 -0
  176. package/dist/static/assets/clone-D2OgLSSn.js.map +1 -0
  177. package/dist/static/assets/cose-bilkent-S5V4N54A-BNegDCxl.js +4943 -0
  178. package/dist/static/assets/cose-bilkent-S5V4N54A-BNegDCxl.js.map +1 -0
  179. package/dist/static/assets/cytoscape.esm-Bm8DJGmZ.js +30240 -0
  180. package/dist/static/assets/cytoscape.esm-Bm8DJGmZ.js.map +1 -0
  181. package/dist/static/assets/dagre-6UL2VRFP-f1XrTRSn.js +695 -0
  182. package/dist/static/assets/dagre-6UL2VRFP-f1XrTRSn.js.map +1 -0
  183. package/dist/static/assets/defaultLocale-DVr69WTU.js +207 -0
  184. package/dist/static/assets/defaultLocale-DVr69WTU.js.map +1 -0
  185. package/dist/static/assets/diagram-PSM6KHXK-8w1WbeDi.js +849 -0
  186. package/dist/static/assets/diagram-PSM6KHXK-8w1WbeDi.js.map +1 -0
  187. package/dist/static/assets/diagram-QEK2KX5R-CF4wtMmR.js +303 -0
  188. package/dist/static/assets/diagram-QEK2KX5R-CF4wtMmR.js.map +1 -0
  189. package/dist/static/assets/diagram-S2PKOQOG-8p3Avgn2.js +213 -0
  190. package/dist/static/assets/diagram-S2PKOQOG-8p3Avgn2.js.map +1 -0
  191. package/dist/static/assets/erDiagram-Q2GNP2WA-BMKLxlM9.js +1159 -0
  192. package/dist/static/assets/erDiagram-Q2GNP2WA-BMKLxlM9.js.map +1 -0
  193. package/dist/static/assets/favicon-Cmg5RbCj.svg +8 -0
  194. package/dist/static/assets/flowDiagram-NV44I4VS-CgEuPNK2.js +2332 -0
  195. package/dist/static/assets/flowDiagram-NV44I4VS-CgEuPNK2.js.map +1 -0
  196. package/dist/static/assets/ganttDiagram-JELNMOA3-bJkDCf-9.js +3681 -0
  197. package/dist/static/assets/ganttDiagram-JELNMOA3-bJkDCf-9.js.map +1 -0
  198. package/dist/static/assets/gitGraphDiagram-NY62KEGX-4QE9kesp.js +1206 -0
  199. package/dist/static/assets/gitGraphDiagram-NY62KEGX-4QE9kesp.js.map +1 -0
  200. package/dist/static/assets/graph-CS0Pmm7c.js +597 -0
  201. package/dist/static/assets/graph-CS0Pmm7c.js.map +1 -0
  202. package/dist/static/assets/index-BS6HnGzC.js +112303 -0
  203. package/dist/static/assets/index-BS6HnGzC.js.map +1 -0
  204. package/dist/static/assets/index-DaIsSzC_.css +483 -0
  205. package/dist/static/assets/infoDiagram-WHAUD3N6-ypBcKfUs.js +34 -0
  206. package/dist/static/assets/infoDiagram-WHAUD3N6-ypBcKfUs.js.map +1 -0
  207. package/dist/static/assets/init-ZxktEp_H.js +17 -0
  208. package/dist/static/assets/init-ZxktEp_H.js.map +1 -0
  209. package/dist/static/assets/journeyDiagram-XKPGCS4Q-QnrxDowJ.js +1255 -0
  210. package/dist/static/assets/journeyDiagram-XKPGCS4Q-QnrxDowJ.js.map +1 -0
  211. package/dist/static/assets/kanban-definition-3W4ZIXB7-CfvEc4z5.js +1048 -0
  212. package/dist/static/assets/kanban-definition-3W4ZIXB7-CfvEc4z5.js.map +1 -0
  213. package/dist/static/assets/layout-8TGxpm23.js +2218 -0
  214. package/dist/static/assets/layout-8TGxpm23.js.map +1 -0
  215. package/dist/static/assets/linear-BATBPQQv.js +341 -0
  216. package/dist/static/assets/linear-BATBPQQv.js.map +1 -0
  217. package/dist/static/assets/min-B3oVH3AC.js +42 -0
  218. package/dist/static/assets/min-B3oVH3AC.js.map +1 -0
  219. package/dist/static/assets/mindmap-definition-VGOIOE7T-L7VLwwF8.js +1127 -0
  220. package/dist/static/assets/mindmap-definition-VGOIOE7T-L7VLwwF8.js.map +1 -0
  221. package/dist/static/assets/ordinal-CxptdPJm.js +77 -0
  222. package/dist/static/assets/ordinal-CxptdPJm.js.map +1 -0
  223. package/dist/static/assets/pieDiagram-ADFJNKIX-CFW3zIhM.js +241 -0
  224. package/dist/static/assets/pieDiagram-ADFJNKIX-CFW3zIhM.js.map +1 -0
  225. package/dist/static/assets/quadrantDiagram-AYHSOK5B-B7ssen3E.js +1338 -0
  226. package/dist/static/assets/quadrantDiagram-AYHSOK5B-B7ssen3E.js.map +1 -0
  227. package/dist/static/assets/requirementDiagram-UZGBJVZJ-D0v5BArv.js +1162 -0
  228. package/dist/static/assets/requirementDiagram-UZGBJVZJ-D0v5BArv.js.map +1 -0
  229. package/dist/static/assets/sankeyDiagram-TZEHDZUN-B7slncJe.js +1195 -0
  230. package/dist/static/assets/sankeyDiagram-TZEHDZUN-B7slncJe.js.map +1 -0
  231. package/dist/static/assets/sequenceDiagram-WL72ISMW-oXU2lRh_.js +3875 -0
  232. package/dist/static/assets/sequenceDiagram-WL72ISMW-oXU2lRh_.js.map +1 -0
  233. package/dist/static/assets/stateDiagram-FKZM4ZOC-CFYsEd0x.js +452 -0
  234. package/dist/static/assets/stateDiagram-FKZM4ZOC-CFYsEd0x.js.map +1 -0
  235. package/dist/static/assets/stateDiagram-v2-4FDKWEC3-C0UWaNA7.js +22 -0
  236. package/dist/static/assets/stateDiagram-v2-4FDKWEC3-C0UWaNA7.js.map +1 -0
  237. package/dist/static/assets/timeline-definition-IT6M3QCI-C3KODUrh.js +1223 -0
  238. package/dist/static/assets/timeline-definition-IT6M3QCI-C3KODUrh.js.map +1 -0
  239. package/dist/static/assets/treemap-KMMF4GRG-DAGDLhj2.js +18753 -0
  240. package/dist/static/assets/treemap-KMMF4GRG-DAGDLhj2.js.map +1 -0
  241. package/dist/static/assets/xychartDiagram-PRI3JC2R-C0J9iwTO.js +1888 -0
  242. package/dist/static/assets/xychartDiagram-PRI3JC2R-C0J9iwTO.js.map +1 -0
  243. package/dist/static/index.html +71 -0
  244. package/dist/static/testing/dom-observation-utils.js +425 -0
  245. package/dist/static/testing/e2e-test-helper.js +3119 -0
  246. package/dist/team.js +1160 -0
  247. package/dist/tellask.js +431 -0
  248. package/dist/tool.js +150 -0
  249. package/dist/tools/apply-patch.js +542 -0
  250. package/dist/tools/builtins.js +196 -0
  251. package/dist/tools/context-health.js +177 -0
  252. package/dist/tools/ctrl.js +478 -0
  253. package/dist/tools/diag.js +583 -0
  254. package/dist/tools/env.js +184 -0
  255. package/dist/tools/fs.js +818 -0
  256. package/dist/tools/mcp.js +138 -0
  257. package/dist/tools/mem.js +349 -0
  258. package/dist/tools/os.js +751 -0
  259. package/dist/tools/prompts/team_mgmt.en.md +70 -0
  260. package/dist/tools/prompts/team_mgmt.zh.md +70 -0
  261. package/dist/tools/prompts/ws_mod.en.md +86 -0
  262. package/dist/tools/prompts/ws_mod.zh.md +87 -0
  263. package/dist/tools/registry-snapshot.js +31 -0
  264. package/dist/tools/registry.js +121 -0
  265. package/dist/tools/ripgrep.js +678 -0
  266. package/dist/tools/team-mgmt.js +3300 -0
  267. package/dist/tools/txt.js +3178 -0
  268. package/dist/utils/id.js +72 -0
  269. package/dist/utils/task-doc.js +236 -0
  270. package/dist/utils/task-package.js +522 -0
  271. package/dist/utils/taskdoc-search.js +280 -0
  272. package/dist/utils/taskdoc.js +400 -0
  273. package/package.json +69 -0
@@ -0,0 +1,3128 @@
1
+ "use strict";
2
+ /**
3
+ * Module: persistence
4
+ *
5
+ * Modern dialog persistence with strong typing and latest.yaml support.
6
+ * Provides file-based storage with append-only events and atomic operations.
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.DialogPersistence = exports.DiskFileDialogStore = void 0;
43
+ const fs = __importStar(require("fs"));
44
+ const node_crypto_1 = require("node:crypto");
45
+ const path = __importStar(require("path"));
46
+ const yaml = __importStar(require("yaml"));
47
+ const dialog_1 = require("./dialog");
48
+ const evt_registry_1 = require("./evt-registry");
49
+ const log_1 = require("./log");
50
+ const async_fifo_mutex_1 = require("./shared/async-fifo-mutex");
51
+ const runtime_language_1 = require("./shared/runtime-language");
52
+ const inter_dialog_format_1 = require("./shared/utils/inter-dialog-format");
53
+ const time_1 = require("./shared/utils/time");
54
+ const registry_1 = require("./tools/registry");
55
+ function getErrorCode(error) {
56
+ if (typeof error !== 'object' || error === null)
57
+ return undefined;
58
+ const maybeCode = error.code;
59
+ return typeof maybeCode === 'string' ? maybeCode : undefined;
60
+ }
61
+ function isRecord(value) {
62
+ return typeof value === 'object' && value !== null;
63
+ }
64
+ function normalizeQ4HKind(value) {
65
+ return value === 'keep_going_budget_exhausted' ||
66
+ value === 'context_health_critical' ||
67
+ value === 'generic'
68
+ ? value
69
+ : 'generic';
70
+ }
71
+ function isAssignmentFromSup(value) {
72
+ if (!isRecord(value))
73
+ return false;
74
+ if (typeof value.headLine !== 'string')
75
+ return false;
76
+ if (typeof value.callBody !== 'string')
77
+ return false;
78
+ if (typeof value.originMemberId !== 'string')
79
+ return false;
80
+ if (typeof value.callerDialogId !== 'string')
81
+ return false;
82
+ if (typeof value.callId !== 'string')
83
+ return false;
84
+ if ('collectiveTargets' in value) {
85
+ const collectiveTargets = value.collectiveTargets;
86
+ if (collectiveTargets === undefined)
87
+ return true;
88
+ if (!Array.isArray(collectiveTargets))
89
+ return false;
90
+ if (!collectiveTargets.every((item) => typeof item === 'string'))
91
+ return false;
92
+ }
93
+ return true;
94
+ }
95
+ function isRootDialogMetadataFile(value) {
96
+ if (!isRecord(value))
97
+ return false;
98
+ if (typeof value.id !== 'string')
99
+ return false;
100
+ if (typeof value.agentId !== 'string')
101
+ return false;
102
+ if (typeof value.taskDocPath !== 'string')
103
+ return false;
104
+ if (typeof value.createdAt !== 'string')
105
+ return false;
106
+ if (value.supdialogId !== undefined)
107
+ return false;
108
+ if (value.tellaskSession !== undefined)
109
+ return false;
110
+ if (value.assignmentFromSup !== undefined)
111
+ return false;
112
+ return true;
113
+ }
114
+ function isSubdialogMetadataFile(value) {
115
+ if (!isRecord(value))
116
+ return false;
117
+ if (typeof value.id !== 'string')
118
+ return false;
119
+ if (typeof value.agentId !== 'string')
120
+ return false;
121
+ if (typeof value.taskDocPath !== 'string')
122
+ return false;
123
+ if (typeof value.createdAt !== 'string')
124
+ return false;
125
+ if (typeof value.supdialogId !== 'string')
126
+ return false;
127
+ if (value.tellaskSession !== undefined && typeof value.tellaskSession !== 'string')
128
+ return false;
129
+ if (!isAssignmentFromSup(value.assignmentFromSup))
130
+ return false;
131
+ return true;
132
+ }
133
+ function isDialogMetadataFile(value) {
134
+ return isRootDialogMetadataFile(value) || isSubdialogMetadataFile(value);
135
+ }
136
+ function isDialogLatestFile(value) {
137
+ if (!isRecord(value))
138
+ return false;
139
+ return (typeof value.currentRound === 'number' &&
140
+ typeof value.lastModified === 'string' &&
141
+ (value.status === 'active' || value.status === 'completed' || value.status === 'archived') &&
142
+ (value.disableDiligencePush === undefined || typeof value.disableDiligencePush === 'boolean'));
143
+ }
144
+ function isSubdialogResponseRecord(value) {
145
+ if (!isRecord(value))
146
+ return false;
147
+ if (typeof value.responseId !== 'string')
148
+ return false;
149
+ if (typeof value.subdialogId !== 'string')
150
+ return false;
151
+ if (typeof value.response !== 'string')
152
+ return false;
153
+ if (typeof value.completedAt !== 'string')
154
+ return false;
155
+ if (value.callType !== 'A' && value.callType !== 'B' && value.callType !== 'C')
156
+ return false;
157
+ if (typeof value.headLine !== 'string')
158
+ return false;
159
+ if (typeof value.responderId !== 'string')
160
+ return false;
161
+ if (typeof value.originMemberId !== 'string')
162
+ return false;
163
+ if (typeof value.callId !== 'string')
164
+ return false;
165
+ return true;
166
+ }
167
+ // Remove old type definitions - now using shared/types/storage.ts
168
+ const tellask_1 = require("./tellask");
169
+ const id_1 = require("./utils/id");
170
+ /**
171
+ * Uses append-only pattern for events, exceptional overwrite for reminders
172
+ */
173
+ class DiskFileDialogStore extends dialog_1.DialogStore {
174
+ constructor(dialogId) {
175
+ super();
176
+ // Track saying/thinking content for persistence
177
+ this.sayingContent = '';
178
+ this.thinkingContent = '';
179
+ this.dialogId = dialogId;
180
+ }
181
+ // === DialogStore interface methods (for compatibility) ===
182
+ /**
183
+ * Create subdialog with automatic persistence
184
+ */
185
+ async createSubDialog(supdialog, targetAgentId, headLine, callBody, options) {
186
+ const generatedId = (0, id_1.generateDialogID)();
187
+ // For subdialogs, use the supdialog's root dialog ID as the root
188
+ const subdialogId = new dialog_1.DialogID(generatedId, supdialog.id.rootId);
189
+ // Prepare subdialog store
190
+ const subdialogStore = new DiskFileDialogStore(subdialogId);
191
+ const subdialog = new dialog_1.SubDialog(subdialogStore, supdialog, supdialog.taskDocPath, subdialogId, targetAgentId, {
192
+ headLine,
193
+ callBody,
194
+ originMemberId: options.originMemberId,
195
+ callerDialogId: options.callerDialogId,
196
+ callId: options.callId,
197
+ }, options.tellaskSession);
198
+ // Initial subdialog user prompt is now persisted at first drive (driver.ts)
199
+ // Ensure subdialog directory and persist metadata under supdialog/.subdialogs/
200
+ await this.ensureSubdialogDirectory(subdialogId);
201
+ const metadata = {
202
+ id: subdialogId.selfId,
203
+ agentId: targetAgentId,
204
+ taskDocPath: supdialog.taskDocPath,
205
+ createdAt: (0, time_1.formatUnifiedTimestamp)(new Date()),
206
+ supdialogId: supdialog.id.selfId,
207
+ tellaskSession: options.tellaskSession,
208
+ assignmentFromSup: {
209
+ headLine,
210
+ callBody,
211
+ originMemberId: options.originMemberId,
212
+ callerDialogId: options.callerDialogId,
213
+ callId: options.callId,
214
+ },
215
+ };
216
+ await DialogPersistence.saveSubdialogMetadata(subdialogId, metadata);
217
+ await DialogPersistence.saveDialogMetadata(subdialogId, metadata);
218
+ // Initialize latest.yaml via the mutation API (write-back will flush).
219
+ await DialogPersistence.mutateDialogLatest(subdialogId, () => ({
220
+ kind: 'replace',
221
+ next: {
222
+ currentRound: 1,
223
+ lastModified: (0, time_1.formatUnifiedTimestamp)(new Date()),
224
+ status: 'active',
225
+ messageCount: 0,
226
+ functionCallCount: 0,
227
+ subdialogCount: 0,
228
+ runState: { kind: 'idle_waiting_user' },
229
+ disableDiligencePush: false,
230
+ },
231
+ }));
232
+ // Supdialog clarification context is persisted in subdialog metadata (supdialogCall)
233
+ const parentRound = await DialogPersistence.getCurrentRoundNumber(supdialog.id);
234
+ const subdialogCreatedEvt = {
235
+ type: 'subdialog_created_evt',
236
+ dialog: {
237
+ selfId: subdialogId.selfId,
238
+ rootId: subdialogId.rootId,
239
+ },
240
+ timestamp: new Date().toISOString(),
241
+ round: parentRound,
242
+ parentDialog: {
243
+ selfId: supdialog.id.selfId,
244
+ rootId: supdialog.id.rootId,
245
+ },
246
+ subDialog: {
247
+ selfId: subdialogId.selfId,
248
+ rootId: subdialogId.rootId,
249
+ },
250
+ targetAgentId,
251
+ headLine,
252
+ callBody,
253
+ };
254
+ // Post subdialog_created_evt to PARENT's PubChan so frontend can receive it
255
+ // The frontend subscribes to the parent's events, not the subdialog's
256
+ (0, evt_registry_1.postDialogEvent)(supdialog, subdialogCreatedEvt);
257
+ return subdialog;
258
+ }
259
+ /**
260
+ * Receive and handle function call results (includes logging)
261
+ */
262
+ async receiveFuncResult(dialog, funcResult) {
263
+ const round = dialog.activeGenRoundOrUndefined ?? dialog.currentRound;
264
+ // Persist function result record
265
+ const funcResultRecord = {
266
+ ts: (0, time_1.formatUnifiedTimestamp)(new Date()),
267
+ type: 'func_result_record',
268
+ id: funcResult.id,
269
+ name: funcResult.name,
270
+ content: funcResult.content,
271
+ genseq: dialog.activeGenSeq,
272
+ };
273
+ await this.appendEvent(round, funcResultRecord);
274
+ // Send event to frontend
275
+ const funcResultEvt = {
276
+ type: 'func_result_evt',
277
+ id: funcResult.id,
278
+ name: funcResult.name,
279
+ content: funcResult.content,
280
+ round,
281
+ };
282
+ (0, evt_registry_1.postDialogEvent)(dialog, funcResultEvt);
283
+ }
284
+ /**
285
+ * Receive and handle tellask tool responses with callId for inline result display
286
+ *
287
+ * Call Types:
288
+ * - Tellask Call (inline bubble): !?@<mention-id>
289
+ * - Result displays INLINE in the same bubble
290
+ * - Uses callId for correlation between call_start and response
291
+ * - Uses receiveToolResponse() + callId parameter
292
+ *
293
+ * - Teammate Tellask: !?@agentName (e.g., !?@coder, !?@tester)
294
+ * - Result displays in SEPARATE bubble (subdialog response)
295
+ * - Uses calleeDialogId for correlation
296
+ * - Uses receiveTeammateResponse() instead
297
+ *
298
+ * @param dialog - The dialog receiving the response
299
+ * @param responderId - ID of the tool/agent that responded (e.g., "add_reminder")
300
+ * @param headLine - Headline of the original tool call
301
+ * @param result - The result content to display
302
+ * @param status - Response status ('completed' | 'failed')
303
+ * @param callId - Correlation ID from call_start_evt (REQUIRED for inline display)
304
+ */
305
+ async receiveToolResponse(dialog, responderId, headLine, result, status, callId) {
306
+ const round = dialog.activeGenRoundOrUndefined ?? dialog.currentRound;
307
+ const calling_genseq = dialog.activeGenSeqOrUndefined;
308
+ // Persist record WITH callId for replay correlation
309
+ const ev = {
310
+ ts: (0, time_1.formatUnifiedTimestamp)(new Date()),
311
+ type: 'tool_call_result_record',
312
+ responderId,
313
+ headLine,
314
+ status,
315
+ result,
316
+ calling_genseq,
317
+ callId,
318
+ };
319
+ await this.appendEvent(round, ev);
320
+ // Emit ToolCallResponseEvent WITH callId for UI correlation
321
+ const toolResponseEvt = {
322
+ type: 'tool_call_response_evt',
323
+ responderId,
324
+ headLine,
325
+ status,
326
+ result,
327
+ round,
328
+ calling_genseq,
329
+ callId,
330
+ };
331
+ (0, evt_registry_1.postDialogEvent)(dialog, toolResponseEvt);
332
+ }
333
+ /**
334
+ * Receive and handle TEAMMATE TELLASK responses (separate bubble for @agentName tellasks)
335
+ *
336
+ * Call Types:
337
+ * - Teammate Tellask: !?@agentName (e.g., !?@coder, !?@tester)
338
+ * - Result displays in SEPARATE bubble (subdialog or supdialog response)
339
+ * - Uses calleeDialogId for correlation (not callId)
340
+ * - Uses this method (receiveTeammateResponse)
341
+ *
342
+ * @param dialog - The dialog receiving the response
343
+ * @param responderId - ID of the teammate agent (e.g., "coder")
344
+ * @param headLine - Headline of the original teammate tellask
345
+ * @param status - Response status ('completed' | 'failed')
346
+ * @param calleeDialogId - ID of the callee dialog (subdialog OR supdialog) for navigation links
347
+ */
348
+ async receiveTeammateResponse(dialog, responderId, headLine, status, calleeDialogId, options) {
349
+ const round = dialog.activeGenRoundOrUndefined ?? dialog.currentRound;
350
+ const calling_genseq = dialog.activeGenSeqOrUndefined;
351
+ const calleeDialogSelfId = calleeDialogId ? calleeDialogId.selfId : undefined;
352
+ const response = options.response;
353
+ const agentId = options.agentId;
354
+ const callId = options.callId;
355
+ const originMemberId = options.originMemberId;
356
+ const result = (0, inter_dialog_format_1.formatTeammateResponseContent)({
357
+ responderId,
358
+ requesterId: originMemberId,
359
+ originalCallHeadLine: headLine,
360
+ responseBody: response,
361
+ language: (0, runtime_language_1.getWorkLanguage)(),
362
+ });
363
+ const ev = {
364
+ ts: (0, time_1.formatUnifiedTimestamp)(new Date()),
365
+ type: 'teammate_response_record',
366
+ responderId,
367
+ calleeDialogId: calleeDialogSelfId,
368
+ headLine,
369
+ status,
370
+ result,
371
+ calling_genseq,
372
+ response,
373
+ agentId,
374
+ callId,
375
+ originMemberId,
376
+ };
377
+ await this.appendEvent(round, ev);
378
+ const teammateResponseEvt = {
379
+ type: 'teammate_response_evt',
380
+ responderId,
381
+ calleeDialogId: calleeDialogSelfId,
382
+ headLine,
383
+ status,
384
+ result,
385
+ round,
386
+ calling_genseq,
387
+ response,
388
+ agentId,
389
+ callId,
390
+ originMemberId,
391
+ };
392
+ (0, evt_registry_1.postDialogEvent)(dialog, teammateResponseEvt);
393
+ }
394
+ /**
395
+ * Ensure subdialog directory exists (delegate to DialogPersistence)
396
+ */
397
+ async ensureSubdialogDirectory(dialogId) {
398
+ return await DialogPersistence.ensureSubdialogDirectory(dialogId);
399
+ }
400
+ /**
401
+ * Append event to round JSONL file (delegate to DialogPersistence)
402
+ */
403
+ async appendEvent(round, event) {
404
+ await DialogPersistence.appendEvent(this.dialogId, round, event);
405
+ }
406
+ /**
407
+ * Notify start of LLM generation for frontend bubble management
408
+ * CRITICAL: This must be called BEFORE any substream events (thinking_start, markdown_start, etc.)
409
+ * to ensure proper event ordering on the frontend.
410
+ */
411
+ async notifyGeneratingStart(dialog) {
412
+ const round = dialog.activeGenRoundOrUndefined ?? dialog.currentRound;
413
+ const genseq = dialog.activeGenSeq;
414
+ try {
415
+ const ev = {
416
+ ts: (0, time_1.formatUnifiedTimestamp)(new Date()),
417
+ type: 'gen_start_record',
418
+ genseq: genseq,
419
+ };
420
+ await this.appendEvent(round, ev);
421
+ // Emit generating_start_evt event
422
+ // This event MUST be emitted and processed before any substream events
423
+ // to ensure the frontend has created the generation bubble before receiving
424
+ // thinking/markdown/calling events
425
+ const genStartEvt = {
426
+ type: 'generating_start_evt',
427
+ round,
428
+ genseq: genseq,
429
+ };
430
+ (0, evt_registry_1.postDialogEvent)(dialog, genStartEvt);
431
+ // Update generating flag in latest.yaml
432
+ await DialogPersistence.mutateDialogLatest(this.dialogId, () => ({
433
+ kind: 'patch',
434
+ patch: { generating: true },
435
+ }));
436
+ }
437
+ catch (err) {
438
+ log_1.log.warn('Failed to persist gen_start event', err);
439
+ }
440
+ }
441
+ /**
442
+ * Notify end of LLM generation for frontend bubble management
443
+ */
444
+ async notifyGeneratingFinish(dialog, contextHealth, llmGenModel) {
445
+ const round = dialog.activeGenRoundOrUndefined ?? dialog.currentRound;
446
+ const genseq = dialog.activeGenSeq;
447
+ if (genseq === undefined) {
448
+ throw new Error('Missing active genseq for notifyGeneratingFinish');
449
+ }
450
+ try {
451
+ const ev = {
452
+ ts: (0, time_1.formatUnifiedTimestamp)(new Date()),
453
+ type: 'gen_finish_record',
454
+ genseq: genseq,
455
+ contextHealth,
456
+ llmGenModel,
457
+ };
458
+ await this.appendEvent(round, ev);
459
+ // Emit generating_finish_evt event (this was missing, causing double triggering issue)
460
+ const genFinishEvt = {
461
+ type: 'generating_finish_evt',
462
+ round,
463
+ genseq: genseq,
464
+ llmGenModel,
465
+ };
466
+ (0, evt_registry_1.postDialogEvent)(dialog, genFinishEvt);
467
+ if (contextHealth) {
468
+ const ctxEvt = {
469
+ type: 'context_health_evt',
470
+ round,
471
+ genseq,
472
+ contextHealth,
473
+ };
474
+ (0, evt_registry_1.postDialogEvent)(dialog, ctxEvt);
475
+ }
476
+ // Update generating flag in latest.yaml
477
+ await DialogPersistence.mutateDialogLatest(this.dialogId, () => ({
478
+ kind: 'patch',
479
+ patch: { generating: false },
480
+ }));
481
+ }
482
+ catch (err) {
483
+ log_1.log.warn('Failed to persist gen_finish event', err);
484
+ }
485
+ }
486
+ async sayingStart(dialog) {
487
+ // Reset saying content tracker
488
+ this.sayingContent = '';
489
+ }
490
+ async sayingChunk(dialog, chunk) {
491
+ // Collect saying content for persistence
492
+ this.sayingContent += chunk;
493
+ }
494
+ async sayingFinish(dialog) {
495
+ const round = dialog.activeGenRoundOrUndefined ?? dialog.currentRound;
496
+ const sayingContent = this.sayingContent.trim();
497
+ // Persist saying content as a message event
498
+ if (sayingContent) {
499
+ const sayingMessageEvent = {
500
+ ts: (0, time_1.formatUnifiedTimestamp)(new Date()),
501
+ type: 'agent_words_record',
502
+ genseq: dialog.activeGenSeq,
503
+ content: sayingContent,
504
+ };
505
+ await this.appendEvent(round, sayingMessageEvent);
506
+ }
507
+ }
508
+ async thinkingStart(dialog) {
509
+ const round = dialog.activeGenRoundOrUndefined ?? dialog.currentRound;
510
+ // Reset thinking content tracker
511
+ this.thinkingContent = '';
512
+ const thinkingStartEvt = {
513
+ type: 'thinking_start_evt',
514
+ round,
515
+ genseq: dialog.activeGenSeq,
516
+ };
517
+ (0, evt_registry_1.postDialogEvent)(dialog, thinkingStartEvt);
518
+ }
519
+ async thinkingChunk(dialog, chunk) {
520
+ const round = dialog.activeGenRoundOrUndefined ?? dialog.currentRound;
521
+ // Collect thinking content for persistence
522
+ this.thinkingContent += chunk;
523
+ const thinkingChunkEvt = {
524
+ type: 'thinking_chunk_evt',
525
+ chunk,
526
+ round,
527
+ genseq: dialog.activeGenSeq,
528
+ };
529
+ (0, evt_registry_1.postDialogEvent)(dialog, thinkingChunkEvt);
530
+ }
531
+ async thinkingFinish(dialog) {
532
+ const round = dialog.activeGenRoundOrUndefined ?? dialog.currentRound;
533
+ // Persist thinking content as a message event
534
+ if (this.thinkingContent.trim()) {
535
+ const thinkingMessageEvent = {
536
+ ts: (0, time_1.formatUnifiedTimestamp)(new Date()),
537
+ type: 'agent_thought_record',
538
+ genseq: dialog.activeGenSeq,
539
+ content: this.thinkingContent.trim(),
540
+ };
541
+ await this.appendEvent(round, thinkingMessageEvent);
542
+ }
543
+ const thinkingFinishEvt = {
544
+ type: 'thinking_finish_evt',
545
+ round,
546
+ genseq: dialog.activeGenSeq,
547
+ };
548
+ (0, evt_registry_1.postDialogEvent)(dialog, thinkingFinishEvt);
549
+ }
550
+ async markdownStart(dialog) {
551
+ const round = dialog.activeGenRoundOrUndefined ?? dialog.currentRound;
552
+ const markdownStartEvt = {
553
+ type: 'markdown_start_evt',
554
+ round,
555
+ genseq: dialog.activeGenSeq,
556
+ };
557
+ (0, evt_registry_1.postDialogEvent)(dialog, markdownStartEvt);
558
+ }
559
+ async markdownChunk(dialog, chunk) {
560
+ const round = dialog.activeGenRoundOrUndefined ?? dialog.currentRound;
561
+ const evt = {
562
+ type: 'markdown_chunk_evt',
563
+ chunk,
564
+ round,
565
+ genseq: dialog.activeGenSeq,
566
+ };
567
+ (0, evt_registry_1.postDialogEvent)(dialog, evt);
568
+ }
569
+ async markdownFinish(dialog) {
570
+ const round = dialog.activeGenRoundOrUndefined ?? dialog.currentRound;
571
+ const evt = {
572
+ type: 'markdown_finish_evt',
573
+ round,
574
+ genseq: dialog.activeGenSeq,
575
+ };
576
+ (0, evt_registry_1.postDialogEvent)(dialog, evt);
577
+ }
578
+ // Tellask call streaming methods
579
+ async callingStart(dialog, validation) {
580
+ const round = dialog.activeGenRoundOrUndefined ?? dialog.currentRound;
581
+ const evt = {
582
+ type: 'tool_call_start_evt',
583
+ validation,
584
+ round,
585
+ genseq: dialog.activeGenSeq,
586
+ };
587
+ (0, evt_registry_1.postDialogEvent)(dialog, evt);
588
+ }
589
+ async callingHeadlineChunk(dialog, chunk) {
590
+ const round = dialog.activeGenRoundOrUndefined ?? dialog.currentRound;
591
+ const evt = {
592
+ type: 'tool_call_headline_chunk_evt',
593
+ chunk,
594
+ round,
595
+ genseq: dialog.activeGenSeq,
596
+ };
597
+ (0, evt_registry_1.postDialogEvent)(dialog, evt);
598
+ }
599
+ async callingHeadlineFinish(dialog) {
600
+ const round = dialog.activeGenRoundOrUndefined ?? dialog.currentRound;
601
+ const evt = {
602
+ type: 'tool_call_headline_finish_evt',
603
+ round,
604
+ genseq: dialog.activeGenSeq,
605
+ };
606
+ (0, evt_registry_1.postDialogEvent)(dialog, evt);
607
+ }
608
+ async callingBodyStart(dialog) {
609
+ const round = dialog.activeGenRoundOrUndefined ?? dialog.currentRound;
610
+ const evt = {
611
+ type: 'tool_call_body_start_evt',
612
+ round,
613
+ genseq: dialog.activeGenSeq,
614
+ };
615
+ (0, evt_registry_1.postDialogEvent)(dialog, evt);
616
+ }
617
+ async callingBodyChunk(dialog, chunk) {
618
+ const round = dialog.activeGenRoundOrUndefined ?? dialog.currentRound;
619
+ const evt = {
620
+ type: 'tool_call_body_chunk_evt',
621
+ chunk,
622
+ round,
623
+ genseq: dialog.activeGenSeq,
624
+ };
625
+ (0, evt_registry_1.postDialogEvent)(dialog, evt);
626
+ }
627
+ async callingBodyFinish(dialog) {
628
+ const round = dialog.activeGenRoundOrUndefined ?? dialog.currentRound;
629
+ const evt = {
630
+ type: 'tool_call_body_finish_evt',
631
+ round,
632
+ genseq: dialog.activeGenSeq,
633
+ };
634
+ (0, evt_registry_1.postDialogEvent)(dialog, evt);
635
+ }
636
+ async callingFinish(dialog, callId) {
637
+ const round = dialog.activeGenRoundOrUndefined ?? dialog.currentRound;
638
+ const evt = {
639
+ type: 'tool_call_finish_evt',
640
+ callId,
641
+ round,
642
+ genseq: dialog.activeGenSeq,
643
+ };
644
+ (0, evt_registry_1.postDialogEvent)(dialog, evt);
645
+ }
646
+ // Function call events (non-streaming mode - single event captures entire call)
647
+ async funcCallRequested(dialog, funcId, funcName, argumentsStr) {
648
+ const round = dialog.activeGenRoundOrUndefined ?? dialog.currentRound;
649
+ const funcCallEvt = {
650
+ type: 'func_call_requested_evt',
651
+ funcId,
652
+ funcName,
653
+ arguments: argumentsStr,
654
+ round,
655
+ genseq: dialog.activeGenSeq,
656
+ };
657
+ (0, evt_registry_1.postDialogEvent)(dialog, funcCallEvt);
658
+ }
659
+ /**
660
+ * Emit stream error for current generation lifecycle (uses active genseq when present)
661
+ */
662
+ async streamError(dialog, error) {
663
+ log_1.log.error(`Dialog stream error '${error}'`, new Error(), { dialog });
664
+ const round = dialog.activeGenRoundOrUndefined ?? dialog.currentRound;
665
+ const genseq = typeof dialog.activeGenSeq === 'number' ? dialog.activeGenSeq : undefined;
666
+ // Enhanced stream error event with better error classification
667
+ const streamErrorEvent = {
668
+ type: 'stream_error_evt',
669
+ round,
670
+ genseq,
671
+ error,
672
+ };
673
+ (0, evt_registry_1.postDialogEvent)(dialog, streamErrorEvent);
674
+ }
675
+ /**
676
+ * Start new round (append-only JSONL + exceptional reminder persistence)
677
+ */
678
+ async startNewRound(dialog, _newRoundPrompt) {
679
+ const previousRound = dialog.currentRound;
680
+ const newRound = previousRound + 1;
681
+ // Persist reminders state for new round (exceptional overwrite)
682
+ // Use the currently attached dialog's reminders to avoid stale state
683
+ await this.persistReminders(dialog, dialog.reminders || []);
684
+ // Update latest.yaml with new round (lastModified is set by persistence layer)
685
+ await DialogPersistence.mutateDialogLatest(this.dialogId, () => ({
686
+ kind: 'patch',
687
+ patch: { currentRound: newRound },
688
+ }));
689
+ // Post round update event
690
+ const roundUpdateEvt = {
691
+ type: 'round_update',
692
+ round: newRound,
693
+ totalRounds: newRound,
694
+ };
695
+ (0, evt_registry_1.postDialogEvent)(dialog, roundUpdateEvt);
696
+ }
697
+ /**
698
+ * Persist reminder state (exceptional overwrite pattern)
699
+ * Note: Event emission is handled by processReminderUpdates() in Dialog
700
+ */
701
+ async persistReminders(dialog, reminders) {
702
+ await DialogPersistence._saveReminderState(this.dialogId, reminders);
703
+ }
704
+ /**
705
+ * Persist a user message to storage
706
+ * Note: The end_of_user_saying_evt is emitted by the driver after user content
707
+ * is rendered and any tellask calls are parsed/executed.
708
+ */
709
+ async persistUserMessage(dialog, content, msgId, grammar, userLanguageCode) {
710
+ const round = dialog.currentRound;
711
+ // Use activeGenSeqOrUndefined to handle case when genseq hasn't been initialized yet
712
+ const genseq = dialog.activeGenSeqOrUndefined ?? 1;
713
+ const humanEv = {
714
+ ts: (0, time_1.formatUnifiedTimestamp)(new Date()),
715
+ type: 'human_text_record',
716
+ genseq: genseq,
717
+ content: String(content || ''),
718
+ msgId: msgId,
719
+ grammar,
720
+ userLanguageCode,
721
+ };
722
+ await this.appendEvent(round, humanEv);
723
+ // Note: end_of_user_saying_evt is now emitted by llm/driver.ts after tellask calls complete
724
+ }
725
+ /**
726
+ * Persist an assistant message to storage
727
+ */
728
+ async persistAgentMessage(dialog, content, genseq, type, provider_data) {
729
+ const round = dialog.activeGenRoundOrUndefined ?? dialog.currentRound;
730
+ const event = type === 'thinking_msg'
731
+ ? {
732
+ ts: (0, time_1.formatUnifiedTimestamp)(new Date()),
733
+ type: 'agent_thought_record',
734
+ genseq,
735
+ content: content || '',
736
+ provider_data,
737
+ }
738
+ : {
739
+ ts: (0, time_1.formatUnifiedTimestamp)(new Date()),
740
+ type: 'agent_words_record',
741
+ genseq,
742
+ content: content || '',
743
+ };
744
+ await this.appendEvent(round, event);
745
+ }
746
+ /**
747
+ * Persist a function call to storage
748
+ */
749
+ async persistFunctionCall(dialog, id, name, arguments_, genseq) {
750
+ const round = dialog.activeGenRoundOrUndefined ?? dialog.currentRound;
751
+ const funcCallEvent = {
752
+ ts: (0, time_1.formatUnifiedTimestamp)(new Date()),
753
+ type: 'func_call_record',
754
+ genseq,
755
+ id,
756
+ name,
757
+ arguments: arguments_,
758
+ };
759
+ await this.appendEvent(round, funcCallEvent);
760
+ // NOTE: func_call_evt REMOVED - persistence uses FuncCallRecord directly
761
+ // UI display uses func_call_requested_evt instead
762
+ }
763
+ /**
764
+ * Update questions for human state (exceptional overwrite pattern)
765
+ */
766
+ async updateQuestions4Human(dialog, questions) {
767
+ await DialogPersistence._saveQuestions4HumanState(this.dialogId, questions);
768
+ }
769
+ /**
770
+ * Load Questions for Human state from storage
771
+ */
772
+ async loadQuestions4Human(dialogId, status) {
773
+ return await DialogPersistence.loadQuestions4HumanState(dialogId, status);
774
+ }
775
+ async loadDialogMetadata(dialogId, status) {
776
+ return await DialogPersistence.loadDialogMetadata(dialogId, status);
777
+ }
778
+ async loadPendingSubdialogs(rootDialogId, status) {
779
+ const records = await DialogPersistence.loadPendingSubdialogs(rootDialogId, status);
780
+ return records.map((record) => ({
781
+ subdialogId: new dialog_1.DialogID(record.subdialogId, rootDialogId.rootId),
782
+ createdAt: record.createdAt,
783
+ headLine: record.headLine,
784
+ targetAgentId: record.targetAgentId,
785
+ callType: record.callType,
786
+ tellaskSession: record.tellaskSession,
787
+ }));
788
+ }
789
+ async saveSubdialogRegistry(rootDialogId, entries, status) {
790
+ await DialogPersistence.saveSubdialogRegistry(rootDialogId, entries, status);
791
+ }
792
+ async loadSubdialogRegistry(rootDialog, status) {
793
+ const entries = await DialogPersistence.loadSubdialogRegistry(rootDialog.id, status);
794
+ for (const entry of entries) {
795
+ if (!entry.tellaskSession)
796
+ continue;
797
+ const existing = rootDialog.lookupDialog(entry.subdialogId.selfId);
798
+ if (existing) {
799
+ if (existing instanceof dialog_1.SubDialog && existing.tellaskSession) {
800
+ rootDialog.registerSubdialog(existing);
801
+ }
802
+ continue;
803
+ }
804
+ const subdialogState = await DialogPersistence.restoreDialog(entry.subdialogId, status);
805
+ if (!subdialogState)
806
+ continue;
807
+ const assignmentFromSup = subdialogState.metadata.assignmentFromSup;
808
+ if (!assignmentFromSup)
809
+ continue;
810
+ const subdialogStore = new DiskFileDialogStore(entry.subdialogId);
811
+ const subdialog = new dialog_1.SubDialog(subdialogStore, rootDialog, subdialogState.metadata.taskDocPath, new dialog_1.DialogID(entry.subdialogId.selfId, rootDialog.id.rootId), subdialogState.metadata.agentId, assignmentFromSup, entry.tellaskSession, {
812
+ messages: subdialogState.messages,
813
+ reminders: subdialogState.reminders,
814
+ currentRound: subdialogState.currentRound,
815
+ });
816
+ rootDialog.registerSubdialog(subdialog);
817
+ }
818
+ }
819
+ /**
820
+ * Clear Questions for Human state in storage
821
+ */
822
+ async clearQuestions4Human(dialog) {
823
+ const previousQuestions = await DialogPersistence.loadQuestions4HumanState(dialog.id);
824
+ const previousCount = previousQuestions.length;
825
+ if (previousCount > 0) {
826
+ await DialogPersistence.clearQuestions4HumanState(dialog.id);
827
+ // Emit q4h_answered events for each removed question
828
+ for (const q of previousQuestions) {
829
+ const answeredEvent = {
830
+ type: 'q4h_answered',
831
+ questionId: q.id,
832
+ selfId: dialog.id.selfId,
833
+ };
834
+ (0, evt_registry_1.postDialogEvent)(dialog, answeredEvent);
835
+ }
836
+ }
837
+ }
838
+ /**
839
+ * Get current questions for human count for UI decoration
840
+ */
841
+ async getQuestions4HumanCount() {
842
+ const questions = await DialogPersistence.loadQuestions4HumanState(this.dialogId);
843
+ return questions.length;
844
+ }
845
+ /**
846
+ * Load current round number from persisted metadata
847
+ */
848
+ async loadCurrentRound(dialogId) {
849
+ return await DialogPersistence.getCurrentRoundNumber(dialogId, 'running');
850
+ }
851
+ /**
852
+ * Get next sequence number for generation
853
+ */
854
+ async getNextSeq(dialogId, round) {
855
+ return await DialogPersistence.getNextSeq(dialogId, round, 'running');
856
+ }
857
+ /**
858
+ * Send dialog events directly to a specific WebSocket connection for dialog restoration
859
+ * CRITICAL: This bypasses PubChan to ensure only the requesting session receives restoration events
860
+ * Unlike replayDialogEvents(), this sends events directly to ws.send() instead of postDialogEvent()
861
+ * @param ws - WebSocket connection to send events to
862
+ * @param dialog - Dialog object containing metadata
863
+ * @param round - Optional round number (uses dialog.currentRound if not provided)
864
+ * @param totalRounds - Optional total rounds count (defaults to round/currentRound)
865
+ */
866
+ async sendDialogEventsDirectly(ws, dialog, round, totalRounds, status = 'running') {
867
+ try {
868
+ // Use provided round or fallback to dialog.currentRound (which may be stale for new Dialog objects)
869
+ const currentRound = round ?? dialog.currentRound;
870
+ const effectiveTotalRounds = totalRounds ?? currentRound;
871
+ const persistenceEvents = await DialogPersistence.readRoundEvents(dialog.id, currentRound, status);
872
+ // Send round_update event directly to this WebSocket only
873
+ ws.send(JSON.stringify({
874
+ type: 'round_update',
875
+ dialog: {
876
+ selfId: dialog.id.selfId,
877
+ rootId: dialog.id.rootId,
878
+ },
879
+ round: currentRound,
880
+ totalRounds: effectiveTotalRounds,
881
+ }));
882
+ // Events are already in chronological order from JSONL file (append-only pattern)
883
+ // Send each persistence event directly to the requesting WebSocket
884
+ for (const event of persistenceEvents) {
885
+ await this.sendEventDirectlyToWebSocket(ws, dialog, currentRound, event);
886
+ }
887
+ // Rehydrate reminders from dialog state
888
+ const dialogState = await DialogPersistence.restoreDialog(dialog.id, status);
889
+ const rehydrated = (dialogState?.reminders ?? []).map((r) => {
890
+ return { content: r.content, owner: r.owner, meta: r.meta };
891
+ });
892
+ dialog.reminders.length = 0;
893
+ dialog.reminders.push(...rehydrated);
894
+ }
895
+ catch (error) {
896
+ log_1.log.error(`Failed to send dialog events directly for ${dialog.id.selfId}:`, error);
897
+ throw error;
898
+ }
899
+ }
900
+ /**
901
+ * Send a single persistence event directly to a WebSocket connection
902
+ * CRITICAL: Avoid PubChan completely for dialog restoration to the single client's display_dialog request
903
+ */
904
+ async sendEventDirectlyToWebSocket(ws, dialog, round, event) {
905
+ switch (event.type) {
906
+ case 'human_text_record': {
907
+ const genseq = event.genseq;
908
+ const content = event.content || '';
909
+ const grammar = event.grammar ?? 'tellask';
910
+ const userLanguageCode = event.userLanguageCode;
911
+ if (content) {
912
+ if (grammar === 'tellask') {
913
+ const receiver = {
914
+ markdownStart: async () => {
915
+ if (ws.readyState === 1) {
916
+ ws.send(JSON.stringify({
917
+ type: 'markdown_start_evt',
918
+ round,
919
+ genseq,
920
+ dialog: { selfId: dialog.id.selfId, rootId: dialog.id.rootId },
921
+ timestamp: event.ts,
922
+ }));
923
+ }
924
+ },
925
+ markdownChunk: async (chunk) => {
926
+ if (ws.readyState === 1) {
927
+ ws.send(JSON.stringify({
928
+ type: 'markdown_chunk_evt',
929
+ chunk,
930
+ round,
931
+ genseq,
932
+ dialog: { selfId: dialog.id.selfId, rootId: dialog.id.rootId },
933
+ timestamp: event.ts,
934
+ }));
935
+ }
936
+ },
937
+ markdownFinish: async () => {
938
+ if (ws.readyState === 1) {
939
+ ws.send(JSON.stringify({
940
+ type: 'markdown_finish_evt',
941
+ round,
942
+ genseq,
943
+ dialog: { selfId: dialog.id.selfId, rootId: dialog.id.rootId },
944
+ timestamp: event.ts,
945
+ }));
946
+ }
947
+ },
948
+ callStart: async (validation) => {
949
+ if (ws.readyState === 1) {
950
+ ws.send(JSON.stringify({
951
+ type: 'tool_call_start_evt',
952
+ validation,
953
+ round,
954
+ genseq,
955
+ dialog: { selfId: dialog.id.selfId, rootId: dialog.id.rootId },
956
+ timestamp: event.ts,
957
+ }));
958
+ }
959
+ },
960
+ callHeadLineChunk: async (chunk) => {
961
+ if (ws.readyState === 1) {
962
+ ws.send(JSON.stringify({
963
+ type: 'tool_call_headline_chunk_evt',
964
+ chunk,
965
+ round,
966
+ genseq,
967
+ dialog: { selfId: dialog.id.selfId, rootId: dialog.id.rootId },
968
+ timestamp: event.ts,
969
+ }));
970
+ }
971
+ },
972
+ callHeadLineFinish: async () => {
973
+ if (ws.readyState === 1) {
974
+ ws.send(JSON.stringify({
975
+ type: 'tool_call_headline_finish_evt',
976
+ round,
977
+ genseq,
978
+ dialog: { selfId: dialog.id.selfId, rootId: dialog.id.rootId },
979
+ timestamp: event.ts,
980
+ }));
981
+ }
982
+ },
983
+ callBodyStart: async () => {
984
+ if (ws.readyState === 1) {
985
+ ws.send(JSON.stringify({
986
+ type: 'tool_call_body_start_evt',
987
+ round,
988
+ genseq,
989
+ dialog: { selfId: dialog.id.selfId, rootId: dialog.id.rootId },
990
+ timestamp: event.ts,
991
+ }));
992
+ }
993
+ },
994
+ callBodyChunk: async (chunk) => {
995
+ if (ws.readyState === 1) {
996
+ ws.send(JSON.stringify({
997
+ type: 'tool_call_body_chunk_evt',
998
+ chunk,
999
+ round,
1000
+ genseq,
1001
+ dialog: { selfId: dialog.id.selfId, rootId: dialog.id.rootId },
1002
+ timestamp: event.ts,
1003
+ }));
1004
+ }
1005
+ },
1006
+ callBodyFinish: async () => {
1007
+ if (ws.readyState === 1) {
1008
+ ws.send(JSON.stringify({
1009
+ type: 'tool_call_body_finish_evt',
1010
+ round,
1011
+ genseq,
1012
+ dialog: { selfId: dialog.id.selfId, rootId: dialog.id.rootId },
1013
+ timestamp: event.ts,
1014
+ }));
1015
+ }
1016
+ },
1017
+ callFinish: async (_callId) => {
1018
+ if (ws.readyState === 1) {
1019
+ ws.send(JSON.stringify({
1020
+ type: 'tool_call_finish_evt',
1021
+ callId: _callId,
1022
+ round,
1023
+ genseq,
1024
+ dialog: { selfId: dialog.id.selfId, rootId: dialog.id.rootId },
1025
+ timestamp: event.ts,
1026
+ }));
1027
+ }
1028
+ },
1029
+ };
1030
+ // Parse user content through TellaskStreamParser (same as live mode)
1031
+ const streamingParser = new tellask_1.TellaskStreamParser(receiver);
1032
+ await streamingParser.takeUpstreamChunk(content);
1033
+ await streamingParser.finalize();
1034
+ }
1035
+ else {
1036
+ if (ws.readyState === 1) {
1037
+ ws.send(JSON.stringify({
1038
+ type: 'markdown_start_evt',
1039
+ round,
1040
+ genseq,
1041
+ dialog: { selfId: dialog.id.selfId, rootId: dialog.id.rootId },
1042
+ timestamp: event.ts,
1043
+ }));
1044
+ ws.send(JSON.stringify({
1045
+ type: 'markdown_chunk_evt',
1046
+ chunk: content,
1047
+ round,
1048
+ genseq,
1049
+ dialog: { selfId: dialog.id.selfId, rootId: dialog.id.rootId },
1050
+ timestamp: event.ts,
1051
+ }));
1052
+ ws.send(JSON.stringify({
1053
+ type: 'markdown_finish_evt',
1054
+ round,
1055
+ genseq,
1056
+ dialog: { selfId: dialog.id.selfId, rootId: dialog.id.rootId },
1057
+ timestamp: event.ts,
1058
+ }));
1059
+ }
1060
+ }
1061
+ }
1062
+ // Emit end_of_user_saying_evt to signal frontend to render <hr/> separator
1063
+ if (ws.readyState === 1) {
1064
+ ws.send(JSON.stringify({
1065
+ type: 'end_of_user_saying_evt',
1066
+ round,
1067
+ genseq,
1068
+ msgId: event.msgId,
1069
+ content,
1070
+ grammar,
1071
+ userLanguageCode,
1072
+ dialog: { selfId: dialog.id.selfId, rootId: dialog.id.rootId },
1073
+ timestamp: event.ts,
1074
+ }));
1075
+ }
1076
+ break;
1077
+ }
1078
+ case 'gen_start_record': {
1079
+ // Create generating_start_evt event using persisted genseq directly
1080
+ const genStartWireEvent = {
1081
+ type: 'generating_start_evt',
1082
+ round,
1083
+ genseq: event.genseq,
1084
+ dialog: {
1085
+ selfId: dialog.id.selfId,
1086
+ rootId: dialog.id.rootId,
1087
+ },
1088
+ timestamp: event.ts,
1089
+ };
1090
+ // Send directly to WebSocket (NO PubChan emission)
1091
+ if (ws.readyState === 1) {
1092
+ ws.send(JSON.stringify(genStartWireEvent));
1093
+ }
1094
+ break;
1095
+ }
1096
+ case 'gen_finish_record': {
1097
+ // Create generating_finish_evt event using persisted genseq directly
1098
+ const genFinishWireEvent = {
1099
+ type: 'generating_finish_evt',
1100
+ round,
1101
+ genseq: event.genseq,
1102
+ llmGenModel: typeof event.llmGenModel === 'string' ? event.llmGenModel : undefined,
1103
+ dialog: {
1104
+ selfId: dialog.id.selfId,
1105
+ rootId: dialog.id.rootId,
1106
+ },
1107
+ timestamp: event.ts,
1108
+ };
1109
+ // Send directly to WebSocket (NO PubChan emission)
1110
+ if (ws.readyState === 1) {
1111
+ ws.send(JSON.stringify(genFinishWireEvent));
1112
+ }
1113
+ if (event.contextHealth) {
1114
+ const ctxWireEvent = {
1115
+ type: 'context_health_evt',
1116
+ round,
1117
+ genseq: event.genseq,
1118
+ contextHealth: event.contextHealth,
1119
+ dialog: {
1120
+ selfId: dialog.id.selfId,
1121
+ rootId: dialog.id.rootId,
1122
+ },
1123
+ timestamp: event.ts,
1124
+ };
1125
+ if (ws.readyState === 1) {
1126
+ ws.send(JSON.stringify(ctxWireEvent));
1127
+ }
1128
+ }
1129
+ break;
1130
+ }
1131
+ case 'agent_thought_record': {
1132
+ // Replay thinking content as thinking events
1133
+ const content = event.content || '';
1134
+ if (content) {
1135
+ // Start thinking phase
1136
+ const thinkingStartEvent = {
1137
+ type: 'thinking_start_evt',
1138
+ round,
1139
+ genseq: event.genseq,
1140
+ dialog: {
1141
+ selfId: dialog.id.selfId,
1142
+ rootId: dialog.id.rootId,
1143
+ },
1144
+ timestamp: event.ts,
1145
+ };
1146
+ if (ws.readyState === 1) {
1147
+ ws.send(JSON.stringify(thinkingStartEvent));
1148
+ }
1149
+ const thinkingChunks = this.createOptimalChunks(content);
1150
+ for (const chunk of thinkingChunks) {
1151
+ const thinkingChunkEvent = {
1152
+ type: 'thinking_chunk_evt',
1153
+ chunk,
1154
+ round,
1155
+ genseq: event.genseq,
1156
+ dialog: {
1157
+ selfId: dialog.id.selfId,
1158
+ rootId: dialog.id.rootId,
1159
+ },
1160
+ timestamp: event.ts,
1161
+ };
1162
+ if (ws.readyState === 1) {
1163
+ ws.send(JSON.stringify(thinkingChunkEvent));
1164
+ }
1165
+ }
1166
+ // Finish thinking phase
1167
+ const thinkingFinishEvent = {
1168
+ type: 'thinking_finish_evt',
1169
+ round,
1170
+ genseq: event.genseq,
1171
+ dialog: {
1172
+ selfId: dialog.id.selfId,
1173
+ rootId: dialog.id.rootId,
1174
+ },
1175
+ timestamp: event.ts,
1176
+ };
1177
+ if (ws.readyState === 1) {
1178
+ ws.send(JSON.stringify(thinkingFinishEvent));
1179
+ }
1180
+ }
1181
+ break;
1182
+ }
1183
+ case 'agent_words_record': {
1184
+ // Replay assistant text using ad-hoc event receiver with closure-based WebSocket access
1185
+ const content = event.content || '';
1186
+ if (content) {
1187
+ // Create ad-hoc receiver similar to driver pattern with closure-based WebSocket access
1188
+ const receiver = {
1189
+ markdownStart: async () => {
1190
+ if (ws.readyState === 1) {
1191
+ ws.send(JSON.stringify({
1192
+ type: 'markdown_start_evt',
1193
+ round,
1194
+ genseq: event.genseq,
1195
+ dialog: {
1196
+ selfId: dialog.id.selfId,
1197
+ rootId: dialog.id.rootId,
1198
+ },
1199
+ timestamp: event.ts,
1200
+ }));
1201
+ }
1202
+ },
1203
+ markdownChunk: async (chunk) => {
1204
+ if (ws.readyState === 1) {
1205
+ ws.send(JSON.stringify({
1206
+ type: 'markdown_chunk_evt',
1207
+ chunk,
1208
+ round,
1209
+ genseq: event.genseq,
1210
+ dialog: {
1211
+ selfId: dialog.id.selfId,
1212
+ rootId: dialog.id.rootId,
1213
+ },
1214
+ timestamp: event.ts,
1215
+ }));
1216
+ }
1217
+ },
1218
+ markdownFinish: async () => {
1219
+ if (ws.readyState === 1) {
1220
+ ws.send(JSON.stringify({
1221
+ type: 'markdown_finish_evt',
1222
+ round,
1223
+ genseq: event.genseq,
1224
+ dialog: {
1225
+ selfId: dialog.id.selfId,
1226
+ rootId: dialog.id.rootId,
1227
+ },
1228
+ timestamp: event.ts,
1229
+ }));
1230
+ }
1231
+ },
1232
+ callStart: async (validation) => {
1233
+ if (ws.readyState === 1) {
1234
+ ws.send(JSON.stringify({
1235
+ type: 'tool_call_start_evt',
1236
+ validation,
1237
+ round,
1238
+ genseq: event.genseq,
1239
+ dialog: {
1240
+ selfId: dialog.id.selfId,
1241
+ rootId: dialog.id.rootId,
1242
+ },
1243
+ timestamp: event.ts,
1244
+ }));
1245
+ }
1246
+ },
1247
+ callHeadLineChunk: async (chunk) => {
1248
+ if (ws.readyState === 1) {
1249
+ ws.send(JSON.stringify({
1250
+ type: 'tool_call_headline_chunk_evt',
1251
+ chunk,
1252
+ round,
1253
+ genseq: event.genseq,
1254
+ dialog: {
1255
+ selfId: dialog.id.selfId,
1256
+ rootId: dialog.id.rootId,
1257
+ },
1258
+ timestamp: event.ts,
1259
+ }));
1260
+ }
1261
+ },
1262
+ callHeadLineFinish: async () => {
1263
+ if (ws.readyState === 1) {
1264
+ ws.send(JSON.stringify({
1265
+ type: 'tool_call_headline_finish_evt',
1266
+ round,
1267
+ genseq: event.genseq,
1268
+ dialog: {
1269
+ selfId: dialog.id.selfId,
1270
+ rootId: dialog.id.rootId,
1271
+ },
1272
+ timestamp: event.ts,
1273
+ }));
1274
+ }
1275
+ },
1276
+ callBodyStart: async () => {
1277
+ if (ws.readyState === 1) {
1278
+ ws.send(JSON.stringify({
1279
+ type: 'tool_call_body_start_evt',
1280
+ round,
1281
+ genseq: event.genseq,
1282
+ dialog: {
1283
+ selfId: dialog.id.selfId,
1284
+ rootId: dialog.id.rootId,
1285
+ },
1286
+ timestamp: event.ts,
1287
+ }));
1288
+ }
1289
+ },
1290
+ callBodyChunk: async (chunk) => {
1291
+ if (ws.readyState === 1) {
1292
+ ws.send(JSON.stringify({
1293
+ type: 'tool_call_body_chunk_evt',
1294
+ chunk,
1295
+ round,
1296
+ genseq: event.genseq,
1297
+ dialog: {
1298
+ selfId: dialog.id.selfId,
1299
+ rootId: dialog.id.rootId,
1300
+ },
1301
+ timestamp: event.ts,
1302
+ }));
1303
+ }
1304
+ },
1305
+ callBodyFinish: async () => {
1306
+ if (ws.readyState === 1) {
1307
+ ws.send(JSON.stringify({
1308
+ type: 'tool_call_body_finish_evt',
1309
+ round,
1310
+ genseq: event.genseq,
1311
+ dialog: {
1312
+ selfId: dialog.id.selfId,
1313
+ rootId: dialog.id.rootId,
1314
+ },
1315
+ timestamp: event.ts,
1316
+ }));
1317
+ }
1318
+ },
1319
+ callFinish: async (callId) => {
1320
+ if (ws.readyState === 1) {
1321
+ ws.send(JSON.stringify({
1322
+ type: 'tool_call_finish_evt',
1323
+ callId,
1324
+ round,
1325
+ genseq: event.genseq,
1326
+ dialog: {
1327
+ selfId: dialog.id.selfId,
1328
+ rootId: dialog.id.rootId,
1329
+ },
1330
+ timestamp: event.ts,
1331
+ }));
1332
+ }
1333
+ },
1334
+ };
1335
+ // Use the same TellaskStreamParser that live streaming uses
1336
+ const streamingParser = new tellask_1.TellaskStreamParser(receiver);
1337
+ // Stream the content through the parser to ensure consistent event emission
1338
+ await streamingParser.takeUpstreamChunk(content);
1339
+ await streamingParser.finalize();
1340
+ }
1341
+ break;
1342
+ }
1343
+ case 'func_call_record': {
1344
+ // Handle function call events from persistence
1345
+ // NOTE: func_call_evt REMOVED - emit func_call_requested_evt for UI instead
1346
+ const funcCall = {
1347
+ type: 'func_call_requested_evt',
1348
+ funcId: event.id,
1349
+ funcName: event.name,
1350
+ arguments: JSON.stringify(event.arguments),
1351
+ round,
1352
+ genseq: event.genseq,
1353
+ dialog: {
1354
+ selfId: dialog.id.selfId,
1355
+ rootId: dialog.id.rootId,
1356
+ },
1357
+ timestamp: event.ts,
1358
+ };
1359
+ if (ws.readyState === 1) {
1360
+ ws.send(JSON.stringify(funcCall));
1361
+ }
1362
+ break;
1363
+ }
1364
+ case 'func_result_record': {
1365
+ // Handle function result events from persistence
1366
+ const funcResult = {
1367
+ type: 'func_result_evt',
1368
+ id: event.id,
1369
+ name: event.name,
1370
+ content: event.content,
1371
+ round,
1372
+ dialog: {
1373
+ selfId: dialog.id.selfId,
1374
+ rootId: dialog.id.rootId,
1375
+ },
1376
+ timestamp: event.ts,
1377
+ };
1378
+ if (ws.readyState === 1) {
1379
+ ws.send(JSON.stringify(funcResult));
1380
+ }
1381
+ break;
1382
+ }
1383
+ case 'quest_for_sup_record': {
1384
+ // Handle subdialog creation requests
1385
+ const subdialogCreatedEvent = {
1386
+ type: 'subdialog_created_evt',
1387
+ dialog: {
1388
+ // Add dialog field for proper event routing
1389
+ selfId: event.subDialogId,
1390
+ rootId: dialog.id.rootId,
1391
+ },
1392
+ parentDialog: {
1393
+ selfId: dialog.id.selfId,
1394
+ rootId: dialog.id.rootId,
1395
+ },
1396
+ subDialog: {
1397
+ selfId: event.subDialogId,
1398
+ rootId: dialog.id.rootId, // Use parent's rootId for subdialog's rootId
1399
+ },
1400
+ targetAgentId: 'unknown', // Will be resolved during actual subdialog creation
1401
+ headLine: event.headLine,
1402
+ callBody: event.callBody,
1403
+ timestamp: event.ts,
1404
+ };
1405
+ if (ws.readyState === 1) {
1406
+ ws.send(JSON.stringify(subdialogCreatedEvent));
1407
+ }
1408
+ break;
1409
+ }
1410
+ case 'tool_call_result_record': {
1411
+ // Handle tool call results
1412
+ const responseEvent = {
1413
+ type: 'tool_call_response_evt',
1414
+ responderId: event.responderId,
1415
+ headLine: event.headLine,
1416
+ status: event.status,
1417
+ result: event.result,
1418
+ callId: event.callId || '',
1419
+ round,
1420
+ calling_genseq: event.calling_genseq,
1421
+ dialog: {
1422
+ selfId: dialog.id.selfId,
1423
+ rootId: dialog.id.rootId,
1424
+ },
1425
+ timestamp: event.ts,
1426
+ };
1427
+ if (ws.readyState === 1) {
1428
+ ws.send(JSON.stringify(responseEvent));
1429
+ }
1430
+ break;
1431
+ }
1432
+ case 'teammate_response_record': {
1433
+ // Handle teammate response events (separate bubble for @teammate tellasks)
1434
+ const formattedResult = (0, inter_dialog_format_1.formatTeammateResponseContent)({
1435
+ responderId: event.responderId,
1436
+ requesterId: event.originMemberId,
1437
+ originalCallHeadLine: event.headLine,
1438
+ responseBody: event.response,
1439
+ language: (0, runtime_language_1.getWorkLanguage)(),
1440
+ });
1441
+ const teammateResponseEvent = {
1442
+ type: 'teammate_response_evt',
1443
+ responderId: event.responderId,
1444
+ calleeDialogId: event.calleeDialogId,
1445
+ headLine: event.headLine,
1446
+ status: event.status,
1447
+ result: formattedResult,
1448
+ response: event.response,
1449
+ agentId: event.agentId,
1450
+ callId: event.callId,
1451
+ originMemberId: event.originMemberId,
1452
+ round,
1453
+ calling_genseq: event.calling_genseq,
1454
+ dialog: {
1455
+ selfId: dialog.id.selfId,
1456
+ rootId: dialog.id.rootId,
1457
+ },
1458
+ timestamp: event.ts,
1459
+ };
1460
+ if (ws.readyState === 1) {
1461
+ ws.send(JSON.stringify(teammateResponseEvent));
1462
+ }
1463
+ break;
1464
+ }
1465
+ default:
1466
+ // Unknown event type - log but don't crash
1467
+ log_1.log.warn(`Unknown persistence event type during direct WebSocket send`, undefined, event);
1468
+ break;
1469
+ }
1470
+ }
1471
+ /**
1472
+ * Create optimal text chunks for websocket transmission
1473
+ * Splits content into 1MB pieces for efficient websocket streaming
1474
+ */
1475
+ createOptimalChunks(content, maxChunk = 1000000) {
1476
+ const chunks = [];
1477
+ let remaining = content.trim();
1478
+ while (remaining.length > 0) {
1479
+ // Use 1MB chunks for optimal websocket transmission
1480
+ const targetSize = Math.min(remaining.length, maxChunk);
1481
+ const chunk = remaining.slice(0, targetSize);
1482
+ chunks.push(chunk);
1483
+ remaining = remaining.slice(chunk.length).trim();
1484
+ }
1485
+ return chunks.filter((chunk) => chunk.length > 0);
1486
+ }
1487
+ }
1488
+ exports.DiskFileDialogStore = DiskFileDialogStore;
1489
+ /**
1490
+ * Utility class for managing dialog persistence
1491
+ */
1492
+ class DialogPersistence {
1493
+ static getLatestWriteBackMutex(key) {
1494
+ const existing = this.latestWriteBackMutexes.get(key);
1495
+ if (existing)
1496
+ return existing;
1497
+ const created = new async_fifo_mutex_1.AsyncFifoMutex();
1498
+ this.latestWriteBackMutexes.set(key, created);
1499
+ return created;
1500
+ }
1501
+ static getLatestWriteBackKey(dialogId, status) {
1502
+ // Include dialogs root dir to avoid cross-test/process.cwd collisions.
1503
+ return `${this.getDialogsRootDir()}|${status}|${dialogId.valueOf()}`;
1504
+ }
1505
+ /**
1506
+ * Get the base dialogs directory path
1507
+ */
1508
+ static getDialogsRootDir() {
1509
+ return path.join(process.cwd(), this.DIALOGS_DIR);
1510
+ }
1511
+ /**
1512
+ * Save dialog state to JSON file for persistence (internal use only)
1513
+ */
1514
+ static async saveDialogState(state) {
1515
+ try {
1516
+ const dialogPath = await this.ensureRootDialogDirectory(new dialog_1.DialogID(state.metadata.id));
1517
+ // Save state as JSON file
1518
+ const stateFile = path.join(dialogPath, 'state.json');
1519
+ await fs.promises.writeFile(stateFile, JSON.stringify({
1520
+ metadata: state.metadata,
1521
+ currentRound: state.currentRound,
1522
+ messages: state.messages,
1523
+ reminders: state.reminders,
1524
+ savedAt: (0, time_1.formatUnifiedTimestamp)(new Date()),
1525
+ }, null, 2), 'utf-8');
1526
+ }
1527
+ catch (error) {
1528
+ log_1.log.error(`Failed to save dialog state for ${state.metadata.id}:`, error);
1529
+ throw error;
1530
+ }
1531
+ }
1532
+ /**
1533
+ * Load dialog state from JSON file
1534
+ */
1535
+ static async loadDialogState(dialogId) {
1536
+ try {
1537
+ const dialogPath = this.getRootDialogPath(dialogId, 'running');
1538
+ const stateFile = path.join(dialogPath, 'state.json');
1539
+ // Check if state file exists
1540
+ try {
1541
+ await fs.promises.access(stateFile);
1542
+ }
1543
+ catch {
1544
+ log_1.log.warn(`No state file found for dialog ${dialogId.selfId}, returning null`);
1545
+ return null;
1546
+ }
1547
+ const stateData = JSON.parse(await fs.promises.readFile(stateFile, 'utf-8'));
1548
+ return {
1549
+ metadata: stateData.metadata,
1550
+ currentRound: stateData.currentRound,
1551
+ messages: stateData.messages,
1552
+ reminders: stateData.reminders || [],
1553
+ };
1554
+ }
1555
+ catch (error) {
1556
+ log_1.log.error(`Failed to load dialog state for root ${dialogId.selfId}:`, error);
1557
+ return null;
1558
+ }
1559
+ }
1560
+ /**
1561
+ * Get the full path for a dialog directory
1562
+ */
1563
+ static getRootDialogPath(dialogId, status = 'running') {
1564
+ if (dialogId.rootId !== dialogId.selfId) {
1565
+ throw new Error('Expected root dialog id');
1566
+ }
1567
+ let statusDir;
1568
+ if (status === 'running') {
1569
+ statusDir = this.RUN_DIR;
1570
+ }
1571
+ else if (status === 'completed') {
1572
+ statusDir = this.DONE_DIR;
1573
+ }
1574
+ else {
1575
+ statusDir = this.ARCHIVE_DIR;
1576
+ }
1577
+ return path.join(this.getDialogsRootDir(), statusDir, dialogId.selfId);
1578
+ }
1579
+ /**
1580
+ * Get the events/state directory for a dialog (composite ID for subdialogs)
1581
+ */
1582
+ static getDialogEventsPath(dialogId, status = 'running') {
1583
+ // Root dialogs store events under their own directory.
1584
+ // Subdialogs store events under the root's subdialogs/<self> directory.
1585
+ if (dialogId.rootId === dialogId.selfId) {
1586
+ return this.getRootDialogPath(dialogId, status);
1587
+ }
1588
+ return this.getSubdialogPath(dialogId, status);
1589
+ }
1590
+ /**
1591
+ * Get the path for a subdialog within a supdialog
1592
+ */
1593
+ static getSubdialogPath(dialogId, status = 'running') {
1594
+ if (dialogId.rootId === dialogId.selfId) {
1595
+ throw new Error('Expected subdialog id (self differs from root)');
1596
+ }
1597
+ const rootPath = this.getRootDialogPath(new dialog_1.DialogID(dialogId.rootId), status);
1598
+ return path.join(rootPath, this.SUBDIALOGS_DIR, dialogId.selfId);
1599
+ }
1600
+ /**
1601
+ * Ensure dialog directory structure exists
1602
+ */
1603
+ static async ensureRootDialogDirectory(dialogId, status = 'running') {
1604
+ const dialogPath = this.getRootDialogPath(dialogId, status);
1605
+ try {
1606
+ await fs.promises.mkdir(dialogPath, { recursive: true });
1607
+ return dialogPath;
1608
+ }
1609
+ catch (error) {
1610
+ log_1.log.error(`Failed to create dialog directory ${dialogPath}:`, error);
1611
+ throw error;
1612
+ }
1613
+ }
1614
+ /**
1615
+ * Ensure subdialog directory structure exists
1616
+ */
1617
+ static async ensureSubdialogDirectory(dialogId, status = 'running') {
1618
+ const subdialogPath = this.getSubdialogPath(dialogId, status);
1619
+ try {
1620
+ await fs.promises.mkdir(subdialogPath, { recursive: true });
1621
+ return subdialogPath;
1622
+ }
1623
+ catch (error) {
1624
+ log_1.log.error(`Failed to create subdialog directory ${subdialogPath}:`, error);
1625
+ throw error;
1626
+ }
1627
+ }
1628
+ /**
1629
+ * Mark a dialog as completed
1630
+ */
1631
+ static async markDialogCompleted(dialogId) {
1632
+ try {
1633
+ const dialogPath = this.getRootDialogPath(dialogId, 'running');
1634
+ const completedPath = this.getRootDialogPath(dialogId, 'completed');
1635
+ await fs.promises.mkdir(completedPath, { recursive: true });
1636
+ // Move files from current to completed
1637
+ const files = await fs.promises.readdir(dialogPath);
1638
+ for (const file of files) {
1639
+ const src = path.join(dialogPath, file);
1640
+ const dest = path.join(completedPath, file);
1641
+ await fs.promises.rename(src, dest);
1642
+ }
1643
+ }
1644
+ catch (error) {
1645
+ log_1.log.error(`Failed to mark dialog ${dialogId} as completed:`, error);
1646
+ throw error;
1647
+ }
1648
+ }
1649
+ /**
1650
+ * List all dialog IDs by scanning for dialog.yaml files and validating their IDs
1651
+ */
1652
+ static async listDialogs(status = 'running') {
1653
+ try {
1654
+ const statusDir = this.getDialogsRootDir();
1655
+ const specificDir = path.join(statusDir, status === 'running'
1656
+ ? this.RUN_DIR
1657
+ : status === 'completed'
1658
+ ? this.DONE_DIR
1659
+ : this.ARCHIVE_DIR);
1660
+ const validDialogIds = [];
1661
+ // Recursively find all dialog.yaml files
1662
+ const findDialogYamls = async (dirPath, relativePath = '') => {
1663
+ try {
1664
+ const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
1665
+ for (const entry of entries) {
1666
+ const fullPath = path.join(dirPath, entry.name);
1667
+ const entryRelativePath = path.join(relativePath, entry.name);
1668
+ if (entry.isDirectory()) {
1669
+ // Recursively search subdirectories
1670
+ await findDialogYamls(fullPath, entryRelativePath);
1671
+ }
1672
+ else if (entry.name === 'dialog.yaml') {
1673
+ // Found a dialog.yaml file, record its ID regardless of nesting structure
1674
+ try {
1675
+ const content = await fs.promises.readFile(fullPath, 'utf-8');
1676
+ const parsed = yaml.parse(content);
1677
+ if (parsed?.id && typeof parsed.id === 'string') {
1678
+ validDialogIds.push(parsed.id);
1679
+ }
1680
+ }
1681
+ catch (yamlError) {
1682
+ log_1.log.warn(`🔍 listDialogs: Failed to parse dialog.yaml at ${fullPath}:`, yamlError);
1683
+ }
1684
+ }
1685
+ }
1686
+ }
1687
+ catch (error) {
1688
+ log_1.log.warn(`🔍 listDialogs: Error reading directory ${dirPath}:`, error);
1689
+ }
1690
+ };
1691
+ try {
1692
+ // Check if directory exists before trying to read it
1693
+ const dirExists = await fs.promises
1694
+ .stat(specificDir)
1695
+ .then(() => true)
1696
+ .catch(() => false);
1697
+ if (dirExists) {
1698
+ await findDialogYamls(specificDir);
1699
+ }
1700
+ return validDialogIds;
1701
+ }
1702
+ catch (error) {
1703
+ log_1.log.warn(`🔍 listDialogs: Error processing directory ${specificDir}:`, error instanceof Error ? error.message : String(error));
1704
+ return [];
1705
+ }
1706
+ }
1707
+ catch (error) {
1708
+ log_1.log.error('Failed to list dialogs:', error);
1709
+ return [];
1710
+ }
1711
+ }
1712
+ /**
1713
+ * List all dialog IDs (root + subdialogs) together with their root IDs.
1714
+ * This is the only safe way to enumerate subdialogs because their directory names
1715
+ * are not guaranteed to be their selfId.
1716
+ */
1717
+ static async listAllDialogIds(status = 'running') {
1718
+ const statusDir = this.getDialogsRootDir();
1719
+ const specificDir = path.join(statusDir, status === 'running'
1720
+ ? this.RUN_DIR
1721
+ : status === 'completed'
1722
+ ? this.DONE_DIR
1723
+ : this.ARCHIVE_DIR);
1724
+ const result = [];
1725
+ const rootDialogIdByDialogYamlPath = new Map();
1726
+ const readDialogYamlId = async (dialogYamlPath) => {
1727
+ const cached = rootDialogIdByDialogYamlPath.get(dialogYamlPath);
1728
+ if (cached !== undefined)
1729
+ return cached;
1730
+ try {
1731
+ const content = await fs.promises.readFile(dialogYamlPath, 'utf-8');
1732
+ const parsed = yaml.parse(content);
1733
+ if (typeof parsed !== 'object' || parsed === null) {
1734
+ rootDialogIdByDialogYamlPath.set(dialogYamlPath, null);
1735
+ return null;
1736
+ }
1737
+ const idValue = parsed.id;
1738
+ if (typeof idValue !== 'string' || idValue.trim() === '') {
1739
+ rootDialogIdByDialogYamlPath.set(dialogYamlPath, null);
1740
+ return null;
1741
+ }
1742
+ const normalized = idValue.trim();
1743
+ rootDialogIdByDialogYamlPath.set(dialogYamlPath, normalized);
1744
+ return normalized;
1745
+ }
1746
+ catch {
1747
+ rootDialogIdByDialogYamlPath.set(dialogYamlPath, null);
1748
+ return null;
1749
+ }
1750
+ };
1751
+ const inferRootIdFromRelativeDir = async (relativeDir) => {
1752
+ const dir = relativeDir.trim();
1753
+ if (dir === '' || dir === '.' || dir === path.sep)
1754
+ return null;
1755
+ const segments = dir.split(path.sep).filter((seg) => seg.length > 0 && seg !== '.');
1756
+ if (segments.length === 0)
1757
+ return null;
1758
+ // Root dialog IDs in this repo can contain path separators (e.g. "f4/44/cd85c4e2").
1759
+ // The root dialog directory is therefore nested (RUN_DIR/<rootId>/dialog.yaml).
1760
+ //
1761
+ // To infer the rootId for any dialog.yaml we find (root or subdialog), scan prefixes of the
1762
+ // directory path and pick the first prefix that is itself a valid root dialog directory:
1763
+ // - it has a dialog.yaml
1764
+ // - its dialog.yaml id matches the prefix joined with '/'
1765
+ for (let i = 1; i <= segments.length; i++) {
1766
+ const prefixSegs = segments.slice(0, i);
1767
+ const candidateDialogYamlPath = path.join(specificDir, ...prefixSegs, 'dialog.yaml');
1768
+ const id = await readDialogYamlId(candidateDialogYamlPath);
1769
+ const expectedId = prefixSegs.join('/');
1770
+ if (id === expectedId)
1771
+ return expectedId;
1772
+ }
1773
+ return null;
1774
+ };
1775
+ const findDialogYamls = async (dirPath, relativePath = '') => {
1776
+ let entries;
1777
+ try {
1778
+ entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
1779
+ }
1780
+ catch (err) {
1781
+ log_1.log.warn(`🔍 listAllDialogIds: Error reading directory ${dirPath}:`, err);
1782
+ return;
1783
+ }
1784
+ for (const entry of entries) {
1785
+ const fullPath = path.join(dirPath, entry.name);
1786
+ const entryRelativePath = relativePath ? path.join(relativePath, entry.name) : entry.name;
1787
+ if (entry.isDirectory()) {
1788
+ await findDialogYamls(fullPath, entryRelativePath);
1789
+ continue;
1790
+ }
1791
+ if (entry.name !== 'dialog.yaml')
1792
+ continue;
1793
+ const relDir = path.dirname(entryRelativePath);
1794
+ const rootId = await inferRootIdFromRelativeDir(relDir);
1795
+ if (!rootId)
1796
+ continue;
1797
+ try {
1798
+ const content = await fs.promises.readFile(fullPath, 'utf-8');
1799
+ const parsed = yaml.parse(content);
1800
+ if (typeof parsed !== 'object' || parsed === null)
1801
+ continue;
1802
+ const idValue = parsed.id;
1803
+ if (typeof idValue !== 'string' || idValue.trim() === '')
1804
+ continue;
1805
+ result.push(new dialog_1.DialogID(idValue, rootId));
1806
+ }
1807
+ catch (yamlError) {
1808
+ log_1.log.warn(`🔍 listAllDialogIds: Failed to parse dialog.yaml at ${fullPath}:`, yamlError);
1809
+ }
1810
+ }
1811
+ };
1812
+ const dirExists = await fs.promises
1813
+ .stat(specificDir)
1814
+ .then(() => true)
1815
+ .catch(() => false);
1816
+ if (!dirExists)
1817
+ return [];
1818
+ await findDialogYamls(specificDir);
1819
+ return result;
1820
+ }
1821
+ // === NEW JSONL ROUND-BASED METHODS ===
1822
+ /**
1823
+ * Append event to round JSONL file (append-only pattern)
1824
+ */
1825
+ static async appendEvent(dialogId, round, event, status = 'running') {
1826
+ try {
1827
+ const dialogPath = this.getDialogEventsPath(dialogId, status);
1828
+ const roundFilename = this.getRoundFilename(round);
1829
+ const roundFilePath = path.join(dialogPath, roundFilename);
1830
+ // Ensure directory exists
1831
+ await fs.promises.mkdir(dialogPath, { recursive: true });
1832
+ // Atomic append operation
1833
+ const eventLine = JSON.stringify(event) + '\n';
1834
+ await fs.promises.appendFile(roundFilePath, eventLine, 'utf-8');
1835
+ // Update latest.yaml with new lastModified timestamp
1836
+ await this.mutateDialogLatest(dialogId, () => ({
1837
+ kind: 'patch',
1838
+ patch: {
1839
+ lastModified: (0, time_1.formatUnifiedTimestamp)(new Date()),
1840
+ currentRound: round,
1841
+ },
1842
+ }), status);
1843
+ }
1844
+ catch (error) {
1845
+ log_1.log.error(`Failed to append event to dialog ${dialogId} round ${round}:`, error);
1846
+ throw error;
1847
+ }
1848
+ }
1849
+ /**
1850
+ * Read all events from round JSONL file
1851
+ */
1852
+ static async readRoundEvents(dialogId, round, status = 'running') {
1853
+ try {
1854
+ const dialogPath = this.getDialogEventsPath(dialogId, status);
1855
+ const roundFilename = this.getRoundFilename(round);
1856
+ const roundFilePath = path.join(dialogPath, roundFilename);
1857
+ try {
1858
+ const content = await fs.promises.readFile(roundFilePath, 'utf-8');
1859
+ const events = [];
1860
+ for (const line of content.trim().split('\n')) {
1861
+ if (line.trim()) {
1862
+ events.push(JSON.parse(line));
1863
+ }
1864
+ }
1865
+ return events;
1866
+ }
1867
+ catch (error) {
1868
+ if (getErrorCode(error) === 'ENOENT') {
1869
+ // Round file doesn't exist - return empty array
1870
+ return [];
1871
+ }
1872
+ throw error;
1873
+ }
1874
+ }
1875
+ catch (error) {
1876
+ log_1.log.error(`Failed to read round events for dialog ${dialogId} round ${round}:`, error);
1877
+ throw error;
1878
+ }
1879
+ }
1880
+ /**
1881
+ * Compute next sequence number for a round by scanning existing events
1882
+ */
1883
+ static async getNextSeq(dialogId, round, status = 'running') {
1884
+ const events = await this.readRoundEvents(dialogId, round, status);
1885
+ let maxSeq = 0;
1886
+ for (const ev of events) {
1887
+ if ('genseq' in ev && typeof ev.genseq === 'number' && ev.genseq > maxSeq) {
1888
+ maxSeq = ev.genseq;
1889
+ }
1890
+ }
1891
+ return maxSeq + 1;
1892
+ }
1893
+ /**
1894
+ * Get current round number from latest.yaml (performance optimization)
1895
+ * UI navigation can assume natural numbering schema back to 1
1896
+ */
1897
+ static async getCurrentRoundNumber(dialogId, status = 'running') {
1898
+ try {
1899
+ const latest = await this.loadDialogLatest(dialogId, status);
1900
+ return latest?.currentRound || 1;
1901
+ }
1902
+ catch (error) {
1903
+ log_1.log.error(`Failed to get current round for dialog ${dialogId}:`, error);
1904
+ return 1;
1905
+ }
1906
+ }
1907
+ /**
1908
+ * Save reminder state (exceptional overwrite pattern) (internal use only)
1909
+ */
1910
+ static async _saveReminderState(dialogId, reminders, status = 'running') {
1911
+ try {
1912
+ const dialogPath = this.getDialogEventsPath(dialogId, status);
1913
+ await fs.promises.mkdir(dialogPath, { recursive: true });
1914
+ const remindersFilePath = path.join(dialogPath, 'reminders.json');
1915
+ const reminderState = {
1916
+ reminders: reminders.map((r, index) => ({
1917
+ id: `reminder-${index}`,
1918
+ content: r.content,
1919
+ ownerName: r.owner ? r.owner.name : undefined,
1920
+ meta: r.meta,
1921
+ createdAt: (0, time_1.formatUnifiedTimestamp)(new Date()),
1922
+ priority: 'medium',
1923
+ })),
1924
+ updatedAt: (0, time_1.formatUnifiedTimestamp)(new Date()),
1925
+ };
1926
+ // Atomic write operation
1927
+ const tempFile = `${remindersFilePath}.${process.pid}.${Date.now()}.tmp`;
1928
+ await fs.promises.writeFile(tempFile, JSON.stringify(reminderState, null, 2), 'utf-8');
1929
+ await fs.promises.rename(tempFile, remindersFilePath);
1930
+ }
1931
+ catch (error) {
1932
+ log_1.log.error(`Failed to save reminder state for dialog ${dialogId}:`, error);
1933
+ throw error;
1934
+ }
1935
+ }
1936
+ /**
1937
+ * Load reminder state
1938
+ */
1939
+ static async loadReminderState(dialogId, status = 'running') {
1940
+ try {
1941
+ const dialogPath = this.getDialogEventsPath(dialogId, status);
1942
+ const remindersFilePath = path.join(dialogPath, 'reminders.json');
1943
+ try {
1944
+ const content = await fs.promises.readFile(remindersFilePath, 'utf-8');
1945
+ const reminderState = JSON.parse(content);
1946
+ return reminderState.reminders.map((r) => {
1947
+ const ownerNameFromFile = typeof r.ownerName === 'string' ? r.ownerName : undefined;
1948
+ const owner = ownerNameFromFile ? (0, registry_1.getReminderOwner)(ownerNameFromFile) : undefined;
1949
+ return {
1950
+ id: r.id,
1951
+ content: r.content,
1952
+ owner,
1953
+ meta: r.meta,
1954
+ createdAt: r.createdAt,
1955
+ priority: r.priority,
1956
+ };
1957
+ });
1958
+ }
1959
+ catch (error) {
1960
+ if (getErrorCode(error) === 'ENOENT') {
1961
+ // reminders.json doesn't exist - return empty array
1962
+ return [];
1963
+ }
1964
+ throw error;
1965
+ }
1966
+ }
1967
+ catch (error) {
1968
+ log_1.log.error(`Failed to load reminder state for dialog ${dialogId}:`, error);
1969
+ return [];
1970
+ }
1971
+ }
1972
+ /**
1973
+ * Save questions for human state (exceptional overwrite pattern) (internal use only)
1974
+ */
1975
+ static async _saveQuestions4HumanState(dialogId, questions, status = 'running') {
1976
+ try {
1977
+ const dialogPath = this.getDialogEventsPath(dialogId, status);
1978
+ const questionsFilePath = path.join(dialogPath, 'q4h.yaml');
1979
+ const questionsState = {
1980
+ questions,
1981
+ updatedAt: (0, time_1.formatUnifiedTimestamp)(new Date()),
1982
+ };
1983
+ // Atomic write operation
1984
+ const tempFile = questionsFilePath + '.tmp';
1985
+ const yamlContent = yaml.stringify(questionsState);
1986
+ await fs.promises.writeFile(tempFile, yamlContent, 'utf-8');
1987
+ await fs.promises.rename(tempFile, questionsFilePath);
1988
+ }
1989
+ catch (error) {
1990
+ log_1.log.error(`Failed to save q4h.yaml for dialog ${dialogId}:`, error);
1991
+ throw error;
1992
+ }
1993
+ }
1994
+ /**
1995
+ * Load questions for human state
1996
+ */
1997
+ static async loadQuestions4HumanState(dialogId, status = 'running') {
1998
+ try {
1999
+ const dialogPath = this.getDialogEventsPath(dialogId, status);
2000
+ const questionsFilePath = path.join(dialogPath, 'q4h.yaml');
2001
+ try {
2002
+ const content = await fs.promises.readFile(questionsFilePath, 'utf-8');
2003
+ const questionsState = yaml.parse(content);
2004
+ return questionsState.questions.map((q) => ({
2005
+ ...q,
2006
+ kind: normalizeQ4HKind(q.kind),
2007
+ }));
2008
+ }
2009
+ catch (error) {
2010
+ if (getErrorCode(error) === 'ENOENT') {
2011
+ // q4h.yaml doesn't exist - return empty array
2012
+ return [];
2013
+ }
2014
+ throw error;
2015
+ }
2016
+ }
2017
+ catch (error) {
2018
+ log_1.log.error(`Failed to load q4h.yaml for dialog ${dialogId}:`, error);
2019
+ return [];
2020
+ }
2021
+ }
2022
+ /**
2023
+ * Load all Q4H questions from all running dialogs (for global Q4H display)
2024
+ * Returns array of questions with their dialog context for frontend display
2025
+ */
2026
+ static async loadAllQ4HState() {
2027
+ try {
2028
+ // Get all running dialogs (root + subdialogs) with correct rootId association.
2029
+ const dialogIds = await this.listAllDialogIds('running');
2030
+ const allQuestions = [];
2031
+ for (const dialogIdObj of dialogIds) {
2032
+ try {
2033
+ const questions = await this.loadQuestions4HumanState(dialogIdObj, 'running');
2034
+ const metadata = await this.loadDialogMetadata(dialogIdObj, 'running');
2035
+ if (metadata && questions.length > 0) {
2036
+ for (const q of questions) {
2037
+ allQuestions.push({
2038
+ ...q,
2039
+ selfId: dialogIdObj.selfId,
2040
+ rootId: dialogIdObj.rootId,
2041
+ agentId: metadata.agentId,
2042
+ taskDocPath: metadata.taskDocPath,
2043
+ });
2044
+ }
2045
+ }
2046
+ }
2047
+ catch (err) {
2048
+ log_1.log.warn(`Failed to load Q4H for dialog ${dialogIdObj.valueOf()}:`, err);
2049
+ }
2050
+ }
2051
+ return allQuestions;
2052
+ }
2053
+ catch (error) {
2054
+ log_1.log.error('Failed to load all Q4H state:', error);
2055
+ return [];
2056
+ }
2057
+ }
2058
+ static async clearQuestions4HumanState(dialogId, status = 'running') {
2059
+ try {
2060
+ const dialogPath = this.getDialogEventsPath(dialogId, status);
2061
+ const questionsFilePath = path.join(dialogPath, 'q4h.yaml');
2062
+ let previousCount = 0;
2063
+ let existingQuestions = [];
2064
+ try {
2065
+ existingQuestions = await this.loadQuestions4HumanState(dialogId, status);
2066
+ previousCount = existingQuestions.length;
2067
+ }
2068
+ catch (err) {
2069
+ log_1.log.debug('No existing questions state found, using default count', err);
2070
+ }
2071
+ await fs.promises.rm(questionsFilePath, { force: true });
2072
+ // Emit q4h_answered events for each removed question
2073
+ for (const q of existingQuestions) {
2074
+ const answeredEvent = {
2075
+ type: 'q4h_answered',
2076
+ questionId: q.id,
2077
+ selfId: dialogId.selfId,
2078
+ };
2079
+ (0, evt_registry_1.postDialogEventById)(dialogId, answeredEvent);
2080
+ }
2081
+ }
2082
+ catch (error) {
2083
+ log_1.log.error(`Failed to clear q4h.yaml for dialog ${dialogId}:`, error);
2084
+ }
2085
+ }
2086
+ // === PHASE 6: SUBDIALOG SUPPLY PERSISTENCE ===
2087
+ /**
2088
+ * Save pending subdialogs for Type A supply mechanism.
2089
+ * Tracks subdialogs that were created but not yet completed.
2090
+ */
2091
+ static async savePendingSubdialogs(rootDialogId, pendingSubdialogs, status = 'running') {
2092
+ try {
2093
+ const dialogPath = this.getDialogResponsesPath(rootDialogId, status);
2094
+ await fs.promises.mkdir(dialogPath, { recursive: true });
2095
+ const filePath = path.join(dialogPath, 'pending-subdialogs.json');
2096
+ // Atomic write operation
2097
+ const tempFile = filePath + '.tmp';
2098
+ await fs.promises.writeFile(tempFile, JSON.stringify(pendingSubdialogs, null, 2), 'utf-8');
2099
+ await fs.promises.rename(tempFile, filePath);
2100
+ }
2101
+ catch (error) {
2102
+ log_1.log.error(`Failed to save pending subdialogs for dialog ${rootDialogId}:`, error);
2103
+ throw error;
2104
+ }
2105
+ }
2106
+ /**
2107
+ * Load pending subdialogs for Type A supply mechanism.
2108
+ */
2109
+ static async loadPendingSubdialogs(rootDialogId, status = 'running') {
2110
+ try {
2111
+ const dialogPath = this.getDialogResponsesPath(rootDialogId, status);
2112
+ const filePath = path.join(dialogPath, 'pending-subdialogs.json');
2113
+ try {
2114
+ const content = await fs.promises.readFile(filePath, 'utf-8');
2115
+ return JSON.parse(content);
2116
+ }
2117
+ catch (error) {
2118
+ if (getErrorCode(error) === 'ENOENT') {
2119
+ return [];
2120
+ }
2121
+ throw error;
2122
+ }
2123
+ }
2124
+ catch (error) {
2125
+ log_1.log.error(`Failed to load pending subdialogs for dialog ${rootDialogId}:`, error);
2126
+ return [];
2127
+ }
2128
+ }
2129
+ /**
2130
+ * Get the path for storing subdialog responses (supports both root and subdialog parents).
2131
+ * For Type C subdialogs created inside another subdialog, responses are stored at the parent's level.
2132
+ */
2133
+ static getDialogResponsesPath(dialogId, status = 'running') {
2134
+ // Root dialogs store responses in their own directory.
2135
+ // Subdialogs store responses in the parent's location (root or subdialog).
2136
+ if (dialogId.rootId === dialogId.selfId) {
2137
+ // Root dialog: use root's directory
2138
+ return this.getRootDialogPath(dialogId, status);
2139
+ }
2140
+ // Subdialog: store in parent's subdialogs directory
2141
+ // The parent is always identified by rootId (could be root or parent subdialog)
2142
+ const parentSelfId = dialogId.rootId;
2143
+ const rootPath = this.getRootDialogPath(new dialog_1.DialogID(parentSelfId), status);
2144
+ return path.join(rootPath, this.SUBDIALOGS_DIR, dialogId.selfId);
2145
+ }
2146
+ /**
2147
+ * Save subdialog responses for Type A supply mechanism.
2148
+ * Tracks responses from completed subdialogs.
2149
+ */
2150
+ static async saveSubdialogResponses(rootDialogId, responses, status = 'running') {
2151
+ try {
2152
+ const dialogPath = this.getDialogResponsesPath(rootDialogId, status);
2153
+ await fs.promises.mkdir(dialogPath, { recursive: true });
2154
+ const filePath = path.join(dialogPath, 'subdialog-responses.json');
2155
+ // Atomic write operation
2156
+ const tempFile = filePath + '.tmp';
2157
+ await fs.promises.writeFile(tempFile, JSON.stringify(responses, null, 2), 'utf-8');
2158
+ await fs.promises.rename(tempFile, filePath);
2159
+ }
2160
+ catch (error) {
2161
+ log_1.log.error(`Failed to save subdialog responses for dialog ${rootDialogId}:`, error);
2162
+ throw error;
2163
+ }
2164
+ }
2165
+ /**
2166
+ * Load subdialog responses for Type A supply mechanism.
2167
+ */
2168
+ static async loadSubdialogResponses(rootDialogId, status = 'running') {
2169
+ try {
2170
+ const dialogPath = this.getDialogResponsesPath(rootDialogId, status);
2171
+ const filePath = path.join(dialogPath, 'subdialog-responses.json');
2172
+ const inflightPath = path.join(dialogPath, 'subdialog-responses.processing.json');
2173
+ try {
2174
+ const results = [];
2175
+ const tryReadArray = async (p) => {
2176
+ try {
2177
+ const content = await fs.promises.readFile(p, 'utf-8');
2178
+ const parsed = JSON.parse(content);
2179
+ return Array.isArray(parsed) ? parsed : [];
2180
+ }
2181
+ catch (error) {
2182
+ if (getErrorCode(error) === 'ENOENT') {
2183
+ return [];
2184
+ }
2185
+ throw error;
2186
+ }
2187
+ };
2188
+ const primary = await tryReadArray(filePath);
2189
+ const inflight = await tryReadArray(inflightPath);
2190
+ for (const item of [...primary, ...inflight]) {
2191
+ if (isSubdialogResponseRecord(item)) {
2192
+ results.push(item);
2193
+ }
2194
+ }
2195
+ // Deduplicate by responseId (primary wins over inflight order is irrelevant)
2196
+ const byId = new Map();
2197
+ for (const r of results) {
2198
+ byId.set(r.responseId, r);
2199
+ }
2200
+ return Array.from(byId.values());
2201
+ }
2202
+ catch (error) {
2203
+ if (getErrorCode(error) === 'ENOENT') {
2204
+ return [];
2205
+ }
2206
+ throw error;
2207
+ }
2208
+ }
2209
+ catch (error) {
2210
+ log_1.log.error(`Failed to load subdialog responses for dialog ${rootDialogId}:`, error);
2211
+ return [];
2212
+ }
2213
+ }
2214
+ static async loadSubdialogResponsesQueue(dialogId, status = 'running') {
2215
+ try {
2216
+ const dialogPath = this.getDialogResponsesPath(dialogId, status);
2217
+ const filePath = path.join(dialogPath, 'subdialog-responses.json');
2218
+ const content = await fs.promises.readFile(filePath, 'utf-8');
2219
+ const parsed = JSON.parse(content);
2220
+ if (!Array.isArray(parsed)) {
2221
+ return [];
2222
+ }
2223
+ return parsed.filter(isSubdialogResponseRecord);
2224
+ }
2225
+ catch (error) {
2226
+ if (getErrorCode(error) === 'ENOENT') {
2227
+ return [];
2228
+ }
2229
+ throw error;
2230
+ }
2231
+ }
2232
+ static async appendSubdialogResponse(dialogId, response, status = 'running') {
2233
+ const existing = await this.loadSubdialogResponsesQueue(dialogId, status);
2234
+ existing.push(response);
2235
+ await this.saveSubdialogResponses(dialogId, existing, status);
2236
+ }
2237
+ static async takeSubdialogResponses(dialogId, status = 'running') {
2238
+ const dialogPath = this.getDialogResponsesPath(dialogId, status);
2239
+ await fs.promises.mkdir(dialogPath, { recursive: true });
2240
+ const filePath = path.join(dialogPath, 'subdialog-responses.json');
2241
+ const inflightPath = path.join(dialogPath, 'subdialog-responses.processing.json');
2242
+ // If a previous processing file exists, merge it back so it will be re-processed.
2243
+ try {
2244
+ await fs.promises.access(inflightPath);
2245
+ await this.rollbackTakenSubdialogResponses(dialogId, status);
2246
+ }
2247
+ catch {
2248
+ // no-op
2249
+ }
2250
+ try {
2251
+ await fs.promises.rename(filePath, inflightPath);
2252
+ }
2253
+ catch (error) {
2254
+ if (getErrorCode(error) === 'ENOENT') {
2255
+ return [];
2256
+ }
2257
+ throw error;
2258
+ }
2259
+ try {
2260
+ const raw = await fs.promises.readFile(inflightPath, 'utf-8');
2261
+ const parsed = JSON.parse(raw);
2262
+ if (!Array.isArray(parsed)) {
2263
+ return [];
2264
+ }
2265
+ return parsed.filter(isSubdialogResponseRecord);
2266
+ }
2267
+ catch (error) {
2268
+ if (getErrorCode(error) === 'ENOENT') {
2269
+ return [];
2270
+ }
2271
+ throw error;
2272
+ }
2273
+ }
2274
+ static async commitTakenSubdialogResponses(dialogId, status = 'running') {
2275
+ const dialogPath = this.getDialogResponsesPath(dialogId, status);
2276
+ const inflightPath = path.join(dialogPath, 'subdialog-responses.processing.json');
2277
+ await fs.promises.rm(inflightPath, { force: true });
2278
+ }
2279
+ static async rollbackTakenSubdialogResponses(dialogId, status = 'running') {
2280
+ const dialogPath = this.getDialogResponsesPath(dialogId, status);
2281
+ await fs.promises.mkdir(dialogPath, { recursive: true });
2282
+ const filePath = path.join(dialogPath, 'subdialog-responses.json');
2283
+ const inflightPath = path.join(dialogPath, 'subdialog-responses.processing.json');
2284
+ let inflight = [];
2285
+ try {
2286
+ const raw = await fs.promises.readFile(inflightPath, 'utf-8');
2287
+ const parsed = JSON.parse(raw);
2288
+ inflight = Array.isArray(parsed) ? parsed : [];
2289
+ }
2290
+ catch (error) {
2291
+ if (getErrorCode(error) === 'ENOENT') {
2292
+ return;
2293
+ }
2294
+ throw error;
2295
+ }
2296
+ let primary = [];
2297
+ try {
2298
+ const raw = await fs.promises.readFile(filePath, 'utf-8');
2299
+ const parsed = JSON.parse(raw);
2300
+ primary = Array.isArray(parsed) ? parsed : [];
2301
+ }
2302
+ catch (error) {
2303
+ if (getErrorCode(error) !== 'ENOENT') {
2304
+ throw error;
2305
+ }
2306
+ }
2307
+ const merged = [...inflight, ...primary].filter(isSubdialogResponseRecord);
2308
+ const byId = new Map();
2309
+ for (const r of merged) {
2310
+ byId.set(r.responseId, r);
2311
+ }
2312
+ const result = Array.from(byId.values());
2313
+ const tempFile = filePath + '.tmp';
2314
+ await fs.promises.writeFile(tempFile, JSON.stringify(result, null, 2), 'utf-8');
2315
+ await fs.promises.rename(tempFile, filePath);
2316
+ await fs.promises.rm(inflightPath, { force: true });
2317
+ }
2318
+ /**
2319
+ * Save root dialog metadata (write-once pattern)
2320
+ */
2321
+ static async saveRootDialogMetadata(dialogId, metadata, status = 'running') {
2322
+ try {
2323
+ const dialogPath = this.getRootDialogPath(dialogId, status);
2324
+ // Ensure dialog directory exists first
2325
+ await fs.promises.mkdir(dialogPath, { recursive: true });
2326
+ // Atomic write operation
2327
+ const metadataFilePath = path.join(dialogPath, 'dialog.yaml');
2328
+ const tempFile = metadataFilePath + '.tmp';
2329
+ const yamlContent = yaml.stringify(metadata);
2330
+ await fs.promises.writeFile(tempFile, yamlContent, 'utf-8');
2331
+ await fs.promises.rename(tempFile, metadataFilePath);
2332
+ }
2333
+ catch (error) {
2334
+ log_1.log.error(`Failed to save dialog YAML for dialog ${dialogId}:`, error);
2335
+ throw error;
2336
+ }
2337
+ }
2338
+ /**
2339
+ * Save dialog metadata (universal - works with any DialogID)
2340
+ */
2341
+ static async saveDialogMetadata(dialogId, metadata, status = 'running') {
2342
+ if (dialogId.rootId === dialogId.selfId) {
2343
+ if (!isRootDialogMetadataFile(metadata)) {
2344
+ throw new Error(`Expected root dialog metadata for ${dialogId.selfId}`);
2345
+ }
2346
+ return this.saveRootDialogMetadata(dialogId, metadata, status);
2347
+ }
2348
+ // For subdialogs, delegate to saveSubdialogMetadata
2349
+ if (!isSubdialogMetadataFile(metadata)) {
2350
+ throw new Error(`Expected subdialog metadata for ${dialogId.selfId}`);
2351
+ }
2352
+ return this.saveSubdialogMetadata(dialogId, metadata, status);
2353
+ }
2354
+ /**
2355
+ * Save dialog metadata (legacy - use saveRootDialogMetadata instead)
2356
+ * @deprecated
2357
+ */
2358
+ static async _saveDialogMetadata(dialogId, metadata, status = 'running') {
2359
+ return this.saveRootDialogMetadata(dialogId, metadata, status);
2360
+ }
2361
+ /**
2362
+ * Save subdialog metadata under the supdialog's .subdialogs directory
2363
+ */
2364
+ static async saveSubdialogMetadata(dialogId, metadata, status = 'running') {
2365
+ try {
2366
+ const subPath = this.getSubdialogPath(dialogId, status);
2367
+ const metadataFilePath = path.join(subPath, 'dialog.yaml');
2368
+ await fs.promises.mkdir(subPath, { recursive: true });
2369
+ const tempFile = metadataFilePath + '.tmp';
2370
+ const yamlContent = yaml.stringify(metadata);
2371
+ await fs.promises.writeFile(tempFile, yamlContent, 'utf-8');
2372
+ await fs.promises.rename(tempFile, metadataFilePath);
2373
+ }
2374
+ catch (error) {
2375
+ log_1.log.error(`Failed to save subdialog YAML for ${dialogId.selfId} under root dialog ${dialogId.rootId}:`, error);
2376
+ throw error;
2377
+ }
2378
+ }
2379
+ /**
2380
+ * Update assignmentFromSup for an existing subdialog.
2381
+ * Persists both subdialog metadata locations for consistency.
2382
+ */
2383
+ static async updateSubdialogAssignment(dialogId, assignment, status = 'running') {
2384
+ if (dialogId.rootId === dialogId.selfId) {
2385
+ throw new Error('updateSubdialogAssignment expects a subdialog id');
2386
+ }
2387
+ const metadata = await this.loadDialogMetadata(dialogId, status);
2388
+ if (!metadata || !isSubdialogMetadataFile(metadata)) {
2389
+ throw new Error(`Missing dialog metadata for subdialog ${dialogId.selfId}`);
2390
+ }
2391
+ const next = {
2392
+ ...metadata,
2393
+ assignmentFromSup: assignment,
2394
+ };
2395
+ await this.saveSubdialogMetadata(dialogId, next, status);
2396
+ await this.saveDialogMetadata(dialogId, next, status);
2397
+ }
2398
+ /**
2399
+ * Load root dialog metadata
2400
+ */
2401
+ static async loadRootDialogMetadata(dialogId, status = 'running') {
2402
+ try {
2403
+ const dialogPath = this.getRootDialogPath(dialogId, status);
2404
+ const metadataFilePath = path.join(dialogPath, 'dialog.yaml');
2405
+ try {
2406
+ const content = await fs.promises.readFile(metadataFilePath, 'utf-8');
2407
+ const parsed = yaml.parse(content);
2408
+ if (!isDialogMetadataFile(parsed)) {
2409
+ throw new Error(`Invalid dialog metadata in ${metadataFilePath}`);
2410
+ }
2411
+ // Validate that the ID in the file matches the expected dialogId
2412
+ if (parsed.id !== dialogId.selfId) {
2413
+ log_1.log.warn(`Dialog ID mismatch in ${metadataFilePath}: expected ${dialogId.selfId}, got ${parsed.id}`);
2414
+ return null;
2415
+ }
2416
+ return parsed;
2417
+ }
2418
+ catch (error) {
2419
+ if (getErrorCode(error) === 'ENOENT') {
2420
+ return null;
2421
+ }
2422
+ throw error;
2423
+ }
2424
+ }
2425
+ catch (error) {
2426
+ log_1.log.error(`Failed to load dialog YAML for dialog ${dialogId.selfId}:`, error);
2427
+ return null;
2428
+ }
2429
+ }
2430
+ /**
2431
+ * Load dialog metadata (universal - works with any DialogID)
2432
+ */
2433
+ static async loadDialogMetadata(dialogId, status = 'running') {
2434
+ // For root dialogs, use the selfId
2435
+ // For subdialogs, this is more complex - we need to find the root metadata
2436
+ if (dialogId.rootId === dialogId.selfId) {
2437
+ return this.loadRootDialogMetadata(dialogId, status);
2438
+ }
2439
+ // For subdialogs, we need to load from the subdialog location
2440
+ const subdialogPath = this.getSubdialogPath(dialogId, status);
2441
+ const metadataFilePath = path.join(subdialogPath, 'dialog.yaml');
2442
+ try {
2443
+ const content = await fs.promises.readFile(metadataFilePath, 'utf-8');
2444
+ const parsed = yaml.parse(content);
2445
+ if (!isDialogMetadataFile(parsed)) {
2446
+ throw new Error(`Invalid dialog metadata in ${metadataFilePath}`);
2447
+ }
2448
+ return parsed;
2449
+ }
2450
+ catch (error) {
2451
+ if (getErrorCode(error) === 'ENOENT') {
2452
+ return null;
2453
+ }
2454
+ throw error;
2455
+ }
2456
+ }
2457
+ /**
2458
+ * Save latest.yaml with current round and lastModified info
2459
+ */
2460
+ static async writeDialogLatestToDisk(dialogId, latest, status = 'running') {
2461
+ try {
2462
+ const dialogPath = this.getDialogEventsPath(dialogId, status);
2463
+ const latestFilePath = path.join(dialogPath, 'latest.yaml');
2464
+ // Ensure directory exists before writing (handles race conditions and new dialogs)
2465
+ await fs.promises.mkdir(dialogPath, { recursive: true });
2466
+ // NOTE: Use a unique temp file name to avoid collisions when multiple updates
2467
+ // happen concurrently for the same dialog (e.g., parallel tool responses).
2468
+ const tempFile = path.join(dialogPath, `.${path.basename(latestFilePath)}.${process.pid}.${(0, node_crypto_1.randomUUID)()}.tmp`);
2469
+ const yamlContent = yaml.stringify(latest);
2470
+ await fs.promises.writeFile(tempFile, yamlContent, 'utf-8');
2471
+ // Rename with retry logic for filesystem sync issues
2472
+ await this.renameWithRetry(tempFile, latestFilePath, yamlContent);
2473
+ // todo: publish RoundEvent here or where more suitable?
2474
+ }
2475
+ catch (error) {
2476
+ log_1.log.error(`Failed to save latest.yaml for dialog ${dialogId.selfId}:`, error);
2477
+ throw error;
2478
+ }
2479
+ }
2480
+ /**
2481
+ * Rename with retry logic to handle filesystem sync issues
2482
+ */
2483
+ static async renameWithRetry(source, destination, yamlContent, maxRetries = 5) {
2484
+ let lastError;
2485
+ const destinationDir = path.dirname(destination);
2486
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
2487
+ try {
2488
+ // Ensure directory exists (handles race conditions)
2489
+ await fs.promises.mkdir(destinationDir, { recursive: true });
2490
+ // Check if source file exists, re-create if missing
2491
+ try {
2492
+ await fs.promises.access(source);
2493
+ }
2494
+ catch {
2495
+ // Source file missing - re-create it
2496
+ await fs.promises.writeFile(source, yamlContent, 'utf-8');
2497
+ }
2498
+ await fs.promises.rename(source, destination);
2499
+ return;
2500
+ }
2501
+ catch (error) {
2502
+ lastError = error;
2503
+ if (getErrorCode(error) !== 'ENOENT' || attempt === maxRetries) {
2504
+ throw error;
2505
+ }
2506
+ // Exponential backoff for ENOENT (race condition or sync issue)
2507
+ await new Promise((resolve) => setTimeout(resolve, 20 * attempt));
2508
+ }
2509
+ }
2510
+ throw lastError;
2511
+ }
2512
+ /**
2513
+ * Load latest.yaml for current round and lastModified info
2514
+ */
2515
+ static async loadDialogLatest(dialogId, status = 'running') {
2516
+ try {
2517
+ const key = this.getLatestWriteBackKey(dialogId, status);
2518
+ const staged = this.latestWriteBack.get(key);
2519
+ if (staged) {
2520
+ return staged.latest;
2521
+ }
2522
+ const dialogPath = this.getDialogEventsPath(dialogId, status);
2523
+ const latestFilePath = path.join(dialogPath, 'latest.yaml');
2524
+ const content = await fs.promises.readFile(latestFilePath, 'utf-8');
2525
+ const parsed = yaml.parse(content);
2526
+ if (!isDialogLatestFile(parsed)) {
2527
+ throw new Error(`Invalid latest.yaml in ${latestFilePath}`);
2528
+ }
2529
+ return parsed;
2530
+ }
2531
+ catch (error) {
2532
+ if (getErrorCode(error) === 'ENOENT') {
2533
+ return null;
2534
+ }
2535
+ throw error;
2536
+ }
2537
+ }
2538
+ /**
2539
+ * Delta-only latest.yaml update API.
2540
+ *
2541
+ * Callers provide a mutation callback which is applied against the most recent
2542
+ * staged state (or disk fallback). This avoids read-modify-write races in user code
2543
+ * and allows lock-free-ish coalescing to disk via the write-back buffer.
2544
+ */
2545
+ static async mutateDialogLatest(dialogId, mutator, status = 'running') {
2546
+ const key = this.getLatestWriteBackKey(dialogId, status);
2547
+ const mutex = this.getLatestWriteBackMutex(key);
2548
+ const release = await mutex.acquire();
2549
+ try {
2550
+ const staged = this.latestWriteBack.get(key);
2551
+ const existing = (staged
2552
+ ? staged.latest
2553
+ : await this.loadDialogLatestFromDisk(dialogId, status)) || {
2554
+ currentRound: 1,
2555
+ lastModified: (0, time_1.formatUnifiedTimestamp)(new Date()),
2556
+ status: 'active',
2557
+ };
2558
+ const mutation = mutator(existing);
2559
+ let updated;
2560
+ if (mutation.kind === 'noop') {
2561
+ return existing;
2562
+ }
2563
+ else if (mutation.kind === 'replace') {
2564
+ updated = {
2565
+ ...mutation.next,
2566
+ lastModified: (0, time_1.formatUnifiedTimestamp)(new Date()),
2567
+ };
2568
+ }
2569
+ else if (mutation.kind === 'patch') {
2570
+ updated = {
2571
+ ...existing,
2572
+ ...mutation.patch,
2573
+ lastModified: mutation.patch.lastModified || (0, time_1.formatUnifiedTimestamp)(new Date()),
2574
+ };
2575
+ }
2576
+ else {
2577
+ const _exhaustive = mutation;
2578
+ throw new Error(`Unhandled dialog latest mutation: ${String(_exhaustive)}`);
2579
+ }
2580
+ const pending = this.latestWriteBack.get(key);
2581
+ if (!pending) {
2582
+ const timer = setTimeout(() => {
2583
+ void this.flushLatestWriteBack(key);
2584
+ }, this.LATEST_WRITEBACK_WINDOW_MS);
2585
+ this.latestWriteBack.set(key, {
2586
+ kind: 'scheduled',
2587
+ dialogId,
2588
+ status,
2589
+ latest: updated,
2590
+ timer,
2591
+ });
2592
+ return updated;
2593
+ }
2594
+ pending.latest = updated;
2595
+ if (pending.kind === 'flushing') {
2596
+ pending.dirty = true;
2597
+ }
2598
+ // Keep the existing timer to ensure a bounded flush window.
2599
+ return updated;
2600
+ }
2601
+ finally {
2602
+ release();
2603
+ }
2604
+ }
2605
+ static async loadDialogLatestFromDisk(dialogId, status) {
2606
+ try {
2607
+ const dialogPath = this.getDialogEventsPath(dialogId, status);
2608
+ const latestFilePath = path.join(dialogPath, 'latest.yaml');
2609
+ const content = await fs.promises.readFile(latestFilePath, 'utf-8');
2610
+ const parsed = yaml.parse(content);
2611
+ if (!isDialogLatestFile(parsed)) {
2612
+ throw new Error(`Invalid latest.yaml in ${latestFilePath}`);
2613
+ }
2614
+ return parsed;
2615
+ }
2616
+ catch (error) {
2617
+ if (getErrorCode(error) === 'ENOENT') {
2618
+ return null;
2619
+ }
2620
+ throw error;
2621
+ }
2622
+ }
2623
+ static async flushLatestWriteBack(key) {
2624
+ const mutex = this.getLatestWriteBackMutex(key);
2625
+ let captured;
2626
+ {
2627
+ const release = await mutex.acquire();
2628
+ try {
2629
+ const entry = this.latestWriteBack.get(key);
2630
+ if (!entry)
2631
+ return;
2632
+ if (entry.kind === 'flushing')
2633
+ return;
2634
+ if (entry.kind !== 'scheduled')
2635
+ return;
2636
+ clearTimeout(entry.timer);
2637
+ const latestToWrite = entry.latest;
2638
+ const inFlight = this.writeDialogLatestToDisk(entry.dialogId, latestToWrite, entry.status);
2639
+ captured = {
2640
+ dialogId: entry.dialogId,
2641
+ status: entry.status,
2642
+ latestToWrite,
2643
+ inFlight,
2644
+ };
2645
+ this.latestWriteBack.set(key, {
2646
+ kind: 'flushing',
2647
+ dialogId: entry.dialogId,
2648
+ status: entry.status,
2649
+ latest: entry.latest,
2650
+ dirty: false,
2651
+ inFlight,
2652
+ });
2653
+ }
2654
+ finally {
2655
+ release();
2656
+ }
2657
+ }
2658
+ if (!captured)
2659
+ return;
2660
+ try {
2661
+ await captured.inFlight;
2662
+ }
2663
+ catch (error) {
2664
+ const release = await mutex.acquire();
2665
+ try {
2666
+ const entry = this.latestWriteBack.get(key);
2667
+ if (!entry)
2668
+ return;
2669
+ if (entry.kind !== 'flushing')
2670
+ return;
2671
+ if (entry.inFlight !== captured.inFlight)
2672
+ return;
2673
+ const timer = setTimeout(() => {
2674
+ void this.flushLatestWriteBack(key);
2675
+ }, this.LATEST_WRITEBACK_WINDOW_MS);
2676
+ this.latestWriteBack.set(key, {
2677
+ kind: 'scheduled',
2678
+ dialogId: entry.dialogId,
2679
+ status: entry.status,
2680
+ latest: entry.latest,
2681
+ timer,
2682
+ });
2683
+ }
2684
+ finally {
2685
+ release();
2686
+ }
2687
+ return;
2688
+ }
2689
+ const release = await mutex.acquire();
2690
+ try {
2691
+ const entry = this.latestWriteBack.get(key);
2692
+ if (!entry)
2693
+ return;
2694
+ if (entry.kind !== 'flushing')
2695
+ return;
2696
+ if (entry.inFlight !== captured.inFlight)
2697
+ return;
2698
+ if (!entry.dirty) {
2699
+ this.latestWriteBack.delete(key);
2700
+ return;
2701
+ }
2702
+ const timer = setTimeout(() => {
2703
+ void this.flushLatestWriteBack(key);
2704
+ }, this.LATEST_WRITEBACK_WINDOW_MS);
2705
+ this.latestWriteBack.set(key, {
2706
+ kind: 'scheduled',
2707
+ dialogId: entry.dialogId,
2708
+ status: entry.status,
2709
+ latest: entry.latest,
2710
+ timer,
2711
+ });
2712
+ }
2713
+ finally {
2714
+ release();
2715
+ }
2716
+ }
2717
+ static async setNeedsDrive(dialogId, needsDrive, status = 'running') {
2718
+ await this.mutateDialogLatest(dialogId, () => ({ kind: 'patch', patch: { needsDrive } }), status);
2719
+ }
2720
+ static async getNeedsDrive(dialogId, status = 'running') {
2721
+ const latest = await this.loadDialogLatest(dialogId, status);
2722
+ return latest?.needsDrive === true;
2723
+ }
2724
+ // === FILE SYSTEM UTILITIES ===
2725
+ /**
2726
+ * Get round filename from round number
2727
+ */
2728
+ static getRoundFilename(round) {
2729
+ return `round-${round.toString().padStart(3, '0')}.jsonl`;
2730
+ }
2731
+ /**
2732
+ * Extract round number from filename
2733
+ */
2734
+ static getRoundFromFilename(filename) {
2735
+ const match = filename.match(/^round-(\d+)\.jsonl$/);
2736
+ if (!match) {
2737
+ throw new Error(`Invalid round filename: ${filename}`);
2738
+ }
2739
+ return parseInt(match[1], 10);
2740
+ }
2741
+ /**
2742
+ * Get dialog status from file system path
2743
+ */
2744
+ static getStatusFromPath(dialogPath) {
2745
+ const parentDir = path.basename(path.dirname(dialogPath));
2746
+ if (parentDir === this.RUN_DIR)
2747
+ return 'running';
2748
+ if (parentDir === this.DONE_DIR)
2749
+ return 'completed';
2750
+ if (parentDir === this.ARCHIVE_DIR)
2751
+ return 'archived';
2752
+ throw new Error(`Unknown dialog status from path: ${parentDir}`);
2753
+ }
2754
+ static async loadQuestions4Human(dialogId, round, status = 'running') {
2755
+ const questions = await this.loadQuestions4HumanState(dialogId, status);
2756
+ return {
2757
+ round,
2758
+ questions,
2759
+ createdAt: (0, time_1.formatUnifiedTimestamp)(new Date()),
2760
+ updatedAt: (0, time_1.formatUnifiedTimestamp)(new Date()),
2761
+ };
2762
+ }
2763
+ /**
2764
+ * Count subdialogs under a root dialog (no single-layer listing exposed)
2765
+ */
2766
+ static async countAllSubdialogsUnderRoot(rootDialogId, status = 'running') {
2767
+ try {
2768
+ const rootPath = this.getRootDialogPath(rootDialogId, status);
2769
+ const subdialogsPath = path.join(rootPath, this.SUBDIALOGS_DIR);
2770
+ try {
2771
+ const entries = await fs.promises.readdir(subdialogsPath, { withFileTypes: true });
2772
+ return entries.filter((entry) => entry.isDirectory()).length;
2773
+ }
2774
+ catch (error) {
2775
+ if (getErrorCode(error) === 'ENOENT') {
2776
+ return 0;
2777
+ }
2778
+ throw error;
2779
+ }
2780
+ }
2781
+ catch (error) {
2782
+ log_1.log.error(`Failed to count all subdialogs under root ${rootDialogId.selfId}:`, error);
2783
+ return 0;
2784
+ }
2785
+ }
2786
+ // === HIERARCHICAL DIALOG RESTORATION ===
2787
+ /**
2788
+ * Restore complete dialog tree from disk
2789
+ */
2790
+ static async restoreDialogTree(rootDialogId, status = 'running') {
2791
+ try {
2792
+ // First restore the root dialog
2793
+ const rootState = await this.restoreDialog(rootDialogId, status);
2794
+ if (!rootState) {
2795
+ return null;
2796
+ }
2797
+ // Recursively restore subdialogs
2798
+ const rootPath = this.getRootDialogPath(rootDialogId, status);
2799
+ const subdialogsPath = path.join(rootPath, this.SUBDIALOGS_DIR);
2800
+ let subdialogIds = [];
2801
+ try {
2802
+ const entries = await fs.promises.readdir(subdialogsPath, { withFileTypes: true });
2803
+ subdialogIds = entries.filter((e) => e.isDirectory()).map((e) => e.name);
2804
+ }
2805
+ catch (err) {
2806
+ if (getErrorCode(err) !== 'ENOENT') {
2807
+ throw err;
2808
+ }
2809
+ }
2810
+ for (const subdialogId of subdialogIds) {
2811
+ await this.restoreDialogTree(new dialog_1.DialogID(subdialogId, rootDialogId.rootId), status);
2812
+ }
2813
+ return rootState;
2814
+ }
2815
+ catch (error) {
2816
+ log_1.log.error(`Failed to restore dialog tree for ${rootDialogId.valueOf()}:`, error);
2817
+ return null;
2818
+ }
2819
+ }
2820
+ /**
2821
+ * Restore dialog from disk using JSONL events (optimized: only latest round loaded)
2822
+ * For historical rounds, use loadRoundEvents() on-demand for UI navigation
2823
+ */
2824
+ static async restoreDialog(dialogId, status = 'running') {
2825
+ try {
2826
+ const metadata = await this.loadDialogMetadata(dialogId, status);
2827
+ if (!metadata) {
2828
+ log_1.log.debug(`No metadata found for dialog ${dialogId}`);
2829
+ return null;
2830
+ }
2831
+ const reminders = await this.loadReminderState(dialogId, status);
2832
+ // Only load latest round for dialog state restoration
2833
+ const currentRound = await this.getCurrentRoundNumber(dialogId, status);
2834
+ const latestEvents = await this.readRoundEvents(dialogId, currentRound, status);
2835
+ const reconstructedState = await this.rebuildFromEvents(latestEvents, metadata, reminders, currentRound);
2836
+ return reconstructedState;
2837
+ }
2838
+ catch (error) {
2839
+ log_1.log.error(`Failed to restore dialog ${dialogId}:`, error);
2840
+ return null;
2841
+ }
2842
+ }
2843
+ /**
2844
+ * Load specific round events for UI navigation (on-demand)
2845
+ */
2846
+ static async loadRoundEvents(dialogId, round, status = 'running') {
2847
+ return await this.readRoundEvents(dialogId, round, status);
2848
+ }
2849
+ /**
2850
+ * Reconstruct dialog state from JSONL events (optimized: only latest round needed)
2851
+ */
2852
+ static async rebuildFromEvents(events, metadata, reminders, currentRound) {
2853
+ // Events are already in chronological order from JSONL file (append-only pattern)
2854
+ const messages = [];
2855
+ let contextHealth;
2856
+ // Simple, straightforward mapping to reconstruct messages from persisted events
2857
+ for (const event of events) {
2858
+ switch (event.type) {
2859
+ case 'agent_thought_record': {
2860
+ // Convert agent thought to ChatMessage
2861
+ messages.push({
2862
+ type: 'thinking_msg',
2863
+ role: 'assistant',
2864
+ genseq: event.genseq,
2865
+ content: event.content,
2866
+ provider_data: event.provider_data,
2867
+ });
2868
+ break;
2869
+ }
2870
+ case 'agent_words_record': {
2871
+ // Convert agent words to ChatMessage
2872
+ messages.push({
2873
+ type: 'saying_msg',
2874
+ role: 'assistant',
2875
+ genseq: event.genseq,
2876
+ content: event.content,
2877
+ });
2878
+ break;
2879
+ }
2880
+ case 'human_text_record': {
2881
+ // Convert human text to prompting message
2882
+ messages.push({
2883
+ type: 'prompting_msg',
2884
+ role: 'user',
2885
+ genseq: event.genseq,
2886
+ msgId: event.msgId,
2887
+ content: event.content,
2888
+ grammar: event.grammar ?? 'tellask',
2889
+ });
2890
+ break;
2891
+ }
2892
+ case 'func_call_record': {
2893
+ // Convert function call to ChatMessage
2894
+ messages.push({
2895
+ type: 'func_call_msg',
2896
+ role: 'assistant',
2897
+ genseq: event.genseq,
2898
+ id: event.id,
2899
+ name: event.name,
2900
+ arguments: event.arguments ? JSON.stringify(event.arguments) : '{}',
2901
+ });
2902
+ break;
2903
+ }
2904
+ case 'func_result_record': {
2905
+ // Convert function result to ChatMessage
2906
+ messages.push({
2907
+ type: 'func_result_msg',
2908
+ role: 'tool',
2909
+ genseq: event.genseq,
2910
+ id: event.id,
2911
+ name: event.name,
2912
+ content: event.content,
2913
+ });
2914
+ break;
2915
+ }
2916
+ case 'tool_call_result_record': {
2917
+ // Convert tool call result to ChatMessage
2918
+ messages.push({
2919
+ type: 'call_result_msg',
2920
+ role: 'tool',
2921
+ responderId: event.responderId,
2922
+ headLine: event.headLine,
2923
+ status: event.status,
2924
+ content: event.result,
2925
+ });
2926
+ break;
2927
+ }
2928
+ case 'teammate_response_record': {
2929
+ // Convert teammate response to ChatMessage (teammate - separate bubble)
2930
+ // Note: Teammate responses are stored as separate records but use same message type
2931
+ const formattedResult = (0, inter_dialog_format_1.formatTeammateResponseContent)({
2932
+ responderId: event.responderId,
2933
+ requesterId: event.originMemberId,
2934
+ originalCallHeadLine: event.headLine,
2935
+ responseBody: event.response,
2936
+ language: (0, runtime_language_1.getWorkLanguage)(),
2937
+ });
2938
+ messages.push({
2939
+ type: 'call_result_msg',
2940
+ role: 'tool',
2941
+ responderId: event.responderId,
2942
+ headLine: event.headLine,
2943
+ status: event.status,
2944
+ content: formattedResult,
2945
+ });
2946
+ break;
2947
+ }
2948
+ // gen_start_record and gen_finish_record are control events, not message content
2949
+ // They don't need to be converted to ChatMessage objects
2950
+ case 'gen_start_record':
2951
+ break;
2952
+ case 'gen_finish_record':
2953
+ if (event.contextHealth) {
2954
+ contextHealth = event.contextHealth;
2955
+ }
2956
+ break;
2957
+ case 'quest_for_sup_record':
2958
+ // These events are handled separately in dialog restoration
2959
+ // Skip them for message reconstruction
2960
+ break;
2961
+ default:
2962
+ log_1.log.warn(`Unknown event type in rebuildFromEvents`, undefined, { event });
2963
+ break;
2964
+ }
2965
+ }
2966
+ return {
2967
+ metadata,
2968
+ currentRound,
2969
+ messages,
2970
+ reminders,
2971
+ contextHealth,
2972
+ };
2973
+ }
2974
+ /**
2975
+ * Move dialog between status directories (run/done/archive)
2976
+ */
2977
+ static async moveDialogStatus(dialogId, fromStatus, toStatus) {
2978
+ try {
2979
+ const fromPath = path.join(this.getDialogsRootDir(), fromStatus === 'running'
2980
+ ? this.RUN_DIR
2981
+ : fromStatus === 'completed'
2982
+ ? this.DONE_DIR
2983
+ : this.ARCHIVE_DIR, dialogId.selfId);
2984
+ const toPath = path.join(this.getDialogsRootDir(), toStatus === 'running'
2985
+ ? this.RUN_DIR
2986
+ : toStatus === 'completed'
2987
+ ? this.DONE_DIR
2988
+ : this.ARCHIVE_DIR, dialogId.selfId);
2989
+ // Ensure destination directory exists
2990
+ await fs.promises.mkdir(toPath, { recursive: true });
2991
+ // Move all files and directories
2992
+ const entries = await fs.promises.readdir(fromPath, { withFileTypes: true });
2993
+ for (const entry of entries) {
2994
+ const srcPath = path.join(fromPath, entry.name);
2995
+ const destPath = path.join(toPath, entry.name);
2996
+ await fs.promises.rename(srcPath, destPath);
2997
+ }
2998
+ // Remove the (now-empty) source directory so the dialog is not detected under both statuses.
2999
+ await fs.promises.rm(fromPath, { recursive: true, force: true });
3000
+ }
3001
+ catch (error) {
3002
+ log_1.log.error(`Failed to move dialog ${dialogId} from ${fromStatus} to ${toStatus}:`, error);
3003
+ throw error;
3004
+ }
3005
+ }
3006
+ /**
3007
+ * Find the current persistence status directory for a root dialog.
3008
+ * Root dialogs are stored under exactly one of: run/ | done/ | archive/
3009
+ */
3010
+ static async findRootDialogStatus(rootDialogId) {
3011
+ if (rootDialogId.selfId !== rootDialogId.rootId) {
3012
+ throw new Error('Expected root dialog id (selfId must equal rootId)');
3013
+ }
3014
+ const candidates = [
3015
+ { status: 'running', dirName: this.RUN_DIR },
3016
+ { status: 'completed', dirName: this.DONE_DIR },
3017
+ { status: 'archived', dirName: this.ARCHIVE_DIR },
3018
+ ];
3019
+ for (const candidate of candidates) {
3020
+ const candidatePath = path.join(this.getDialogsRootDir(), candidate.dirName, rootDialogId.selfId);
3021
+ try {
3022
+ // `run/` can contain stray placeholder directories (e.g. created by status-agnostic metadata updates).
3023
+ // Treat a status as valid only if it contains the root dialog's dialog.yaml.
3024
+ const dialogYamlPath = path.join(candidatePath, 'dialog.yaml');
3025
+ const st = await fs.promises.stat(dialogYamlPath);
3026
+ if (st.isFile()) {
3027
+ return candidate.status;
3028
+ }
3029
+ }
3030
+ catch (error) {
3031
+ if (getErrorCode(error) === 'ENOENT') {
3032
+ continue;
3033
+ }
3034
+ throw error;
3035
+ }
3036
+ }
3037
+ return null;
3038
+ }
3039
+ /**
3040
+ * Delete a root dialog directory (including subdialogs) from disk.
3041
+ * Returns the status directory the dialog was deleted from, or null if not found.
3042
+ */
3043
+ static async deleteRootDialog(rootDialogId) {
3044
+ const status = await this.findRootDialogStatus(rootDialogId);
3045
+ if (!status)
3046
+ return null;
3047
+ // Best-effort cleanup: remove the dialog from all status directories to avoid leaving behind
3048
+ // orphaned placeholder paths (e.g. `run/<id>/latest.yaml`) after a delete.
3049
+ const allStatuses = [
3050
+ 'running',
3051
+ 'completed',
3052
+ 'archived',
3053
+ ];
3054
+ for (const candidate of allStatuses) {
3055
+ const candidatePath = this.getRootDialogPath(rootDialogId, candidate);
3056
+ await fs.promises.rm(candidatePath, { recursive: true, force: true });
3057
+ }
3058
+ return status;
3059
+ }
3060
+ // === REGISTRY PERSISTENCE ===
3061
+ /**
3062
+ * Save subdialog registry (TYPE B entries).
3063
+ */
3064
+ static async saveSubdialogRegistry(rootDialogId, entries, status = 'running') {
3065
+ try {
3066
+ const dialogPath = this.getDialogResponsesPath(rootDialogId, status);
3067
+ const registryFilePath = path.join(dialogPath, 'registry.yaml');
3068
+ await fs.promises.mkdir(dialogPath, { recursive: true });
3069
+ const serializableEntries = entries.map((entry) => ({
3070
+ key: entry.key,
3071
+ subdialogId: entry.subdialogId.selfId,
3072
+ agentId: entry.agentId,
3073
+ tellaskSession: entry.tellaskSession,
3074
+ }));
3075
+ const tempFile = registryFilePath + '.tmp';
3076
+ const yamlContent = yaml.stringify({ entries: serializableEntries });
3077
+ await fs.promises.writeFile(tempFile, yamlContent, 'utf-8');
3078
+ await fs.promises.rename(tempFile, registryFilePath);
3079
+ }
3080
+ catch (error) {
3081
+ log_1.log.error(`Failed to save subdialog registry for dialog ${rootDialogId}:`, error);
3082
+ throw error;
3083
+ }
3084
+ }
3085
+ /**
3086
+ * Load subdialog registry.
3087
+ */
3088
+ static async loadSubdialogRegistry(rootDialogId, status = 'running') {
3089
+ try {
3090
+ const dialogPath = this.getDialogResponsesPath(rootDialogId, status);
3091
+ const registryFilePath = path.join(dialogPath, 'registry.yaml');
3092
+ const content = await fs.promises.readFile(registryFilePath, 'utf-8');
3093
+ const parsed = yaml.parse(content);
3094
+ if (!isRecord(parsed) || !Array.isArray(parsed.entries)) {
3095
+ log_1.log.warn(`Invalid registry.yaml format for dialog ${rootDialogId}`);
3096
+ return [];
3097
+ }
3098
+ const entries = parsed.entries.map((entry) => {
3099
+ if (!isRecord(entry)) {
3100
+ throw new Error('Invalid registry entry');
3101
+ }
3102
+ return {
3103
+ key: entry.key,
3104
+ subdialogId: new dialog_1.DialogID(entry.subdialogId, rootDialogId.rootId),
3105
+ agentId: entry.agentId,
3106
+ tellaskSession: entry.tellaskSession,
3107
+ };
3108
+ });
3109
+ return entries;
3110
+ }
3111
+ catch (error) {
3112
+ if (getErrorCode(error) === 'ENOENT') {
3113
+ return [];
3114
+ }
3115
+ log_1.log.error(`Failed to load subdialog registry for dialog ${rootDialogId}:`, error);
3116
+ return [];
3117
+ }
3118
+ }
3119
+ }
3120
+ exports.DialogPersistence = DialogPersistence;
3121
+ DialogPersistence.DIALOGS_DIR = '.dialogs';
3122
+ DialogPersistence.RUN_DIR = 'run';
3123
+ DialogPersistence.DONE_DIR = 'done';
3124
+ DialogPersistence.ARCHIVE_DIR = 'archive';
3125
+ DialogPersistence.SUBDIALOGS_DIR = 'subdialogs';
3126
+ DialogPersistence.LATEST_WRITEBACK_WINDOW_MS = 300;
3127
+ DialogPersistence.latestWriteBackMutexes = new Map();
3128
+ DialogPersistence.latestWriteBack = new Map();